@bizbasics/ui 0.1.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.
@@ -0,0 +1,68 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+
3
+ interface CardProps {
4
+ children: ReactNode;
5
+ style?: CSSProperties;
6
+ padding?: number | string;
7
+ noBorder?: boolean;
8
+ noShadow?: boolean;
9
+ }
10
+
11
+ interface CardHeaderProps {
12
+ title: string;
13
+ subtitle?: string;
14
+ action?: ReactNode;
15
+ }
16
+
17
+ export function Card({ children, style, padding = "24px", noBorder, noShadow }: CardProps) {
18
+ return (
19
+ <div style={{
20
+ background: "var(--bb-surface)",
21
+ border: noBorder ? "none" : "1px solid var(--bb-border)",
22
+ borderRadius: "var(--bb-radius-lg)",
23
+ boxShadow: noShadow ? "none" : "var(--bb-shadow-sm)",
24
+ padding,
25
+ ...style,
26
+ }}>
27
+ {children}
28
+ </div>
29
+ );
30
+ }
31
+
32
+ export function CardHeader({ title, subtitle, action }: CardHeaderProps) {
33
+ return (
34
+ <div style={{
35
+ display: "flex",
36
+ alignItems: "flex-start",
37
+ justifyContent: "space-between",
38
+ marginBottom: "20px",
39
+ }}>
40
+ <div>
41
+ <h2 style={{
42
+ margin: 0,
43
+ fontSize: "var(--bb-text-lg)",
44
+ fontWeight: 600,
45
+ color: "var(--bb-text-primary)",
46
+ lineHeight: 1.3,
47
+ }}>
48
+ {title}
49
+ </h2>
50
+ {subtitle && (
51
+ <p style={{
52
+ margin: "4px 0 0",
53
+ fontSize: "var(--bb-text-sm)",
54
+ color: "var(--bb-text-muted)",
55
+ }}>
56
+ {subtitle}
57
+ </p>
58
+ )}
59
+ </div>
60
+ {action}
61
+ </div>
62
+ );
63
+ }
64
+
65
+ /** Full-width horizontal divider for use inside cards */
66
+ export function CardDivider() {
67
+ return <div style={{ height: "1px", background: "var(--bb-border-muted)", margin: "16px -24px" }} />;
68
+ }
@@ -0,0 +1,49 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+
3
+ interface Props {
4
+ icon?: ReactNode;
5
+ title: string;
6
+ description?: string;
7
+ action?: ReactNode;
8
+ }
9
+
10
+ const wrapStyle: CSSProperties = {
11
+ display: "flex",
12
+ flexDirection: "column",
13
+ alignItems: "center",
14
+ justifyContent: "center",
15
+ padding: "48px 24px",
16
+ textAlign: "center",
17
+ };
18
+
19
+ export function EmptyState({ icon, title, description, action }: Props) {
20
+ return (
21
+ <div style={wrapStyle}>
22
+ {icon && (
23
+ <div style={{ fontSize: "40px", marginBottom: "16px", opacity: 0.45 }}>
24
+ {icon}
25
+ </div>
26
+ )}
27
+ <h3 style={{
28
+ margin: "0 0 6px",
29
+ fontSize: "var(--bb-text-md)",
30
+ fontWeight: 600,
31
+ color: "var(--bb-text-secondary)",
32
+ }}>
33
+ {title}
34
+ </h3>
35
+ {description && (
36
+ <p style={{
37
+ margin: "0 0 20px",
38
+ fontSize: "var(--bb-text-sm)",
39
+ color: "var(--bb-text-muted)",
40
+ maxWidth: "320px",
41
+ lineHeight: "1.5",
42
+ }}>
43
+ {description}
44
+ </p>
45
+ )}
46
+ {action}
47
+ </div>
48
+ );
49
+ }
@@ -0,0 +1,146 @@
1
+ "use client";
2
+
3
+ import type { CSSProperties, InputHTMLAttributes, TextareaHTMLAttributes, ReactNode } from "react";
4
+
5
+ interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "prefix"> {
6
+ label?: string;
7
+ error?: string;
8
+ hint?: string;
9
+ prefix?: ReactNode;
10
+ suffix?: ReactNode;
11
+ }
12
+
13
+ interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
14
+ label?: string;
15
+ error?: string;
16
+ hint?: string;
17
+ }
18
+
19
+ const labelStyle: CSSProperties = {
20
+ display: "block",
21
+ fontSize: "var(--bb-text-sm)",
22
+ fontWeight: 500,
23
+ color: "var(--bb-text-secondary)",
24
+ marginBottom: "5px",
25
+ };
26
+
27
+ const inputBaseStyle: CSSProperties = {
28
+ width: "100%",
29
+ border: "1px solid var(--bb-border)",
30
+ borderRadius: "var(--bb-radius)",
31
+ padding: "9px 12px",
32
+ fontSize: "var(--bb-text-sm)",
33
+ fontFamily: "var(--bb-font-sans)",
34
+ color: "var(--bb-text-primary)",
35
+ background: "var(--bb-surface)",
36
+ outline: "none",
37
+ transition: "border-color var(--bb-transition), box-shadow var(--bb-transition)",
38
+ };
39
+
40
+ const hintStyle: CSSProperties = {
41
+ fontSize: "var(--bb-text-xs)",
42
+ color: "var(--bb-text-muted)",
43
+ marginTop: "4px",
44
+ };
45
+
46
+ const errorStyle: CSSProperties = {
47
+ fontSize: "var(--bb-text-xs)",
48
+ color: "var(--bb-error)",
49
+ marginTop: "4px",
50
+ };
51
+
52
+ export function Input({ label, error, hint, prefix, suffix, style, ...rest }: InputProps) {
53
+ return (
54
+ <div>
55
+ {label && <label style={labelStyle}>{label}</label>}
56
+ <div style={{ position: "relative", display: "flex", alignItems: "center" }}>
57
+ {prefix && (
58
+ <span style={{ position: "absolute", left: "12px", color: "var(--bb-text-muted)", display: "flex" }}>
59
+ {prefix}
60
+ </span>
61
+ )}
62
+ <input
63
+ style={{
64
+ ...inputBaseStyle,
65
+ paddingLeft: prefix ? "36px" : inputBaseStyle.padding as string,
66
+ paddingRight: suffix ? "36px" : inputBaseStyle.padding as string,
67
+ borderColor: error ? "var(--bb-error)" : "var(--bb-border)",
68
+ ...style,
69
+ }}
70
+ onFocus={(e) => {
71
+ e.currentTarget.style.borderColor = error ? "var(--bb-error)" : "var(--bb-brand)";
72
+ e.currentTarget.style.boxShadow = `0 0 0 3px ${error ? "rgba(220,38,38,0.12)" : "var(--bb-brand-ring)"}`;
73
+ }}
74
+ onBlur={(e) => {
75
+ e.currentTarget.style.borderColor = error ? "var(--bb-error)" : "var(--bb-border)";
76
+ e.currentTarget.style.boxShadow = "none";
77
+ }}
78
+ {...rest}
79
+ />
80
+ {suffix && (
81
+ <span style={{ position: "absolute", right: "12px", color: "var(--bb-text-muted)", display: "flex" }}>
82
+ {suffix}
83
+ </span>
84
+ )}
85
+ </div>
86
+ {error && <p style={errorStyle}>{error}</p>}
87
+ {hint && !error && <p style={hintStyle}>{hint}</p>}
88
+ </div>
89
+ );
90
+ }
91
+
92
+ export function Textarea({ label, error, hint, style, ...rest }: TextareaProps) {
93
+ return (
94
+ <div>
95
+ {label && <label style={labelStyle}>{label}</label>}
96
+ <textarea
97
+ style={{
98
+ ...inputBaseStyle,
99
+ resize: "vertical",
100
+ minHeight: "80px",
101
+ borderColor: error ? "var(--bb-error)" : "var(--bb-border)",
102
+ ...style,
103
+ }}
104
+ onFocus={(e) => {
105
+ e.currentTarget.style.borderColor = error ? "var(--bb-error)" : "var(--bb-brand)";
106
+ e.currentTarget.style.boxShadow = `0 0 0 3px ${error ? "rgba(220,38,38,0.12)" : "var(--bb-brand-ring)"}`;
107
+ }}
108
+ onBlur={(e) => {
109
+ e.currentTarget.style.borderColor = error ? "var(--bb-error)" : "var(--bb-border)";
110
+ e.currentTarget.style.boxShadow = "none";
111
+ }}
112
+ {...rest}
113
+ />
114
+ {error && <p style={errorStyle}>{error}</p>}
115
+ {hint && !error && <p style={hintStyle}>{hint}</p>}
116
+ </div>
117
+ );
118
+ }
119
+
120
+ export function Select({
121
+ label,
122
+ error,
123
+ hint,
124
+ style,
125
+ children,
126
+ ...rest
127
+ }: React.SelectHTMLAttributes<HTMLSelectElement> & { label?: string; error?: string; hint?: string }) {
128
+ return (
129
+ <div>
130
+ {label && <label style={labelStyle}>{label}</label>}
131
+ <select
132
+ style={{
133
+ ...inputBaseStyle,
134
+ cursor: "pointer",
135
+ borderColor: error ? "var(--bb-error)" : "var(--bb-border)",
136
+ ...style,
137
+ }}
138
+ {...rest}
139
+ >
140
+ {children}
141
+ </select>
142
+ {error && <p style={errorStyle}>{error}</p>}
143
+ {hint && !error && <p style={hintStyle}>{hint}</p>}
144
+ </div>
145
+ );
146
+ }
@@ -0,0 +1,119 @@
1
+ "use client";
2
+
3
+ import { useEffect, type CSSProperties, type ReactNode } from "react";
4
+
5
+ interface Props {
6
+ open: boolean;
7
+ onClose: () => void;
8
+ title?: string;
9
+ children: ReactNode;
10
+ footer?: ReactNode;
11
+ width?: number | string;
12
+ }
13
+
14
+ const overlayStyle: CSSProperties = {
15
+ position: "fixed",
16
+ inset: 0,
17
+ background: "rgba(0,0,0,0.55)",
18
+ backdropFilter: "blur(3px)",
19
+ display: "flex",
20
+ alignItems: "center",
21
+ justifyContent: "center",
22
+ zIndex: "var(--bb-z-modal)" as unknown as number,
23
+ padding: "16px",
24
+ };
25
+
26
+ const dialogStyle: CSSProperties = {
27
+ background: "var(--bb-surface)",
28
+ border: "1px solid var(--bb-border)",
29
+ borderRadius: "var(--bb-radius-lg)",
30
+ boxShadow: "var(--bb-shadow-xl)",
31
+ display: "flex",
32
+ flexDirection: "column",
33
+ maxHeight: "calc(100vh - 64px)",
34
+ overflow: "hidden",
35
+ width: "100%",
36
+ animation: "bb-modal-in 0.15s ease",
37
+ };
38
+
39
+ export function Modal({ open, onClose, title, children, footer, width = 480 }: Props) {
40
+ useEffect(() => {
41
+ if (!open) return;
42
+ const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
43
+ document.addEventListener("keydown", handler);
44
+ return () => document.removeEventListener("keydown", handler);
45
+ }, [open, onClose]);
46
+
47
+ if (!open) return null;
48
+
49
+ return (
50
+ <>
51
+ <style>{`
52
+ @keyframes bb-modal-in {
53
+ from { opacity: 0; transform: scale(0.96) translateY(-6px); }
54
+ to { opacity: 1; transform: scale(1) translateY(0); }
55
+ }
56
+ `}</style>
57
+ <div style={overlayStyle} onClick={(e) => { if (e.target === e.currentTarget) onClose(); }} role="dialog" aria-modal>
58
+ <div style={{ ...dialogStyle, maxWidth: typeof width === "number" ? `${width}px` : width }}>
59
+ {/* Header */}
60
+ <div style={{
61
+ display: "flex",
62
+ alignItems: "center",
63
+ justifyContent: "space-between",
64
+ padding: "16px 20px",
65
+ borderBottom: "1px solid var(--bb-border-muted)",
66
+ flexShrink: 0,
67
+ }}>
68
+ <h3 style={{
69
+ margin: 0,
70
+ fontSize: "var(--bb-text-md)",
71
+ fontWeight: 600,
72
+ color: "var(--bb-text-primary)",
73
+ }}>
74
+ {title}
75
+ </h3>
76
+ <button
77
+ onClick={onClose}
78
+ style={{
79
+ display: "flex",
80
+ alignItems: "center",
81
+ justifyContent: "center",
82
+ width: "28px",
83
+ height: "28px",
84
+ borderRadius: "var(--bb-radius)",
85
+ border: "none",
86
+ background: "transparent",
87
+ color: "var(--bb-text-muted)",
88
+ cursor: "pointer",
89
+ fontSize: "18px",
90
+ lineHeight: 1,
91
+ }}
92
+ >
93
+ ×
94
+ </button>
95
+ </div>
96
+
97
+ {/* Body */}
98
+ <div style={{ padding: "20px", overflowY: "auto", flex: 1 }}>
99
+ {children}
100
+ </div>
101
+
102
+ {/* Footer */}
103
+ {footer && (
104
+ <div style={{
105
+ padding: "14px 20px",
106
+ borderTop: "1px solid var(--bb-border-muted)",
107
+ display: "flex",
108
+ justifyContent: "flex-end",
109
+ gap: "8px",
110
+ flexShrink: 0,
111
+ }}>
112
+ {footer}
113
+ </div>
114
+ )}
115
+ </div>
116
+ </div>
117
+ </>
118
+ );
119
+ }
@@ -0,0 +1,89 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import type { CSSProperties, ReactNode } from "react";
5
+
6
+ export interface NavItemDef {
7
+ href: string;
8
+ label: string;
9
+ icon?: ReactNode;
10
+ badge?: number | string;
11
+ exact?: boolean;
12
+ }
13
+
14
+ interface Props extends NavItemDef {
15
+ active: boolean;
16
+ onClick?: () => void;
17
+ }
18
+
19
+ export function NavItem({ href, label, icon, badge, active, onClick }: Props) {
20
+ const style: CSSProperties = {
21
+ display: "flex",
22
+ alignItems: "center",
23
+ gap: "9px",
24
+ padding: "6px 10px",
25
+ borderRadius: "var(--bb-radius)",
26
+ marginBottom: "1px",
27
+ fontSize: "var(--bb-text-sm)",
28
+ fontWeight: active ? 600 : 400,
29
+ color: active ? "var(--bb-sidebar-active-text)" : "var(--bb-sidebar-text)",
30
+ background: active ? "var(--bb-sidebar-active-bg)" : "transparent",
31
+ textDecoration: "none",
32
+ cursor: "pointer",
33
+ transition: "background var(--bb-transition), color var(--bb-transition)",
34
+ userSelect: "none",
35
+ };
36
+
37
+ const content = (
38
+ <>
39
+ {icon && (
40
+ <span style={{ display: "flex", alignItems: "center", width: "16px", flexShrink: 0, opacity: active ? 1 : 0.7 }}>
41
+ {icon}
42
+ </span>
43
+ )}
44
+ <span style={{ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
45
+ {label}
46
+ </span>
47
+ {badge !== undefined && badge !== 0 && badge !== "" && (
48
+ <span style={{
49
+ background: "var(--bb-brand)",
50
+ color: "#fff",
51
+ fontSize: "10px",
52
+ fontWeight: 700,
53
+ lineHeight: 1,
54
+ padding: "2px 5px",
55
+ borderRadius: "var(--bb-radius-full)",
56
+ minWidth: "16px",
57
+ textAlign: "center",
58
+ flexShrink: 0,
59
+ }}>
60
+ {typeof badge === "number" && badge > 99 ? "99+" : badge}
61
+ </span>
62
+ )}
63
+ </>
64
+ );
65
+
66
+ if (onClick) {
67
+ return (
68
+ <button
69
+ onClick={onClick}
70
+ style={{ ...style, width: "100%", border: "none", textAlign: "left", background: active ? "var(--bb-sidebar-active-bg)" : "transparent" }}
71
+ onMouseEnter={(e) => { if (!active) (e.currentTarget as HTMLElement).style.background = "var(--bb-sidebar-hover-bg)"; }}
72
+ onMouseLeave={(e) => { if (!active) (e.currentTarget as HTMLElement).style.background = "transparent"; }}
73
+ >
74
+ {content}
75
+ </button>
76
+ );
77
+ }
78
+
79
+ return (
80
+ <Link
81
+ href={href}
82
+ style={style}
83
+ onMouseEnter={(e) => { if (!active) (e.currentTarget as HTMLElement).style.background = "var(--bb-sidebar-hover-bg)"; }}
84
+ onMouseLeave={(e) => { if (!active) (e.currentTarget as HTMLElement).style.background = "transparent"; }}
85
+ >
86
+ {content}
87
+ </Link>
88
+ );
89
+ }
@@ -0,0 +1,46 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+
3
+ interface Props {
4
+ label?: string;
5
+ children: ReactNode;
6
+ action?: ReactNode;
7
+ }
8
+
9
+ export function NavSection({ label, children, action }: Props) {
10
+ return (
11
+ <div style={{ marginBottom: "4px" }}>
12
+ {label && (
13
+ <div style={{
14
+ display: "flex",
15
+ alignItems: "center",
16
+ justifyContent: "space-between",
17
+ padding: "10px 10px 3px",
18
+ }}>
19
+ <span style={{
20
+ fontSize: "var(--bb-text-xs)",
21
+ fontWeight: 600,
22
+ color: "var(--bb-sidebar-section-label)",
23
+ textTransform: "uppercase",
24
+ letterSpacing: "0.06em",
25
+ }}>
26
+ {label}
27
+ </span>
28
+ {action}
29
+ </div>
30
+ )}
31
+ <div style={{ padding: "0 4px" }}>
32
+ {children}
33
+ </div>
34
+ </div>
35
+ );
36
+ }
37
+
38
+ export function NavDivider() {
39
+ return (
40
+ <div style={{
41
+ height: "1px",
42
+ background: "var(--bb-sidebar-border)",
43
+ margin: "8px 10px",
44
+ }} />
45
+ );
46
+ }