@flopay/ui 0.3.0 → 0.5.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/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ClassValue } from 'clsx';
2
2
  import * as react from 'react';
3
- import { HTMLAttributes, ButtonHTMLAttributes, ReactNode } from 'react';
3
+ import { HTMLAttributes, ButtonHTMLAttributes, ReactNode, InputHTMLAttributes, LabelHTMLAttributes } from 'react';
4
4
 
5
5
  /**
6
6
  * Merge class names, resolving conflicting Tailwind utilities (last wins).
@@ -9,27 +9,73 @@ import { HTMLAttributes, ButtonHTMLAttributes, ReactNode } from 'react';
9
9
  */
10
10
  declare function cn(...inputs: ClassValue[]): string;
11
11
 
12
+ type BadgeVariant = 'neutral' | 'success' | 'warning' | 'danger' | 'info';
12
13
  interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
13
14
  /** Render a leading status dot. */
14
15
  dot?: boolean;
16
+ /** Semantic color. `neutral` is the default outlined pill. */
17
+ variant?: BadgeVariant;
15
18
  }
16
- /** Pill badge, optionally with a leading status dot. */
17
- declare function Badge({ dot, className, children, ...props }: BadgeProps): react.JSX.Element;
19
+ /** Pill badge — neutral by default, or a semantic variant, optionally with a dot. */
20
+ declare function Badge({ dot, variant, className, children, ...props }: BadgeProps): react.JSX.Element;
18
21
 
19
22
  type ButtonVariant = 'primary' | 'secondary' | 'outline';
23
+ type ButtonSize = 'md' | 'sm';
20
24
  interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
21
25
  variant?: ButtonVariant;
26
+ size?: ButtonSize;
22
27
  }
23
- declare function Button({ variant, className, type, ...props }: ButtonProps): react.JSX.Element;
28
+ declare function Button({ variant, size, className, type, ...props }: ButtonProps): react.JSX.Element;
24
29
 
25
- type CardProps = HTMLAttributes<HTMLDivElement>;
26
- /** Bordered surface card with hover elevation. */
27
- declare function Card({ className, ...props }: CardProps): react.JSX.Element;
30
+ type CardPadding = 'none' | 'sm' | 'md' | 'lg';
31
+ interface CardProps extends HTMLAttributes<HTMLDivElement> {
32
+ /** Inner padding. Defaults to `md` (28px); use `none` to supply your own. */
33
+ padding?: CardPadding;
34
+ /** Add pointer cursor + hover elevation (for clickable cards). */
35
+ interactive?: boolean;
36
+ }
37
+ /** Bordered surface card. Static by default; opt into hover with `interactive`. */
38
+ declare function Card({ padding, interactive, className, ...props }: CardProps): react.JSX.Element;
28
39
 
29
40
  type ContainerProps = HTMLAttributes<HTMLDivElement>;
30
41
  /** Centered, max-width content wrapper (var(--max-w)) with horizontal padding. */
31
42
  declare function Container({ className, ...props }: ContainerProps): react.JSX.Element;
32
43
 
44
+ interface SpinnerProps extends HTMLAttributes<HTMLSpanElement> {
45
+ /** Wrap in a centered, padded block (for loading states in a panel). */
46
+ center?: boolean;
47
+ }
48
+ /** Brand-colored loading spinner. */
49
+ declare function Spinner({ center, className, ...props }: SpinnerProps): react.JSX.Element;
50
+ interface EmptyStateProps {
51
+ title: ReactNode;
52
+ description?: ReactNode;
53
+ /** Optional icon rendered in the circular badge above the title. */
54
+ icon?: ReactNode;
55
+ className?: string;
56
+ }
57
+ /** Centered "nothing here yet" placeholder for empty lists/tables. */
58
+ declare function EmptyState({ title, description, icon, className }: EmptyStateProps): react.JSX.Element;
59
+
60
+ type InputProps = InputHTMLAttributes<HTMLInputElement>;
61
+ /** Token-aligned text input (the `.field` look: border, surface, brand focus ring). */
62
+ declare function Input({ className, ...props }: InputProps): react.JSX.Element;
63
+ type LabelProps = LabelHTMLAttributes<HTMLLabelElement>;
64
+ declare function Label({ className, ...props }: LabelProps): react.JSX.Element;
65
+ interface FieldProps {
66
+ label?: ReactNode;
67
+ /** Helper text shown below the control (hidden when `error` is set). */
68
+ hint?: ReactNode;
69
+ /** Error text shown below the control (takes precedence over `hint`). */
70
+ error?: ReactNode;
71
+ /** `htmlFor` for the label, matching the control's id. */
72
+ htmlFor?: string;
73
+ className?: string;
74
+ children: ReactNode;
75
+ }
76
+ /** Labelled field wrapper: optional label above and hint/error below the control. */
77
+ declare function Field({ label, hint, error, htmlFor, className, children }: FieldProps): react.JSX.Element;
78
+
33
79
  type SectionProps = HTMLAttributes<HTMLElement>;
34
80
  /** Vertical page section with max-width and standard padding. */
35
81
  declare function Section({ className, ...props }: SectionProps): react.JSX.Element;
@@ -43,4 +89,4 @@ interface SectionHeaderProps {
43
89
  /** Centered section heading: optional label, title, optional description. */
44
90
  declare function SectionHeader({ label, title, description, className }: SectionHeaderProps): react.JSX.Element;
45
91
 
46
- export { Badge, type BadgeProps, Button, type ButtonProps, type ButtonVariant, Card, type CardProps, Container, type ContainerProps, Section, SectionHeader, type SectionHeaderProps, type SectionProps, cn };
92
+ export { Badge, type BadgeProps, type BadgeVariant, Button, type ButtonProps, type ButtonSize, type ButtonVariant, Card, type CardPadding, type CardProps, Container, type ContainerProps, EmptyState, type EmptyStateProps, Field, type FieldProps, Input, type InputProps, Label, type LabelProps, Section, SectionHeader, type SectionHeaderProps, type SectionProps, Spinner, type SpinnerProps, cn };
package/dist/index.mjs CHANGED
@@ -4,8 +4,21 @@ import {
4
4
 
5
5
  // src/components/badge.tsx
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
7
- function Badge({ dot = false, className, children, ...props }) {
8
- return /* @__PURE__ */ jsxs("span", { className: cn("badge", className), ...props, children: [
7
+ var variantClass = {
8
+ neutral: "",
9
+ success: "badge-success",
10
+ warning: "badge-warning",
11
+ danger: "badge-danger",
12
+ info: "badge-info"
13
+ };
14
+ function Badge({
15
+ dot = false,
16
+ variant = "neutral",
17
+ className,
18
+ children,
19
+ ...props
20
+ }) {
21
+ return /* @__PURE__ */ jsxs("span", { className: cn("badge", variantClass[variant], className), ...props, children: [
9
22
  dot ? /* @__PURE__ */ jsx("span", { className: "badge-dot" }) : null,
10
23
  children
11
24
  ] });
@@ -13,19 +26,43 @@ function Badge({ dot = false, className, children, ...props }) {
13
26
 
14
27
  // src/components/button.tsx
15
28
  import { jsx as jsx2 } from "react/jsx-runtime";
16
- var variantClass = {
29
+ var variantClass2 = {
17
30
  primary: "btn-primary",
18
31
  secondary: "btn-secondary",
19
32
  outline: "btn-outline"
20
33
  };
21
- function Button({ variant = "primary", className, type = "button", ...props }) {
22
- return /* @__PURE__ */ jsx2("button", { type, className: cn(variantClass[variant], className), ...props });
34
+ function Button({
35
+ variant = "primary",
36
+ size = "md",
37
+ className,
38
+ type = "button",
39
+ ...props
40
+ }) {
41
+ return /* @__PURE__ */ jsx2(
42
+ "button",
43
+ {
44
+ type,
45
+ className: cn(variantClass2[variant], size === "sm" && "btn-sm", className),
46
+ ...props
47
+ }
48
+ );
23
49
  }
24
50
 
25
51
  // src/components/card.tsx
26
52
  import { jsx as jsx3 } from "react/jsx-runtime";
27
- function Card({ className, ...props }) {
28
- return /* @__PURE__ */ jsx3("div", { className: cn("card", className), ...props });
53
+ function Card({ padding = "md", interactive = false, className, ...props }) {
54
+ return /* @__PURE__ */ jsx3(
55
+ "div",
56
+ {
57
+ className: cn(
58
+ "card",
59
+ padding !== "none" && `card-pad-${padding}`,
60
+ interactive && "card-interactive",
61
+ className
62
+ ),
63
+ ...props
64
+ }
65
+ );
29
66
  }
30
67
 
31
68
  // src/components/container.tsx
@@ -34,16 +71,46 @@ function Container({ className, ...props }) {
34
71
  return /* @__PURE__ */ jsx4("div", { className: cn("container", className), ...props });
35
72
  }
36
73
 
37
- // src/components/section.tsx
74
+ // src/components/feedback.tsx
38
75
  import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
76
+ function Spinner({ center = false, className, ...props }) {
77
+ const spinner = /* @__PURE__ */ jsx5("span", { className: cn("spinner", className), role: "status", "aria-label": "Loading", ...props });
78
+ return center ? /* @__PURE__ */ jsx5("div", { className: "spinner-center", children: spinner }) : spinner;
79
+ }
80
+ function EmptyState({ title, description, icon, className }) {
81
+ return /* @__PURE__ */ jsxs2("div", { className: cn("empty-state", className), children: [
82
+ icon ? /* @__PURE__ */ jsx5("div", { className: "empty-state-icon", children: icon }) : null,
83
+ /* @__PURE__ */ jsx5("h3", { className: "empty-state-title", children: title }),
84
+ description ? /* @__PURE__ */ jsx5("p", { className: "empty-state-desc", children: description }) : null
85
+ ] });
86
+ }
87
+
88
+ // src/components/field.tsx
89
+ import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
90
+ function Input({ className, ...props }) {
91
+ return /* @__PURE__ */ jsx6("input", { className: cn("field", className), ...props });
92
+ }
93
+ function Label({ className, ...props }) {
94
+ return /* @__PURE__ */ jsx6("label", { className: cn("field-label", className), ...props });
95
+ }
96
+ function Field({ label, hint, error, htmlFor, className, children }) {
97
+ return /* @__PURE__ */ jsxs3("div", { className: cn("field-group", className), children: [
98
+ label ? /* @__PURE__ */ jsx6(Label, { htmlFor, children: label }) : null,
99
+ children,
100
+ error ? /* @__PURE__ */ jsx6("span", { className: "field-error", children: error }) : hint ? /* @__PURE__ */ jsx6("span", { className: "field-hint", children: hint }) : null
101
+ ] });
102
+ }
103
+
104
+ // src/components/section.tsx
105
+ import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
39
106
  function Section({ className, ...props }) {
40
- return /* @__PURE__ */ jsx5("section", { className: cn("section", className), ...props });
107
+ return /* @__PURE__ */ jsx7("section", { className: cn("section", className), ...props });
41
108
  }
42
109
  function SectionHeader({ label, title, description, className }) {
43
- return /* @__PURE__ */ jsxs2("div", { className: cn("section-center", className), children: [
44
- label ? /* @__PURE__ */ jsx5("div", { className: "section-label", children: label }) : null,
45
- /* @__PURE__ */ jsx5("h2", { className: "section-title", children: title }),
46
- description ? /* @__PURE__ */ jsx5("p", { className: "section-desc", children: description }) : null
110
+ return /* @__PURE__ */ jsxs4("div", { className: cn("section-center", className), children: [
111
+ label ? /* @__PURE__ */ jsx7("div", { className: "section-label", children: label }) : null,
112
+ /* @__PURE__ */ jsx7("h2", { className: "section-title", children: title }),
113
+ description ? /* @__PURE__ */ jsx7("p", { className: "section-desc", children: description }) : null
47
114
  ] });
48
115
  }
49
116
  export {
@@ -51,8 +118,13 @@ export {
51
118
  Button,
52
119
  Card,
53
120
  Container,
121
+ EmptyState,
122
+ Field,
123
+ Input,
124
+ Label,
54
125
  Section,
55
126
  SectionHeader,
127
+ Spinner,
56
128
  cn
57
129
  };
58
130
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/badge.tsx","../src/components/button.tsx","../src/components/card.tsx","../src/components/container.tsx","../src/components/section.tsx"],"sourcesContent":["import type { HTMLAttributes } from 'react';\nimport { cn } from '../cn';\n\nexport interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {\n /** Render a leading status dot. */\n dot?: boolean;\n}\n\n/** Pill badge, optionally with a leading status dot. */\nexport function Badge({ dot = false, className, children, ...props }: BadgeProps) {\n return (\n <span className={cn('badge', className)} {...props}>\n {dot ? <span className=\"badge-dot\" /> : null}\n {children}\n </span>\n );\n}\n","import type { ButtonHTMLAttributes } from 'react';\nimport { cn } from '../cn';\n\nexport type ButtonVariant = 'primary' | 'secondary' | 'outline';\n\nexport interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n variant?: ButtonVariant;\n}\n\nconst variantClass: Record<ButtonVariant, string> = {\n primary: 'btn-primary',\n secondary: 'btn-secondary',\n outline: 'btn-outline',\n};\n\nexport function Button({ variant = 'primary', className, type = 'button', ...props }: ButtonProps) {\n return <button type={type} className={cn(variantClass[variant], className)} {...props} />;\n}\n","import type { HTMLAttributes } from 'react';\nimport { cn } from '../cn';\n\nexport type CardProps = HTMLAttributes<HTMLDivElement>;\n\n/** Bordered surface card with hover elevation. */\nexport function Card({ className, ...props }: CardProps) {\n return <div className={cn('card', className)} {...props} />;\n}\n","import type { HTMLAttributes } from 'react';\nimport { cn } from '../cn';\n\nexport type ContainerProps = HTMLAttributes<HTMLDivElement>;\n\n/** Centered, max-width content wrapper (var(--max-w)) with horizontal padding. */\nexport function Container({ className, ...props }: ContainerProps) {\n return <div className={cn('container', className)} {...props} />;\n}\n","import type { HTMLAttributes, ReactNode } from 'react';\nimport { cn } from '../cn';\n\nexport type SectionProps = HTMLAttributes<HTMLElement>;\n\n/** Vertical page section with max-width and standard padding. */\nexport function Section({ className, ...props }: SectionProps) {\n return <section className={cn('section', className)} {...props} />;\n}\n\nexport interface SectionHeaderProps {\n /** Small eyebrow label above the title (brand colored). */\n label?: ReactNode;\n title: ReactNode;\n description?: ReactNode;\n className?: string;\n}\n\n/** Centered section heading: optional label, title, optional description. */\nexport function SectionHeader({ label, title, description, className }: SectionHeaderProps) {\n return (\n <div className={cn('section-center', className)}>\n {label ? <div className=\"section-label\">{label}</div> : null}\n <h2 className=\"section-title\">{title}</h2>\n {description ? <p className=\"section-desc\">{description}</p> : null}\n </div>\n );\n}\n"],"mappings":";;;;;AAWI,SACS,KADT;AAFG,SAAS,MAAM,EAAE,MAAM,OAAO,WAAW,UAAU,GAAG,MAAM,GAAe;AAChF,SACE,qBAAC,UAAK,WAAW,GAAG,SAAS,SAAS,GAAI,GAAG,OAC1C;AAAA,UAAM,oBAAC,UAAK,WAAU,aAAY,IAAK;AAAA,IACvC;AAAA,KACH;AAEJ;;;ACAS,gBAAAA,YAAA;AAPT,IAAM,eAA8C;AAAA,EAClD,SAAS;AAAA,EACT,WAAW;AAAA,EACX,SAAS;AACX;AAEO,SAAS,OAAO,EAAE,UAAU,WAAW,WAAW,OAAO,UAAU,GAAG,MAAM,GAAgB;AACjG,SAAO,gBAAAA,KAAC,YAAO,MAAY,WAAW,GAAG,aAAa,OAAO,GAAG,SAAS,GAAI,GAAG,OAAO;AACzF;;;ACVS,gBAAAC,YAAA;AADF,SAAS,KAAK,EAAE,WAAW,GAAG,MAAM,GAAc;AACvD,SAAO,gBAAAA,KAAC,SAAI,WAAW,GAAG,QAAQ,SAAS,GAAI,GAAG,OAAO;AAC3D;;;ACDS,gBAAAC,YAAA;AADF,SAAS,UAAU,EAAE,WAAW,GAAG,MAAM,GAAmB;AACjE,SAAO,gBAAAA,KAAC,SAAI,WAAW,GAAG,aAAa,SAAS,GAAI,GAAG,OAAO;AAChE;;;ACDS,gBAAAC,MAcL,QAAAC,aAdK;AADF,SAAS,QAAQ,EAAE,WAAW,GAAG,MAAM,GAAiB;AAC7D,SAAO,gBAAAD,KAAC,aAAQ,WAAW,GAAG,WAAW,SAAS,GAAI,GAAG,OAAO;AAClE;AAWO,SAAS,cAAc,EAAE,OAAO,OAAO,aAAa,UAAU,GAAuB;AAC1F,SACE,gBAAAC,MAAC,SAAI,WAAW,GAAG,kBAAkB,SAAS,GAC3C;AAAA,YAAQ,gBAAAD,KAAC,SAAI,WAAU,iBAAiB,iBAAM,IAAS;AAAA,IACxD,gBAAAA,KAAC,QAAG,WAAU,iBAAiB,iBAAM;AAAA,IACpC,cAAc,gBAAAA,KAAC,OAAE,WAAU,gBAAgB,uBAAY,IAAO;AAAA,KACjE;AAEJ;","names":["jsx","jsx","jsx","jsx","jsxs"]}
1
+ {"version":3,"sources":["../src/components/badge.tsx","../src/components/button.tsx","../src/components/card.tsx","../src/components/container.tsx","../src/components/feedback.tsx","../src/components/field.tsx","../src/components/section.tsx"],"sourcesContent":["import type { HTMLAttributes } from 'react';\nimport { cn } from '../cn';\n\nexport type BadgeVariant = 'neutral' | 'success' | 'warning' | 'danger' | 'info';\n\nexport interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {\n /** Render a leading status dot. */\n dot?: boolean;\n /** Semantic color. `neutral` is the default outlined pill. */\n variant?: BadgeVariant;\n}\n\nconst variantClass: Record<BadgeVariant, string> = {\n neutral: '',\n success: 'badge-success',\n warning: 'badge-warning',\n danger: 'badge-danger',\n info: 'badge-info',\n};\n\n/** Pill badge — neutral by default, or a semantic variant, optionally with a dot. */\nexport function Badge({\n dot = false,\n variant = 'neutral',\n className,\n children,\n ...props\n}: BadgeProps) {\n return (\n <span className={cn('badge', variantClass[variant], className)} {...props}>\n {dot ? <span className=\"badge-dot\" /> : null}\n {children}\n </span>\n );\n}\n","import type { ButtonHTMLAttributes } from 'react';\nimport { cn } from '../cn';\n\nexport type ButtonVariant = 'primary' | 'secondary' | 'outline';\nexport type ButtonSize = 'md' | 'sm';\n\nexport interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n variant?: ButtonVariant;\n size?: ButtonSize;\n}\n\nconst variantClass: Record<ButtonVariant, string> = {\n primary: 'btn-primary',\n secondary: 'btn-secondary',\n outline: 'btn-outline',\n};\n\nexport function Button({\n variant = 'primary',\n size = 'md',\n className,\n type = 'button',\n ...props\n}: ButtonProps) {\n return (\n <button\n type={type}\n className={cn(variantClass[variant], size === 'sm' && 'btn-sm', className)}\n {...props}\n />\n );\n}\n","import type { HTMLAttributes } from 'react';\nimport { cn } from '../cn';\n\nexport type CardPadding = 'none' | 'sm' | 'md' | 'lg';\n\nexport interface CardProps extends HTMLAttributes<HTMLDivElement> {\n /** Inner padding. Defaults to `md` (28px); use `none` to supply your own. */\n padding?: CardPadding;\n /** Add pointer cursor + hover elevation (for clickable cards). */\n interactive?: boolean;\n}\n\n/** Bordered surface card. Static by default; opt into hover with `interactive`. */\nexport function Card({ padding = 'md', interactive = false, className, ...props }: CardProps) {\n return (\n <div\n className={cn(\n 'card',\n padding !== 'none' && `card-pad-${padding}`,\n interactive && 'card-interactive',\n className,\n )}\n {...props}\n />\n );\n}\n","import type { HTMLAttributes } from 'react';\nimport { cn } from '../cn';\n\nexport type ContainerProps = HTMLAttributes<HTMLDivElement>;\n\n/** Centered, max-width content wrapper (var(--max-w)) with horizontal padding. */\nexport function Container({ className, ...props }: ContainerProps) {\n return <div className={cn('container', className)} {...props} />;\n}\n","import type { HTMLAttributes, ReactNode } from 'react';\nimport { cn } from '../cn';\n\nexport interface SpinnerProps extends HTMLAttributes<HTMLSpanElement> {\n /** Wrap in a centered, padded block (for loading states in a panel). */\n center?: boolean;\n}\n\n/** Brand-colored loading spinner. */\nexport function Spinner({ center = false, className, ...props }: SpinnerProps) {\n const spinner = (\n <span className={cn('spinner', className)} role=\"status\" aria-label=\"Loading\" {...props} />\n );\n return center ? <div className=\"spinner-center\">{spinner}</div> : spinner;\n}\n\nexport interface EmptyStateProps {\n title: ReactNode;\n description?: ReactNode;\n /** Optional icon rendered in the circular badge above the title. */\n icon?: ReactNode;\n className?: string;\n}\n\n/** Centered \"nothing here yet\" placeholder for empty lists/tables. */\nexport function EmptyState({ title, description, icon, className }: EmptyStateProps) {\n return (\n <div className={cn('empty-state', className)}>\n {icon ? <div className=\"empty-state-icon\">{icon}</div> : null}\n <h3 className=\"empty-state-title\">{title}</h3>\n {description ? <p className=\"empty-state-desc\">{description}</p> : null}\n </div>\n );\n}\n","import type { InputHTMLAttributes, LabelHTMLAttributes, ReactNode } from 'react';\nimport { cn } from '../cn';\n\nexport type InputProps = InputHTMLAttributes<HTMLInputElement>;\n\n/** Token-aligned text input (the `.field` look: border, surface, brand focus ring). */\nexport function Input({ className, ...props }: InputProps) {\n return <input className={cn('field', className)} {...props} />;\n}\n\nexport type LabelProps = LabelHTMLAttributes<HTMLLabelElement>;\n\nexport function Label({ className, ...props }: LabelProps) {\n return <label className={cn('field-label', className)} {...props} />;\n}\n\nexport interface FieldProps {\n label?: ReactNode;\n /** Helper text shown below the control (hidden when `error` is set). */\n hint?: ReactNode;\n /** Error text shown below the control (takes precedence over `hint`). */\n error?: ReactNode;\n /** `htmlFor` for the label, matching the control's id. */\n htmlFor?: string;\n className?: string;\n children: ReactNode;\n}\n\n/** Labelled field wrapper: optional label above and hint/error below the control. */\nexport function Field({ label, hint, error, htmlFor, className, children }: FieldProps) {\n return (\n <div className={cn('field-group', className)}>\n {label ? <Label htmlFor={htmlFor}>{label}</Label> : null}\n {children}\n {error ? (\n <span className=\"field-error\">{error}</span>\n ) : hint ? (\n <span className=\"field-hint\">{hint}</span>\n ) : null}\n </div>\n );\n}\n","import type { HTMLAttributes, ReactNode } from 'react';\nimport { cn } from '../cn';\n\nexport type SectionProps = HTMLAttributes<HTMLElement>;\n\n/** Vertical page section with max-width and standard padding. */\nexport function Section({ className, ...props }: SectionProps) {\n return <section className={cn('section', className)} {...props} />;\n}\n\nexport interface SectionHeaderProps {\n /** Small eyebrow label above the title (brand colored). */\n label?: ReactNode;\n title: ReactNode;\n description?: ReactNode;\n className?: string;\n}\n\n/** Centered section heading: optional label, title, optional description. */\nexport function SectionHeader({ label, title, description, className }: SectionHeaderProps) {\n return (\n <div className={cn('section-center', className)}>\n {label ? <div className=\"section-label\">{label}</div> : null}\n <h2 className=\"section-title\">{title}</h2>\n {description ? <p className=\"section-desc\">{description}</p> : null}\n </div>\n );\n}\n"],"mappings":"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","names":["jsx","variantClass","jsx","jsx","jsx","jsxs","jsx","jsxs","jsx","jsxs"]}
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@flopay/ui",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
- "packageManager": "pnpm@9.15.0",
5
+ "packageManager": "pnpm@11.17.0",
6
6
  "description": "FloPay shared design system — design tokens, a Tailwind v4 preset, and React primitives shared across landing, demo, docs, and dashboard.",
7
7
  "license": "UNLICENSED",
8
8
  "publishConfig": {
@@ -27,12 +27,18 @@
27
27
  "import": "./dist/marketing/index.mjs",
28
28
  "require": "./dist/marketing/index.cjs"
29
29
  },
30
+ "./annotations": {
31
+ "types": "./dist/annotations/index.d.ts",
32
+ "import": "./dist/annotations/index.mjs",
33
+ "require": "./dist/annotations/index.cjs"
34
+ },
30
35
  "./styles": "./styles/index.css",
31
36
  "./styles/index.css": "./styles/index.css",
32
37
  "./styles/tokens.css": "./styles/tokens.css",
33
38
  "./styles/reset.css": "./styles/reset.css",
34
39
  "./styles/components.css": "./styles/components.css",
35
40
  "./styles/preset.css": "./styles/preset.css",
41
+ "./styles/annotations.css": "./styles/annotations.css",
36
42
  "./logo.png": "./assets/logo.png",
37
43
  "./package.json": "./package.json"
38
44
  },
@@ -68,10 +74,10 @@
68
74
  "tailwind-merge": "^3.6.0"
69
75
  },
70
76
  "devDependencies": {
71
- "@biomejs/biome": "^2.4.15",
77
+ "@biomejs/biome": "^2.5.3",
72
78
  "@types/react": "^19.2.15",
73
79
  "@types/react-dom": "^19.2.3",
74
- "next": "^16.2.6",
80
+ "next": "^16.2.10",
75
81
  "react": "^19.2.6",
76
82
  "react-dom": "^19.2.6",
77
83
  "tsup": "^8.3.0",
@@ -0,0 +1,124 @@
1
+ /* @flopay/ui — hand-drawn annotation layer.
2
+ *
3
+ * Marker-style notes and arrows drawn over a UI, for guided demos and product
4
+ * tours. Opt-in, since the arrow geometry is a few KB that most pages don't
5
+ * need:
6
+ *
7
+ * @import "@flopay/ui/styles/annotations.css";
8
+ *
9
+ * Backs the components exported from `@flopay/ui/annotations`, and depends on
10
+ * tokens.css custom properties. */
11
+
12
+ :root {
13
+ /* Brand blue by default, in a handwriting face, so notes read as marker over
14
+ the UI rather than as part of it. Override either per app. */
15
+ --annotation-ink: var(--brand);
16
+ --annotation-font: 'Bradley Hand', 'Segoe Print', 'Chalkboard SE', 'Comic Sans MS', cursive;
17
+ }
18
+
19
+ .annotation-stage {
20
+ width: 100%;
21
+ }
22
+
23
+ /* The positioning context for `pos`. Notes overhang it into the outer stage. */
24
+ .annotation-stage-inner {
25
+ position: relative;
26
+ }
27
+
28
+ /* ── Narrow: the notes as a plain list under the content ── */
29
+ .annotation-layer {
30
+ display: none;
31
+ }
32
+
33
+ .annotation-list {
34
+ max-width: 420px;
35
+ margin: 18px auto 0;
36
+ padding-top: 14px;
37
+ border-top: 1px dashed var(--border-hover);
38
+ list-style: none;
39
+ display: flex;
40
+ flex-direction: column;
41
+ gap: 7px;
42
+ font-family: var(--annotation-font);
43
+ font-size: 16px;
44
+ line-height: 1.45;
45
+ color: var(--annotation-ink);
46
+ }
47
+
48
+ .annotation-list li::before {
49
+ content: '→ ';
50
+ opacity: 0.7;
51
+ }
52
+
53
+ /* ── Wide: notes float around the content ──
54
+ Notes overhang the stage into the surrounding margins, so what matters is
55
+ the room *around* the content — which belongs to an ancestor the package
56
+ doesn't own. A container query on the stage would measure the content box
57
+ instead and never match. Hence a viewport query.
58
+ 1120px is where a ~720px content box has room for 200px notes either side.
59
+ An app in a constrained layout (a sidebar, a split view) should re-declare
60
+ this one block at its own breakpoint; a custom property can't be used in a
61
+ media condition. */
62
+ @media (min-width: 1120px) {
63
+ .annotation-layer {
64
+ display: block;
65
+ position: absolute;
66
+ inset: 0;
67
+ pointer-events: none;
68
+ /* Above the content so an arrow tip can cross onto its target rather than
69
+ stopping at the edge. */
70
+ z-index: 2;
71
+ }
72
+
73
+ .annotation-list {
74
+ display: none;
75
+ }
76
+
77
+ .annotation {
78
+ position: absolute;
79
+ width: 200px;
80
+ display: flex;
81
+ flex-direction: column;
82
+ gap: 1px;
83
+ transform: rotate(var(--annotation-tilt, 0deg));
84
+ font-family: var(--annotation-font);
85
+ font-size: 18px;
86
+ line-height: 1.3;
87
+ color: var(--annotation-ink);
88
+ }
89
+
90
+ /* Arrow above the text for upward variants, below it otherwise. */
91
+ .annotation[data-flip] {
92
+ flex-direction: column-reverse;
93
+ }
94
+
95
+ /* Text rags toward whatever the arrow points at. */
96
+ .annotation[data-side='right'] { text-align: right; }
97
+ .annotation[data-side='left'] { text-align: left; }
98
+ .annotation[data-side='center'] { text-align: center; }
99
+
100
+ /* `position: relative` on both halves is what lets `posText` / `posArrow`
101
+ nudge one without the other. Offsets shift them off the spot `pos` put
102
+ them, leaving the flex layout — and therefore each other — undisturbed. */
103
+ .annotation-text {
104
+ position: relative;
105
+ }
106
+
107
+ /* `--annotation-arrow-rotate` re-aims the curve and the flips mirror it, so
108
+ one drawing covers several approach directions without a new curve. Those
109
+ are visual only — the layout box stays put, which is why a re-aimed arrow
110
+ overhangs it. Scale sits to the right of rotate so it applies first: you
111
+ pick the mirrored shape, then aim it. */
112
+ .annotation-arrow {
113
+ position: relative;
114
+ width: calc(160px * var(--annotation-arrow-scale, 1));
115
+ height: auto;
116
+ opacity: 0.9;
117
+ transform: rotate(var(--annotation-arrow-rotate, 0deg))
118
+ scale(var(--annotation-arrow-flip-x, 1), var(--annotation-arrow-flip-y, 1));
119
+ }
120
+
121
+ .annotation[data-side='right'] .annotation-arrow { align-self: flex-end; }
122
+ .annotation[data-side='left'] .annotation-arrow { align-self: flex-start; }
123
+ .annotation[data-side='center'] .annotation-arrow { align-self: center; }
124
+ }
@@ -116,6 +116,13 @@
116
116
  border-color: var(--border-hover);
117
117
  }
118
118
 
119
+ /* Small / dense modifier — compose with a variant (e.g. "btn-primary btn-sm"). */
120
+ .btn-sm {
121
+ gap: 5px;
122
+ padding: 8px 14px;
123
+ font-size: 13px;
124
+ }
125
+
119
126
  /* ── Badge ── */
120
127
  .badge {
121
128
  display: inline-flex;
@@ -137,16 +144,58 @@
137
144
  flex-shrink: 0;
138
145
  }
139
146
 
147
+ /* Semantic variants — compose with .badge (e.g. "badge badge-success"). */
148
+ .badge-success {
149
+ border-color: transparent;
150
+ background: color-mix(in srgb, var(--success) 12%, transparent);
151
+ color: var(--success);
152
+ }
153
+
154
+ .badge-warning {
155
+ border-color: transparent;
156
+ background: var(--warning-bg);
157
+ color: var(--warning-fg);
158
+ }
159
+
160
+ .badge-danger {
161
+ border-color: transparent;
162
+ background: color-mix(in srgb, var(--danger) 12%, transparent);
163
+ color: var(--danger);
164
+ }
165
+
166
+ .badge-info {
167
+ border-color: transparent;
168
+ background: color-mix(in srgb, var(--brand) 12%, transparent);
169
+ color: var(--brand);
170
+ }
171
+
140
172
  /* ── Card ── */
173
+ /* Surface only — opt into padding (.card-pad-*) and hover (.card-interactive). */
141
174
  .card {
142
- background: var(--bg);
175
+ background: var(--bg-card);
143
176
  border: 1px solid var(--border);
144
177
  border-radius: var(--radius-lg);
145
- padding: 28px;
146
178
  transition: box-shadow 0.2s, border-color 0.2s;
147
179
  }
148
180
 
149
- .card:hover {
181
+ .card-pad-sm {
182
+ padding: 16px;
183
+ }
184
+
185
+ .card-pad-md {
186
+ padding: 28px;
187
+ }
188
+
189
+ .card-pad-lg {
190
+ padding: 36px;
191
+ }
192
+
193
+ .card-interactive {
194
+ cursor: pointer;
195
+ }
196
+
197
+ .card-interactive:hover {
198
+ border-color: var(--border-hover);
150
199
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
151
200
  }
152
201
 
@@ -334,3 +383,109 @@
334
383
  gap: 12px;
335
384
  }
336
385
  }
386
+
387
+ /* ── Form fields ── */
388
+ .field {
389
+ width: 100%;
390
+ border: 1px solid var(--border);
391
+ background: var(--bg-card);
392
+ border-radius: var(--radius);
393
+ padding: 9px 12px;
394
+ font-family: var(--font);
395
+ font-size: 14px;
396
+ color: var(--text);
397
+ transition: border-color 0.15s, box-shadow 0.15s;
398
+ }
399
+
400
+ .field::placeholder {
401
+ color: var(--text-tertiary);
402
+ }
403
+
404
+ .field:focus {
405
+ outline: none;
406
+ border-color: var(--brand);
407
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 20%, transparent);
408
+ }
409
+
410
+ .field[aria-invalid='true'] {
411
+ border-color: var(--danger);
412
+ }
413
+
414
+ .field[aria-invalid='true']:focus {
415
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 20%, transparent);
416
+ }
417
+
418
+ .field-group {
419
+ display: flex;
420
+ flex-direction: column;
421
+ gap: 6px;
422
+ }
423
+
424
+ .field-label {
425
+ font-size: 13px;
426
+ font-weight: 500;
427
+ color: var(--text);
428
+ }
429
+
430
+ .field-hint {
431
+ font-size: 12px;
432
+ color: var(--text-tertiary);
433
+ }
434
+
435
+ .field-error {
436
+ font-size: 12px;
437
+ color: var(--danger);
438
+ }
439
+
440
+ /* ── Spinner ── */
441
+ .spinner {
442
+ display: inline-block;
443
+ width: 24px;
444
+ height: 24px;
445
+ border: 2px solid var(--border);
446
+ border-top-color: var(--brand);
447
+ border-radius: 50%;
448
+ animation: fp-spin 0.6s linear infinite;
449
+ }
450
+
451
+ @keyframes fp-spin {
452
+ to {
453
+ transform: rotate(360deg);
454
+ }
455
+ }
456
+
457
+ .spinner-center {
458
+ display: flex;
459
+ justify-content: center;
460
+ padding: 64px 0;
461
+ }
462
+
463
+ /* ── Empty state ── */
464
+ .empty-state {
465
+ text-align: center;
466
+ padding: 64px 24px;
467
+ }
468
+
469
+ .empty-state-icon {
470
+ display: inline-flex;
471
+ align-items: center;
472
+ justify-content: center;
473
+ width: 48px;
474
+ height: 48px;
475
+ border-radius: 50%;
476
+ background: var(--bg-muted);
477
+ color: var(--text-tertiary);
478
+ margin-bottom: 16px;
479
+ }
480
+
481
+ .empty-state-title {
482
+ font-size: 14px;
483
+ font-weight: 500;
484
+ color: var(--text);
485
+ }
486
+
487
+ .empty-state-desc {
488
+ font-size: 14px;
489
+ color: var(--text-secondary);
490
+ margin-top: 4px;
491
+ }
package/styles/preset.css CHANGED
@@ -45,6 +45,8 @@
45
45
  --color-success: var(--success);
46
46
  --color-danger: var(--danger);
47
47
  --color-warning: var(--warning);
48
+ --color-warning-bg: var(--warning-bg);
49
+ --color-warning-fg: var(--warning-fg);
48
50
 
49
51
  /* Typography */
50
52
  --font-sans: var(--font);
package/styles/tokens.css CHANGED
@@ -45,6 +45,11 @@
45
45
  --danger: #dc2626;
46
46
  --warning: #f5a623;
47
47
 
48
+ /* Readable warning pair for soft surfaces (badges/callouts) — the solid
49
+ --warning is too light to use as text. */
50
+ --warning-bg: #fffbeb;
51
+ --warning-fg: #b45309;
52
+
48
53
  /* Radius */
49
54
  --radius: 8px;
50
55
  --radius-lg: 12px;
@@ -78,4 +83,7 @@
78
83
 
79
84
  --accent: #fafafa;
80
85
  --accent-hover: #e4e4e7;
86
+
87
+ --warning-bg: #451a03;
88
+ --warning-fg: #fcd34d;
81
89
  }