@syscore/ui-library 1.25.0 → 2.0.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/client/components/ui/app-bar.tsx +299 -0
- package/client/components/ui/banner.tsx +108 -0
- package/client/components/ui/bottom-navigation.tsx +223 -0
- package/client/components/ui/card.tsx +108 -26
- package/client/components/ui/date-picker.tsx +188 -0
- package/client/components/ui/empty-state.tsx +197 -0
- package/client/components/ui/file-upload.tsx +214 -0
- package/client/components/ui/footer.tsx +179 -0
- package/client/components/ui/layout.tsx +381 -0
- package/client/components/ui/sidebar.tsx +11 -5
- package/client/components/ui/stepper.tsx +148 -0
- package/client/components/ui/system-bar.tsx +119 -0
- package/client/components/ui/timeline.tsx +181 -0
- package/client/global.css +2304 -41
- package/client/ui/AppBar/app-bar.stories.tsx +280 -0
- package/client/ui/Banner/banner.stories.tsx +238 -0
- package/client/ui/BottomNavigation/bottom-navigation.stories.tsx +285 -0
- package/client/ui/Card.stories.tsx +285 -168
- package/client/ui/DatePicker/DatePicker.stories.tsx +293 -0
- package/client/ui/EmptyState/empty-state.stories.tsx +251 -0
- package/client/ui/FileUpload/FileUpload.stories.tsx +218 -0
- package/client/ui/Footer/footer.stories.tsx +194 -0
- package/client/ui/Layout.stories.tsx +1481 -0
- package/client/ui/Stepper/Stepper.stories.tsx +344 -0
- package/client/ui/SystemBar/system-bar.stories.tsx +179 -0
- package/client/ui/Timeline/timeline.stories.tsx +404 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +219 -0
- package/dist/index.es.js +1504 -99
- package/package.json +1 -1
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
export interface FileUploadProps {
|
|
5
|
+
accept?: string;
|
|
6
|
+
multiple?: boolean;
|
|
7
|
+
maxSize?: number; // bytes
|
|
8
|
+
maxFiles?: number;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
variant?: "default" | "compact";
|
|
11
|
+
onFilesChange?: (files: File[]) => void;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface FileItem {
|
|
16
|
+
file: File;
|
|
17
|
+
id: string;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function formatBytes(bytes: number): string {
|
|
22
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
23
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
24
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getFileIcon(type: string): string {
|
|
28
|
+
if (type.startsWith("image/")) return "🖼";
|
|
29
|
+
if (type === "application/pdf") return "📄";
|
|
30
|
+
if (type.includes("spreadsheet") || type.includes("excel") || type.includes("csv")) return "📊";
|
|
31
|
+
if (type.includes("word") || type.includes("document")) return "📝";
|
|
32
|
+
if (type.includes("zip") || type.includes("compressed")) return "🗜";
|
|
33
|
+
return "📎";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
|
|
37
|
+
(
|
|
38
|
+
{
|
|
39
|
+
accept,
|
|
40
|
+
multiple = false,
|
|
41
|
+
maxSize,
|
|
42
|
+
maxFiles,
|
|
43
|
+
disabled = false,
|
|
44
|
+
variant = "default",
|
|
45
|
+
onFilesChange,
|
|
46
|
+
className,
|
|
47
|
+
},
|
|
48
|
+
ref,
|
|
49
|
+
) => {
|
|
50
|
+
const [files, setFiles] = React.useState<FileItem[]>([]);
|
|
51
|
+
const [isDragging, setIsDragging] = React.useState(false);
|
|
52
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
53
|
+
|
|
54
|
+
const validateFile = (file: File): string | undefined => {
|
|
55
|
+
if (maxSize && file.size > maxSize) {
|
|
56
|
+
return `File exceeds ${formatBytes(maxSize)} limit`;
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const addFiles = (incoming: FileList | File[]) => {
|
|
62
|
+
const incomingArray = Array.from(incoming);
|
|
63
|
+
const next = multiple ? [...files] : [];
|
|
64
|
+
|
|
65
|
+
for (const file of incomingArray) {
|
|
66
|
+
if (maxFiles && next.filter((f) => !f.error).length >= maxFiles) break;
|
|
67
|
+
const error = validateFile(file);
|
|
68
|
+
next.push({ file, id: `${file.name}-${Date.now()}-${Math.random()}`, error });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
setFiles(next);
|
|
72
|
+
onFilesChange?.(next.filter((f) => !f.error).map((f) => f.file));
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const removeFile = (id: string) => {
|
|
76
|
+
const next = files.filter((f) => f.id !== id);
|
|
77
|
+
setFiles(next);
|
|
78
|
+
onFilesChange?.(next.filter((f) => !f.error).map((f) => f.file));
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const onDragOver = (e: React.DragEvent) => {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
if (!disabled) setIsDragging(true);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const onDragLeave = (e: React.DragEvent) => {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
setIsDragging(false);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const onDrop = (e: React.DragEvent) => {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
setIsDragging(false);
|
|
94
|
+
if (disabled) return;
|
|
95
|
+
if (e.dataTransfer.files.length > 0) addFiles(e.dataTransfer.files);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
99
|
+
if (e.target.files && e.target.files.length > 0) {
|
|
100
|
+
addFiles(e.target.files);
|
|
101
|
+
e.target.value = "";
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const openPicker = () => {
|
|
106
|
+
if (!disabled) inputRef.current?.click();
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div ref={ref} className={cn("file-upload", className)}>
|
|
111
|
+
{/* Drop zone */}
|
|
112
|
+
<div
|
|
113
|
+
role="button"
|
|
114
|
+
tabIndex={disabled ? -1 : 0}
|
|
115
|
+
aria-label="Upload files"
|
|
116
|
+
onClick={openPicker}
|
|
117
|
+
onKeyDown={(e) => e.key === "Enter" && openPicker()}
|
|
118
|
+
onDragOver={onDragOver}
|
|
119
|
+
onDragLeave={onDragLeave}
|
|
120
|
+
onDrop={onDrop}
|
|
121
|
+
className={cn(
|
|
122
|
+
"file-upload-zone",
|
|
123
|
+
variant === "compact" && "file-upload-zone--compact",
|
|
124
|
+
isDragging && "file-upload-zone--dragging",
|
|
125
|
+
disabled && "file-upload-zone--disabled",
|
|
126
|
+
)}
|
|
127
|
+
>
|
|
128
|
+
<input
|
|
129
|
+
ref={inputRef}
|
|
130
|
+
type="file"
|
|
131
|
+
accept={accept}
|
|
132
|
+
multiple={multiple}
|
|
133
|
+
disabled={disabled}
|
|
134
|
+
onChange={onInputChange}
|
|
135
|
+
className="file-upload-input"
|
|
136
|
+
tabIndex={-1}
|
|
137
|
+
/>
|
|
138
|
+
|
|
139
|
+
{variant === "compact" ? (
|
|
140
|
+
<div className="file-upload-compact-inner">
|
|
141
|
+
<span className="file-upload-icon">↑</span>
|
|
142
|
+
<span className="file-upload-zone-text">
|
|
143
|
+
<span className="file-upload-zone-link">Click to upload</span>
|
|
144
|
+
{" "}or drag and drop
|
|
145
|
+
</span>
|
|
146
|
+
{accept && (
|
|
147
|
+
<span className="file-upload-hint">{accept.split(",").join(", ")}</span>
|
|
148
|
+
)}
|
|
149
|
+
{maxSize && (
|
|
150
|
+
<span className="file-upload-hint">Max {formatBytes(maxSize)}</span>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
) : (
|
|
154
|
+
<div className="file-upload-default-inner">
|
|
155
|
+
<div className="file-upload-icon-circle">
|
|
156
|
+
<span className="file-upload-icon">↑</span>
|
|
157
|
+
</div>
|
|
158
|
+
<div className="file-upload-zone-body">
|
|
159
|
+
<span className="file-upload-zone-text">
|
|
160
|
+
<span className="file-upload-zone-link">Click to upload</span>
|
|
161
|
+
{" "}or drag and drop
|
|
162
|
+
</span>
|
|
163
|
+
{(accept || maxSize) && (
|
|
164
|
+
<span className="file-upload-hint">
|
|
165
|
+
{[
|
|
166
|
+
accept && accept.split(",").join(", "),
|
|
167
|
+
maxSize && `Max ${formatBytes(maxSize)}`,
|
|
168
|
+
]
|
|
169
|
+
.filter(Boolean)
|
|
170
|
+
.join(" · ")}
|
|
171
|
+
</span>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
{/* File list */}
|
|
179
|
+
{files.length > 0 && (
|
|
180
|
+
<ul className="file-upload-list">
|
|
181
|
+
{files.map(({ file, id, error }) => (
|
|
182
|
+
<li
|
|
183
|
+
key={id}
|
|
184
|
+
className={cn("file-upload-item", error && "file-upload-item--error")}
|
|
185
|
+
>
|
|
186
|
+
<span className="file-upload-item-icon">{getFileIcon(file.type)}</span>
|
|
187
|
+
<div className="file-upload-item-info">
|
|
188
|
+
<span className="file-upload-item-name">{file.name}</span>
|
|
189
|
+
{error ? (
|
|
190
|
+
<span className="file-upload-item-error">{error}</span>
|
|
191
|
+
) : (
|
|
192
|
+
<span className="file-upload-item-size">{formatBytes(file.size)}</span>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
<button
|
|
196
|
+
type="button"
|
|
197
|
+
aria-label={`Remove ${file.name}`}
|
|
198
|
+
onClick={(e) => { e.stopPropagation(); removeFile(id); }}
|
|
199
|
+
className="file-upload-item-remove"
|
|
200
|
+
>
|
|
201
|
+
✕
|
|
202
|
+
</button>
|
|
203
|
+
</li>
|
|
204
|
+
))}
|
|
205
|
+
</ul>
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
FileUpload.displayName = "FileUpload";
|
|
213
|
+
|
|
214
|
+
export { FileUpload };
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
// ─── Footer Root ─────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export interface FooterProps extends React.HTMLAttributes<HTMLElement> {
|
|
7
|
+
/** Adds a top border */
|
|
8
|
+
bordered?: boolean;
|
|
9
|
+
/** Rounds the footer corners */
|
|
10
|
+
rounded?: boolean;
|
|
11
|
+
/** Elevates with a shadow */
|
|
12
|
+
elevated?: boolean;
|
|
13
|
+
/** Background color — any CSS color or var() token */
|
|
14
|
+
color?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const Footer = React.forwardRef<HTMLElement, FooterProps>(
|
|
18
|
+
(
|
|
19
|
+
{
|
|
20
|
+
className,
|
|
21
|
+
children,
|
|
22
|
+
bordered = false,
|
|
23
|
+
rounded = false,
|
|
24
|
+
elevated = false,
|
|
25
|
+
color,
|
|
26
|
+
style,
|
|
27
|
+
...props
|
|
28
|
+
},
|
|
29
|
+
ref
|
|
30
|
+
) => (
|
|
31
|
+
<footer
|
|
32
|
+
ref={ref}
|
|
33
|
+
style={{ backgroundColor: color, ...style }}
|
|
34
|
+
className={cn(
|
|
35
|
+
"footer",
|
|
36
|
+
bordered && "footer--bordered",
|
|
37
|
+
rounded && "footer--rounded",
|
|
38
|
+
elevated && "footer--elevated",
|
|
39
|
+
className
|
|
40
|
+
)}
|
|
41
|
+
{...props}
|
|
42
|
+
>
|
|
43
|
+
{children}
|
|
44
|
+
</footer>
|
|
45
|
+
)
|
|
46
|
+
);
|
|
47
|
+
Footer.displayName = "Footer";
|
|
48
|
+
|
|
49
|
+
// ─── FooterRow ────────────────────────────────────────────────────────────────
|
|
50
|
+
// A generic horizontal flex row inside the footer.
|
|
51
|
+
|
|
52
|
+
export interface FooterRowProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
53
|
+
|
|
54
|
+
const FooterRow = React.forwardRef<HTMLDivElement, FooterRowProps>(
|
|
55
|
+
({ className, ...props }, ref) => (
|
|
56
|
+
<div ref={ref} className={cn("footer-row", className)} {...props} />
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
FooterRow.displayName = "FooterRow";
|
|
60
|
+
|
|
61
|
+
// ─── FooterLinks ──────────────────────────────────────────────────────────────
|
|
62
|
+
// Wraps a set of navigation links in a centered, wrapping row.
|
|
63
|
+
|
|
64
|
+
export interface FooterLinksProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
65
|
+
|
|
66
|
+
const FooterLinks = React.forwardRef<HTMLDivElement, FooterLinksProps>(
|
|
67
|
+
({ className, ...props }, ref) => (
|
|
68
|
+
<div ref={ref} className={cn("footer-links", className)} {...props} />
|
|
69
|
+
)
|
|
70
|
+
);
|
|
71
|
+
FooterLinks.displayName = "FooterLinks";
|
|
72
|
+
|
|
73
|
+
// ─── FooterLink ───────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
export interface FooterLinkProps
|
|
76
|
+
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {}
|
|
77
|
+
|
|
78
|
+
const FooterLink = React.forwardRef<HTMLAnchorElement, FooterLinkProps>(
|
|
79
|
+
({ className, ...props }, ref) => (
|
|
80
|
+
<a ref={ref} className={cn("footer-link", className)} {...props} />
|
|
81
|
+
)
|
|
82
|
+
);
|
|
83
|
+
FooterLink.displayName = "FooterLink";
|
|
84
|
+
|
|
85
|
+
// ─── FooterIconButton ─────────────────────────────────────────────────────────
|
|
86
|
+
// Small circular icon button for social links.
|
|
87
|
+
|
|
88
|
+
export interface FooterIconButtonProps
|
|
89
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
90
|
+
/** Accessible label — required when button contains only an icon */
|
|
91
|
+
"aria-label": string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const FooterIconButton = React.forwardRef<
|
|
95
|
+
HTMLButtonElement,
|
|
96
|
+
FooterIconButtonProps
|
|
97
|
+
>(({ className, children, ...props }, ref) => (
|
|
98
|
+
<button
|
|
99
|
+
ref={ref}
|
|
100
|
+
type="button"
|
|
101
|
+
className={cn("footer-icon-btn", className)}
|
|
102
|
+
{...props}
|
|
103
|
+
>
|
|
104
|
+
{children}
|
|
105
|
+
</button>
|
|
106
|
+
));
|
|
107
|
+
FooterIconButton.displayName = "FooterIconButton";
|
|
108
|
+
|
|
109
|
+
// ─── FooterDivider ────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
export interface FooterDividerProps extends React.HTMLAttributes<HTMLHRElement> {
|
|
112
|
+
/** Width as a CSS value, e.g. "50px" or "50%" */
|
|
113
|
+
width?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const FooterDivider = React.forwardRef<HTMLHRElement, FooterDividerProps>(
|
|
117
|
+
({ className, width, style, ...props }, ref) => (
|
|
118
|
+
<hr
|
|
119
|
+
ref={ref}
|
|
120
|
+
style={{ width, ...style }}
|
|
121
|
+
className={cn("footer-divider", className)}
|
|
122
|
+
{...props}
|
|
123
|
+
/>
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
FooterDivider.displayName = "FooterDivider";
|
|
127
|
+
|
|
128
|
+
// ─── FooterCopyright ──────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
export interface FooterCopyrightProps
|
|
131
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
132
|
+
/** Company / org name */
|
|
133
|
+
company?: string;
|
|
134
|
+
/** Override the year (defaults to current year) */
|
|
135
|
+
year?: number;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const FooterCopyright = React.forwardRef<HTMLDivElement, FooterCopyrightProps>(
|
|
139
|
+
(
|
|
140
|
+
{
|
|
141
|
+
className,
|
|
142
|
+
company = "International WELL Building Institute",
|
|
143
|
+
year,
|
|
144
|
+
...props
|
|
145
|
+
},
|
|
146
|
+
ref
|
|
147
|
+
) => (
|
|
148
|
+
<div ref={ref} className={cn("footer-copyright", className)} {...props}>
|
|
149
|
+
{year ?? new Date().getFullYear()} —{" "}
|
|
150
|
+
<strong>© {company}</strong>
|
|
151
|
+
</div>
|
|
152
|
+
)
|
|
153
|
+
);
|
|
154
|
+
FooterCopyright.displayName = "FooterCopyright";
|
|
155
|
+
|
|
156
|
+
// ─── FooterBand ───────────────────────────────────────────────────────────────
|
|
157
|
+
// A full-width bottom band (used in the teal footer variant).
|
|
158
|
+
|
|
159
|
+
export interface FooterBandProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
160
|
+
|
|
161
|
+
const FooterBand = React.forwardRef<HTMLDivElement, FooterBandProps>(
|
|
162
|
+
({ className, ...props }, ref) => (
|
|
163
|
+
<div ref={ref} className={cn("footer-band", className)} {...props} />
|
|
164
|
+
)
|
|
165
|
+
);
|
|
166
|
+
FooterBand.displayName = "FooterBand";
|
|
167
|
+
|
|
168
|
+
// ─── Exports ─────────────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
export {
|
|
171
|
+
Footer,
|
|
172
|
+
FooterRow,
|
|
173
|
+
FooterLinks,
|
|
174
|
+
FooterLink,
|
|
175
|
+
FooterIconButton,
|
|
176
|
+
FooterDivider,
|
|
177
|
+
FooterCopyright,
|
|
178
|
+
FooterBand,
|
|
179
|
+
};
|