@scttlnd/fintech-design-system 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/AGENTS.md +15 -0
  2. package/README.md +59 -2
  3. package/dist/components/Badge.d.mts +9 -0
  4. package/dist/components/Badge.mjs +18 -0
  5. package/dist/components/Badge.mjs.map +1 -0
  6. package/dist/components/Button.d.mts +6 -0
  7. package/dist/components/Button.mjs +14 -0
  8. package/dist/components/Button.mjs.map +1 -0
  9. package/dist/components/Field.d.mts +20 -0
  10. package/dist/components/Field.mjs +57 -0
  11. package/dist/components/Field.mjs.map +1 -0
  12. package/dist/components/Menu.d.mts +18 -0
  13. package/dist/components/Menu.mjs +109 -0
  14. package/dist/components/Menu.mjs.map +1 -0
  15. package/dist/components/Meter.d.mts +39 -0
  16. package/dist/components/Meter.mjs +82 -0
  17. package/dist/components/Meter.mjs.map +1 -0
  18. package/dist/components/Metric.d.mts +17 -0
  19. package/dist/components/Metric.mjs +34 -0
  20. package/dist/components/Metric.mjs.map +1 -0
  21. package/dist/components/Notice.d.mts +16 -0
  22. package/dist/components/Notice.mjs +48 -0
  23. package/dist/components/Notice.mjs.map +1 -0
  24. package/dist/components/Panel.d.mts +13 -0
  25. package/dist/components/Panel.mjs +42 -0
  26. package/dist/components/Panel.mjs.map +1 -0
  27. package/dist/index.d.mts +10 -0
  28. package/dist/index.mjs +10 -0
  29. package/dist/lib/cn.d.mts +2 -0
  30. package/dist/lib/cn.mjs +7 -0
  31. package/dist/lib/cn.mjs.map +1 -0
  32. package/package.json +72 -4
  33. package/styles/components/badge.css +33 -0
  34. package/styles/components/button.css +45 -0
  35. package/styles/components/empty-state.css +20 -0
  36. package/styles/components/eyebrow.css +11 -0
  37. package/styles/components/field.css +59 -0
  38. package/styles/components/grain.css +245 -0
  39. package/styles/components/menu.css +74 -0
  40. package/styles/components/meter.css +62 -0
  41. package/styles/components/metric.css +28 -0
  42. package/styles/components/notice.css +34 -0
  43. package/styles/components/panel.css +42 -0
  44. package/styles/components/tone.css +15 -0
  45. package/styles/components.css +11 -0
  46. package/styles/index.css +4 -0
  47. package/styles/themes/funder.css +77 -0
  48. package/styles/themes/investor.css +77 -0
  49. package/styles/themes/portare.css +92 -0
  50. package/styles/tokens.css +90 -0
@@ -0,0 +1,48 @@
1
+ import { cn } from "../lib/cn.mjs";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ // src/components/Notice.tsx
4
+ /** Inline message. `danger` announces as an alert; other tones as polite status. */
5
+ function Notice({ tone = "info", title, action, className, children, role, ...props }) {
6
+ return /* @__PURE__ */ jsxs("div", {
7
+ role: role ?? (tone === "danger" ? "alert" : "status"),
8
+ className: cn("fds-notice", `fds-notice--${tone}`, className),
9
+ ...props,
10
+ children: [/* @__PURE__ */ jsxs("div", {
11
+ className: "fds-notice__body",
12
+ children: [title ? /* @__PURE__ */ jsx("p", {
13
+ className: "fds-notice__title",
14
+ children: title
15
+ }) : null, children ? /* @__PURE__ */ jsx("div", {
16
+ className: "fds-notice__text",
17
+ children
18
+ }) : null]
19
+ }), action ? /* @__PURE__ */ jsx("div", {
20
+ className: "fds-notice__action",
21
+ children: action
22
+ }) : null]
23
+ });
24
+ }
25
+ /** Placeholder for lists or panels with no data, with an optional next step. */
26
+ function EmptyState({ title, description, action, className, ...props }) {
27
+ return /* @__PURE__ */ jsxs("div", {
28
+ className: cn("fds-empty", className),
29
+ ...props,
30
+ children: [
31
+ /* @__PURE__ */ jsx("p", {
32
+ className: "fds-empty__title",
33
+ children: title
34
+ }),
35
+ description ? /* @__PURE__ */ jsx("p", {
36
+ className: "fds-empty__description",
37
+ children: description
38
+ }) : null,
39
+ action ? /* @__PURE__ */ jsx("div", {
40
+ className: "fds-empty__action",
41
+ children: action
42
+ }) : null
43
+ ]
44
+ });
45
+ }
46
+ export { EmptyState, Notice };
47
+
48
+ //# sourceMappingURL=Notice.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Notice.mjs","names":[],"sources":["../../src/components/Notice.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn.js\";\n\nexport interface NoticeProps extends Omit<HTMLAttributes<HTMLDivElement>, \"title\"> {\n tone?: \"info\" | \"success\" | \"warn\" | \"danger\";\n title?: ReactNode;\n action?: ReactNode;\n}\n\n/** Inline message. `danger` announces as an alert; other tones as polite status. */\nexport function Notice({\n tone = \"info\",\n title,\n action,\n className,\n children,\n role,\n ...props\n}: NoticeProps) {\n return (\n <div\n role={role ?? (tone === \"danger\" ? \"alert\" : \"status\")}\n className={cn(\"fds-notice\", `fds-notice--${tone}`, className)}\n {...props}\n >\n <div className=\"fds-notice__body\">\n {title ? <p className=\"fds-notice__title\">{title}</p> : null}\n {children ? <div className=\"fds-notice__text\">{children}</div> : null}\n </div>\n {action ? <div className=\"fds-notice__action\">{action}</div> : null}\n </div>\n );\n}\n\nexport interface EmptyStateProps extends Omit<HTMLAttributes<HTMLDivElement>, \"title\"> {\n title: ReactNode;\n description?: ReactNode;\n action?: ReactNode;\n}\n\n/** Placeholder for lists or panels with no data, with an optional next step. */\nexport function EmptyState({ title, description, action, className, ...props }: EmptyStateProps) {\n return (\n <div className={cn(\"fds-empty\", className)} {...props}>\n <p className=\"fds-empty__title\">{title}</p>\n {description ? <p className=\"fds-empty__description\">{description}</p> : null}\n {action ? <div className=\"fds-empty__action\">{action}</div> : null}\n </div>\n );\n}\n"],"mappings":";;;;AAUA,SAAgB,OAAO,EACrB,OAAO,QACP,OACA,QACA,WACA,UACA,MACA,GAAG,SACW;CACd,OACE,qBAAC,OAAD;EACE,MAAM,SAAS,SAAS,WAAW,UAAU;EAC7C,WAAW,GAAG,cAAc,eAAe,QAAQ,SAAS;EAC5D,GAAI;EAHN,UAAA,CAKE,qBAAC,OAAD;GAAK,WAAU;GAAf,UAAA,CACG,QAAQ,oBAAC,KAAD;IAAG,WAAU;IAAqB,UAAA;GAAS,CAAA,IAAI,MACvD,WAAW,oBAAC,OAAD;IAAK,WAAU;IAAoB;GAAc,CAAA,IAAI,IAC9D;EACJ,CAAA,GAAA,SAAS,oBAAC,OAAD;GAAK,WAAU;GAAsB,UAAA;EAAY,CAAA,IAAI,IAC5D;;AAET;;AASA,SAAgB,WAAW,EAAE,OAAO,aAAa,QAAQ,WAAW,GAAG,SAA0B;CAC/F,OACE,qBAAC,OAAD;EAAK,WAAW,GAAG,aAAa,SAAS;EAAG,GAAI;EAAhD,UAAA;GACE,oBAAC,KAAD;IAAG,WAAU;IAAoB,UAAA;GAAS,CAAA;GACzC,cAAc,oBAAC,KAAD;IAAG,WAAU;IAA0B,UAAA;GAAe,CAAA,IAAI;GACxE,SAAS,oBAAC,OAAD;IAAK,WAAU;IAAqB,UAAA;GAAY,CAAA,IAAI;EAC3D;;AAET"}
@@ -0,0 +1,13 @@
1
+ import { HTMLAttributes, ReactNode } from "react";
2
+ export interface PanelProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
3
+ title: ReactNode;
4
+ eyebrow?: ReactNode;
5
+ help?: ReactNode;
6
+ /** Optional trailing action area (buttons, menu). */
7
+ action?: ReactNode;
8
+ /** Heading element for the title. Defaults to h2; pick the level that fits the page outline. */
9
+ headingLevel?: 2 | 3 | 4;
10
+ }
11
+ /** Titled surface with optional eyebrow, help text and action. Renders a labelled `<section>`. */
12
+ export declare function Panel({ title, eyebrow, help, action, headingLevel, className, children, ...props }: PanelProps): import("react").JSX.Element;
13
+ //# sourceMappingURL=Panel.d.mts.map
@@ -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"}
@@ -0,0 +1,10 @@
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 ADDED
@@ -0,0 +1,10 @@
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";
10
+ export { Badge, Button, DistributionList, EmptyState, Field, Input, KeyValue, Menu, Meter, Metric, Notice, Panel, SegmentedMeter, Select, Textarea, cn };
@@ -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,74 @@
1
1
  {
2
2
  "name": "@scttlnd/fintech-design-system",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.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
+ "typecheck": "tsc --noEmit",
39
+ "changeset": "changeset",
40
+ "version": "changeset version && vp check --fix",
41
+ "release": "changeset publish",
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"
51
+ },
52
+ "devDependencies": {
53
+ "@changesets/changelog-github": "^1.0.1",
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",
58
+ "@types/react": "^18.3.18",
59
+ "@types/react-dom": "^18.3.7",
60
+ "axe-core": "^4.14.0",
61
+ "happy-dom": "^21.0.4",
62
+ "react": "^18.3.1",
63
+ "react-dom": "^18.3.1",
64
+ "typescript": "^5.7.3",
65
+ "vite-plus": "^1.5.0",
66
+ "vitest-axe": "^0.1.0"
67
+ },
68
+ "peerDependencies": {
69
+ "react": "^18.3.0 || ^19.0.0"
70
+ },
71
+ "engines": {
72
+ "node": ">=22.14.0"
73
+ }
74
+ }
@@ -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
+ }
@@ -0,0 +1,45 @@
1
+ @layer fds {
2
+ .fds-button {
3
+ display: inline-flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+ gap: 0.5rem;
7
+ min-height: var(--fds-control-h);
8
+ padding: 0.55rem 1rem;
9
+ border-radius: var(--fds-radius-md);
10
+ font: inherit;
11
+ font-weight: 600;
12
+ cursor: pointer;
13
+ transition:
14
+ background-color var(--fds-duration-fast) var(--fds-ease-out),
15
+ border-color var(--fds-duration-fast) var(--fds-ease-out);
16
+ }
17
+ .fds-button:focus-visible {
18
+ outline: 2px solid var(--fds-focus-ring);
19
+ outline-offset: 2px;
20
+ }
21
+ .fds-button:disabled {
22
+ opacity: 0.5;
23
+ cursor: not-allowed;
24
+ }
25
+ .fds-button--primary {
26
+ background: var(--fds-accent);
27
+ color: var(--fds-on-accent);
28
+ border: 1px solid transparent;
29
+ }
30
+ .fds-button--secondary {
31
+ background: var(--fds-panel-raised);
32
+ color: var(--fds-foreground);
33
+ border: 1px solid var(--fds-line);
34
+ }
35
+ .fds-button--ghost {
36
+ background: transparent;
37
+ color: var(--fds-foreground);
38
+ border: 1px solid transparent;
39
+ }
40
+ @media (prefers-reduced-motion: reduce) {
41
+ .fds-button {
42
+ transition: none;
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,20 @@
1
+ @layer fds {
2
+ /* Empty state */
3
+ .fds-empty {
4
+ padding: 2rem 1rem;
5
+ text-align: center;
6
+ }
7
+ .fds-empty__title {
8
+ margin: 0;
9
+ font-weight: 600;
10
+ }
11
+ .fds-empty__description {
12
+ margin: 0.5rem auto 0;
13
+ max-width: 45ch;
14
+ font-size: 0.875rem;
15
+ color: var(--fds-muted);
16
+ }
17
+ .fds-empty__action {
18
+ margin-top: 1rem;
19
+ }
20
+ }
@@ -0,0 +1,11 @@
1
+ @layer fds {
2
+ /* Shared primitives. Only --fds-* tokens; tones are derived with color-mix so every theme works. */
3
+
4
+ .fds-eyebrow {
5
+ margin: 0;
6
+ font-size: 0.75rem;
7
+ letter-spacing: 0.06em;
8
+ text-transform: uppercase;
9
+ color: var(--fds-muted);
10
+ }
11
+ }
@@ -0,0 +1,59 @@
1
+ @layer fds {
2
+ /* Form controls: 44px targets on touch / small screens, 16px font to stop iOS zoom. */
3
+ .fds-field {
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: 0.375rem;
7
+ }
8
+ .fds-field__label {
9
+ font-size: 0.875rem;
10
+ font-weight: 500;
11
+ }
12
+ .fds-field__hint,
13
+ .fds-field__error {
14
+ margin: 0;
15
+ font-size: 0.8125rem;
16
+ }
17
+ .fds-field__hint {
18
+ color: var(--fds-muted);
19
+ }
20
+ .fds-field__error {
21
+ color: var(--fds-danger);
22
+ }
23
+ .fds-control {
24
+ width: 100%;
25
+ min-height: 2.75rem;
26
+ padding: 0.5rem 0.75rem;
27
+ border: 1px solid var(--fds-line);
28
+ border-radius: var(--fds-radius-md);
29
+ background: var(--fds-panel-soft, var(--fds-panel));
30
+ color: var(--fds-foreground);
31
+ font: inherit;
32
+ font-size: 1rem;
33
+ }
34
+ .fds-control--multiline {
35
+ min-height: 6rem;
36
+ resize: vertical;
37
+ }
38
+ .fds-control:focus-visible {
39
+ outline: 2px solid var(--fds-focus-ring);
40
+ outline-offset: 2px;
41
+ border-color: var(--fds-accent);
42
+ }
43
+ .fds-control[aria-invalid="true"] {
44
+ border-color: var(--fds-danger);
45
+ }
46
+ .fds-control:disabled {
47
+ opacity: 0.5;
48
+ cursor: not-allowed;
49
+ }
50
+ @media (min-width: 640px) and (pointer: fine) {
51
+ .fds-control {
52
+ min-height: 2.25rem;
53
+ font-size: 0.875rem;
54
+ }
55
+ .fds-control--multiline {
56
+ min-height: 6rem;
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,245 @@
1
+ @layer fds {
2
+ /*
3
+ * Grain surfaces: a quiet, hairline-bordered surface with a soft two-colour glow and fine film grain.
4
+ * On by default for every .fds-panel; opt out with data-fds-grain="off" on the element or any ancestor
5
+ * (for example <html>). Vivid, hero and the other modifiers are explicit classes on .fds-grain.
6
+ * Colours derive from the active theme (accent, panel, line), so every theme works. `light-dark()`
7
+ * follows the theme's `color-scheme`, so no per-theme overrides are needed.
8
+ */
9
+ /* Browsers without light-dark() keep the plain panel instead of an invalid background. */
10
+ @supports (color: light-dark(#000, #fff)) {
11
+ :root,
12
+ [data-fds-theme] {
13
+ --fds-glow-a: var(--fds-accent-strong);
14
+ --fds-glow-b: light-dark(#6941a5, #a78bfa);
15
+ --fds-glow-warm-a: var(--fds-warn);
16
+ --fds-glow-warm-b: var(--fds-danger);
17
+ /* Glow share (light / dark scheme). Kept low so text contrast is unaffected. */
18
+ --fds-glow-l: 16%;
19
+ --fds-glow-d: 26%;
20
+ /* Grain alpha (light / dark scheme). Grain is a tinted alpha mask: dark specks on light, light on dark. */
21
+ --fds-grain-l: 0.16;
22
+ --fds-grain-d: 0.09;
23
+ --fds-grain-size: 160px;
24
+ --fds-grain-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1.9 -.55'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
25
+ /* Vivid hues: saturated colour fields with light streaks. Decoration only; put content on a stage. */
26
+ --fds-vivid-indigo-1: #4f30e8;
27
+ --fds-vivid-indigo-2: #8b8ffb;
28
+ --fds-vivid-teal-1: #0d9488;
29
+ --fds-vivid-teal-2: #5eead4;
30
+ --fds-vivid-green-1: #15803d;
31
+ --fds-vivid-green-2: #86efac;
32
+ --fds-vivid-ember-1: #c2410c;
33
+ --fds-vivid-ember-2: #fdba74;
34
+ --fds-stage-bg: #0d0e12;
35
+ --fds-stage-fg: #f4f4f5;
36
+ --fds-stage-muted: #a1a1aa;
37
+ --fds-stage-line: #26272d;
38
+ }
39
+
40
+ .fds-grain:not(:where([data-fds-grain="off"], [data-fds-grain="off"] *)),
41
+ .fds-panel:not(:where([data-fds-grain="off"], [data-fds-grain="off"] *)) {
42
+ position: relative;
43
+ isolation: isolate;
44
+ border: 1px solid var(--fds-line);
45
+ border-radius: var(--fds-radius-lg);
46
+ color: var(--fds-foreground);
47
+ --fds-tint-1: light-dark(
48
+ color-mix(in oklab, var(--fds-glow-1, var(--fds-glow-a)) var(--fds-glow-l), transparent),
49
+ color-mix(in oklab, var(--fds-glow-1, var(--fds-glow-a)) var(--fds-glow-d), transparent)
50
+ );
51
+ --fds-tint-2: light-dark(
52
+ color-mix(in oklab, var(--fds-glow-2, var(--fds-glow-b)) var(--fds-glow-l), transparent),
53
+ color-mix(in oklab, var(--fds-glow-2, var(--fds-glow-b)) var(--fds-glow-d), transparent)
54
+ );
55
+ background:
56
+ radial-gradient(120% 100% at 0% 0%, var(--fds-tint-1), transparent 62%),
57
+ radial-gradient(100% 90% at 100% 100%, var(--fds-tint-2), transparent 58%), var(--fds-panel);
58
+ }
59
+
60
+ /* Grain sits above the gradient and below the content, so text stays crisp. */
61
+ .fds-grain:not(:where([data-fds-grain="off"], [data-fds-grain="off"] *))::before,
62
+ .fds-panel:not(:where([data-fds-grain="off"], [data-fds-grain="off"] *))::before {
63
+ content: "";
64
+ position: absolute;
65
+ inset: 0;
66
+ z-index: -1;
67
+ border-radius: inherit;
68
+ pointer-events: none;
69
+ background: light-dark(
70
+ rgb(0 0 0 / var(--fds-grain-l)),
71
+ rgb(255 255 255 / var(--fds-grain-d))
72
+ );
73
+ -webkit-mask-image: var(--fds-grain-image);
74
+ mask-image: var(--fds-grain-image);
75
+ -webkit-mask-size: var(--fds-grain-size) var(--fds-grain-size);
76
+ mask-size: var(--fds-grain-size) var(--fds-grain-size);
77
+ }
78
+
79
+ .fds-grain--warm {
80
+ --fds-glow-1: var(--fds-glow-warm-a);
81
+ --fds-glow-2: var(--fds-glow-warm-b);
82
+ }
83
+ .fds-grain--subtle {
84
+ --fds-glow-l: 8%;
85
+ --fds-glow-d: 14%;
86
+ --fds-grain-l: 0.1;
87
+ --fds-grain-d: 0.05;
88
+ }
89
+ .fds-grain--hero {
90
+ --fds-glow-l: 20%;
91
+ --fds-glow-d: 34%;
92
+ padding: clamp(1.5rem, 5vw, 3.5rem);
93
+ border-radius: calc(var(--fds-radius-lg) * 1.5);
94
+ }
95
+
96
+ /*
97
+ * Vivid: a saturated colour field with diagonal light streaks and strong grain, for hero bands and
98
+ * feature panels. Text must sit on a .fds-grain__stage (a near-black inset) so contrast never depends
99
+ * on the gradient. Hues: --indigo (default), --teal, --green, --ember.
100
+ */
101
+ .fds-grain--vivid {
102
+ --fds-v1: var(--fds-vivid-indigo-1);
103
+ --fds-v2: var(--fds-vivid-indigo-2);
104
+ --fds-grain-l: 0.3;
105
+ --fds-grain-d: 0.3;
106
+ padding: clamp(1rem, 4vw, 2.5rem);
107
+ border-color: transparent;
108
+ background:
109
+ linear-gradient(118deg, transparent 30%, rgb(255 255 255 / 0.28) 30.4%, transparent 31.6%),
110
+ linear-gradient(118deg, transparent 52%, rgb(255 255 255 / 0.18) 52.3%, transparent 53.2%),
111
+ linear-gradient(62deg, transparent 64%, rgb(255 255 255 / 0.22) 64.4%, transparent 65.6%),
112
+ radial-gradient(90% 80% at 15% 10%, var(--fds-v2), transparent 60%),
113
+ radial-gradient(
114
+ 80% 90% at 90% 95%,
115
+ color-mix(in oklab, var(--fds-v2) 70%, black),
116
+ transparent 65%
117
+ ),
118
+ linear-gradient(160deg, var(--fds-v1), color-mix(in oklab, var(--fds-v1) 55%, black));
119
+ }
120
+ .fds-panel.fds-grain--vivid {
121
+ padding: clamp(1rem, 4vw, 2.5rem);
122
+ }
123
+ .fds-grain--teal {
124
+ --fds-v1: var(--fds-vivid-teal-1);
125
+ --fds-v2: var(--fds-vivid-teal-2);
126
+ }
127
+ .fds-grain--green {
128
+ --fds-v1: var(--fds-vivid-green-1);
129
+ --fds-v2: var(--fds-vivid-green-2);
130
+ }
131
+ .fds-grain--ember {
132
+ --fds-v1: var(--fds-vivid-ember-1);
133
+ --fds-v2: var(--fds-vivid-ember-2);
134
+ }
135
+
136
+ /* Near-black inset that carries the content on a vivid field. Same colours in every theme. */
137
+ .fds-grain__stage {
138
+ padding: var(--fds-space-6);
139
+ border: 1px solid var(--fds-stage-line);
140
+ border-radius: var(--fds-radius-md);
141
+ background: var(--fds-stage-bg);
142
+ color: var(--fds-stage-fg);
143
+ font-family: var(--fds-font-mono);
144
+ font-size: var(--fds-text-sm);
145
+ line-height: var(--fds-leading-relaxed);
146
+ box-shadow: 0 18px 48px rgb(0 0 0 / 0.35);
147
+ }
148
+ .fds-grain__stage .fds-eyebrow,
149
+ .fds-grain__stage p {
150
+ color: var(--fds-stage-muted);
151
+ }
152
+
153
+ /* Flat hairline cells: dividers instead of shadowed cards. */
154
+ .fds-cells {
155
+ display: grid;
156
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
157
+ gap: 1px;
158
+ border-block: 1px solid var(--fds-line);
159
+ background: var(--fds-line);
160
+ }
161
+ .fds-cells > * {
162
+ padding: var(--fds-space-6);
163
+ background: var(--fds-background);
164
+ }
165
+
166
+ /* Inline mono chip for commands and identifiers. */
167
+ .fds-code {
168
+ padding: 0.1em 0.45em;
169
+ border: 1px solid var(--fds-line);
170
+ border-radius: var(--fds-radius-sm);
171
+ background: var(--fds-panel-soft);
172
+ font-family: var(--fds-font-mono);
173
+ font-size: 0.875em;
174
+ }
175
+
176
+ /*
177
+ * Components sit on top of the grain: it is background only. Any component fill that is translucent
178
+ * would let the grain show through, so inside a grain surface those fills are mixed with the panel
179
+ * colour instead of transparent. The result matches the flat-panel look, but is opaque.
180
+ */
181
+ :is(.fds-grain, .fds-panel):not(:where([data-fds-grain="off"], [data-fds-grain="off"] *))
182
+ .fds-badge {
183
+ background: color-mix(in srgb, var(--fds-badge-color) 12%, var(--fds-panel));
184
+ border-color: color-mix(in srgb, var(--fds-badge-color) 30%, var(--fds-panel));
185
+ }
186
+ :is(.fds-grain, .fds-panel):not(:where([data-fds-grain="off"], [data-fds-grain="off"] *))
187
+ .fds-notice {
188
+ background: color-mix(in srgb, var(--fds-notice-color) 10%, var(--fds-panel));
189
+ border-color: color-mix(in srgb, var(--fds-notice-color) 35%, var(--fds-panel));
190
+ }
191
+ /* Disabled controls use opacity: 0.5, which is see-through. Reproduce it as opaque mixes instead. */
192
+ :is(.fds-grain, .fds-panel):not(:where([data-fds-grain="off"], [data-fds-grain="off"] *))
193
+ :is(.fds-button, .fds-control):disabled {
194
+ opacity: 1;
195
+ }
196
+ :is(.fds-grain, .fds-panel):not(:where([data-fds-grain="off"], [data-fds-grain="off"] *))
197
+ .fds-button--primary:disabled {
198
+ background: color-mix(in srgb, var(--fds-accent) 50%, var(--fds-panel));
199
+ color: color-mix(in srgb, var(--fds-on-accent) 50%, var(--fds-panel));
200
+ }
201
+ :is(.fds-grain, .fds-panel):not(:where([data-fds-grain="off"], [data-fds-grain="off"] *))
202
+ .fds-button--secondary:disabled {
203
+ background: color-mix(in srgb, var(--fds-panel-raised) 50%, var(--fds-panel));
204
+ border-color: color-mix(in srgb, var(--fds-line) 50%, var(--fds-panel));
205
+ color: color-mix(in srgb, var(--fds-foreground) 50%, var(--fds-panel));
206
+ }
207
+ :is(.fds-grain, .fds-panel):not(:where([data-fds-grain="off"], [data-fds-grain="off"] *))
208
+ .fds-button--ghost:disabled {
209
+ color: color-mix(in srgb, var(--fds-foreground) 50%, var(--fds-panel));
210
+ }
211
+ :is(.fds-grain, .fds-panel):not(:where([data-fds-grain="off"], [data-fds-grain="off"] *))
212
+ .fds-control:disabled {
213
+ background: color-mix(in srgb, var(--fds-panel-soft, var(--fds-panel)) 50%, var(--fds-panel));
214
+ border-color: color-mix(in srgb, var(--fds-line) 50%, var(--fds-panel));
215
+ color: color-mix(in srgb, var(--fds-foreground) 50%, var(--fds-panel));
216
+ }
217
+
218
+ /* Quiet, technical labelling inside grain surfaces. */
219
+ .fds-grain .fds-eyebrow,
220
+ .fds-panel:not(:where([data-fds-grain="off"], [data-fds-grain="off"] *)) .fds-eyebrow {
221
+ font-family: var(--fds-font-mono);
222
+ letter-spacing: 0.08em;
223
+ }
224
+
225
+ /* Decoration only: drop it where it cannot help. */
226
+ @media (forced-colors: active) {
227
+ .fds-grain,
228
+ .fds-panel.fds-grain {
229
+ background: Canvas;
230
+ }
231
+ .fds-grain::before {
232
+ display: none;
233
+ }
234
+ }
235
+ @media print {
236
+ .fds-grain,
237
+ .fds-panel.fds-grain {
238
+ background: none;
239
+ }
240
+ .fds-grain::before {
241
+ display: none;
242
+ }
243
+ }
244
+ }
245
+ }