@syscore/ui-library 1.27.3 → 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.
@@ -0,0 +1,299 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ // ─── AppBar Root ────────────────────────────────────────────────────────────
5
+
6
+ export interface AppBarProps extends React.HTMLAttributes<HTMLElement> {
7
+ /** Collapses the bar to show only the prepend slot */
8
+ collapse?: boolean;
9
+ /** Rounds the corners of the bar */
10
+ rounded?: boolean;
11
+ /** Elevates the bar with a shadow */
12
+ elevated?: boolean;
13
+ /** Makes the bar flat (no shadow, no border) */
14
+ flat?: boolean;
15
+ /** Renders bar as fixed (default) or sticky */
16
+ position?: "fixed" | "sticky" | "relative" | "static";
17
+ /** Color / surface variant */
18
+ color?: "default" | "primary" | "transparent";
19
+ /**
20
+ * Density of the bar — controls height.
21
+ * - "default" → 64px
22
+ * - "comfortable" → 56px
23
+ * - "compact" → 48px
24
+ * - "prominent" → 128px (title sits at the bottom)
25
+ */
26
+ density?: "default" | "comfortable" | "compact" | "prominent";
27
+ /**
28
+ * URL for a background image on the app bar.
29
+ * Pair with `imageOverlay` for a gradient on top.
30
+ */
31
+ image?: string;
32
+ /**
33
+ * CSS gradient string applied as an overlay on top of the `image`.
34
+ * Example: "to top right, rgba(19,84,122,.8), rgba(128,208,199,.8)"
35
+ * Renders as: linear-gradient(<value>)
36
+ */
37
+ imageOverlay?: string;
38
+ }
39
+
40
+ const DENSITY_HEIGHT: Record<
41
+ NonNullable<AppBarProps["density"]>,
42
+ number
43
+ > = {
44
+ default: 64,
45
+ comfortable: 56,
46
+ compact: 48,
47
+ prominent: 128,
48
+ };
49
+
50
+ const AppBar = React.forwardRef<HTMLElement, AppBarProps>(
51
+ (
52
+ {
53
+ className,
54
+ children,
55
+ collapse = false,
56
+ rounded = false,
57
+ elevated = false,
58
+ flat = false,
59
+ position = "fixed",
60
+ color = "default",
61
+ density = "default",
62
+ image,
63
+ imageOverlay,
64
+ style,
65
+ ...props
66
+ },
67
+ ref
68
+ ) => {
69
+ const height = DENSITY_HEIGHT[density];
70
+ const isProminent = density === "prominent";
71
+
72
+ return (
73
+ <header
74
+ ref={ref}
75
+ role="banner"
76
+ style={{
77
+ height: collapse ? undefined : height,
78
+ ...style,
79
+ }}
80
+ className={cn(
81
+ "app-bar",
82
+ // position
83
+ position === "fixed" && "app-bar--fixed",
84
+ position === "sticky" && "app-bar--sticky",
85
+ position === "relative" && "app-bar--relative",
86
+ position === "static" && "app-bar--static",
87
+ // color variants
88
+ color === "primary" && "app-bar--primary",
89
+ color === "transparent" && "app-bar--transparent",
90
+ // density
91
+ density === "comfortable" && "app-bar--comfortable",
92
+ density === "compact" && "app-bar--compact",
93
+ isProminent && "app-bar--prominent",
94
+ // modifiers
95
+ rounded && "app-bar--rounded",
96
+ elevated && "app-bar--elevated",
97
+ flat && "app-bar--flat",
98
+ collapse && "app-bar--collapse",
99
+ // image
100
+ image && "app-bar--image",
101
+ className
102
+ )}
103
+ {...props}
104
+ >
105
+ {/* Background image layer */}
106
+ {image && (
107
+ <span
108
+ className="app-bar-image-layer"
109
+ style={{ backgroundImage: `url(${image})` }}
110
+ aria-hidden="true"
111
+ >
112
+ {imageOverlay && (
113
+ <span
114
+ className="app-bar-image-overlay"
115
+ style={{
116
+ background: `linear-gradient(${imageOverlay})`,
117
+ }}
118
+ />
119
+ )}
120
+ </span>
121
+ )}
122
+
123
+ {/* Prominent layout: split children into top-row (prepend/append)
124
+ and bottom-row (title) so the title sits at the bottom */}
125
+ {isProminent ? (
126
+ <ProminentLayout>{children}</ProminentLayout>
127
+ ) : (
128
+ children
129
+ )}
130
+ </header>
131
+ );
132
+ }
133
+ );
134
+ AppBar.displayName = "AppBar";
135
+
136
+ // ─── AppBarNavIcon ───────────────────────────────────────────────────────────
137
+
138
+ export interface AppBarNavIconProps
139
+ extends React.ButtonHTMLAttributes<HTMLButtonElement> {
140
+ /** Custom icon; defaults to hamburger menu */
141
+ icon?: React.ReactNode;
142
+ }
143
+
144
+ const AppBarNavIcon = React.forwardRef<HTMLButtonElement, AppBarNavIconProps>(
145
+ ({ className, icon, children, ...props }, ref) => (
146
+ <button
147
+ ref={ref}
148
+ type="button"
149
+ aria-label="Open navigation menu"
150
+ className={cn("app-bar-nav-icon", className)}
151
+ {...props}
152
+ >
153
+ {children ?? icon ?? <HamburgerIcon />}
154
+ </button>
155
+ )
156
+ );
157
+ AppBarNavIcon.displayName = "AppBarNavIcon";
158
+
159
+ // ─── AppBarTitle ─────────────────────────────────────────────────────────────
160
+
161
+ export interface AppBarTitleProps extends React.HTMLAttributes<HTMLDivElement> {}
162
+
163
+ const AppBarTitle = React.forwardRef<HTMLDivElement, AppBarTitleProps>(
164
+ ({ className, ...props }, ref) => (
165
+ <div
166
+ ref={ref}
167
+ className={cn("app-bar-title", className)}
168
+ {...props}
169
+ />
170
+ )
171
+ );
172
+ AppBarTitle.displayName = "AppBarTitle";
173
+
174
+ // ─── AppBarPrepend ───────────────────────────────────────────────────────────
175
+
176
+ export interface AppBarPrependProps
177
+ extends React.HTMLAttributes<HTMLDivElement> {}
178
+
179
+ const AppBarPrepend = React.forwardRef<HTMLDivElement, AppBarPrependProps>(
180
+ ({ className, ...props }, ref) => (
181
+ <div
182
+ ref={ref}
183
+ className={cn("app-bar-prepend", className)}
184
+ {...props}
185
+ />
186
+ )
187
+ );
188
+ AppBarPrepend.displayName = "AppBarPrepend";
189
+
190
+ // ─── AppBarAppend ─────────────────────────────────────────────────────────────
191
+
192
+ export interface AppBarAppendProps
193
+ extends React.HTMLAttributes<HTMLDivElement> {}
194
+
195
+ const AppBarAppend = React.forwardRef<HTMLDivElement, AppBarAppendProps>(
196
+ ({ className, ...props }, ref) => (
197
+ <div
198
+ ref={ref}
199
+ className={cn("app-bar-append", className)}
200
+ {...props}
201
+ />
202
+ )
203
+ );
204
+ AppBarAppend.displayName = "AppBarAppend";
205
+
206
+ // ─── AppBarContent (fills remaining horizontal space) ────────────────────────
207
+
208
+ export interface AppBarContentProps
209
+ extends React.HTMLAttributes<HTMLDivElement> {}
210
+
211
+ const AppBarContent = React.forwardRef<HTMLDivElement, AppBarContentProps>(
212
+ ({ className, ...props }, ref) => (
213
+ <div
214
+ ref={ref}
215
+ className={cn("app-bar-content", className)}
216
+ {...props}
217
+ />
218
+ )
219
+ );
220
+ AppBarContent.displayName = "AppBarContent";
221
+
222
+ // ─── ProminentLayout helper ───────────────────────────────────────────────
223
+ // Splits children into two rows: prepend+append at top, title at bottom.
224
+ // Matches Vuetify's prominent bar layout.
225
+
226
+ function ProminentLayout({ children }: { children: React.ReactNode }) {
227
+ const kids = React.Children.toArray(children);
228
+
229
+ const topKids = kids.filter((child) => {
230
+ if (!React.isValidElement(child)) return false;
231
+ const name =
232
+ (child.type as { displayName?: string }).displayName ?? "";
233
+ return name === "AppBarPrepend" || name === "AppBarAppend";
234
+ });
235
+
236
+ const bottomKids = kids.filter((child) => {
237
+ if (!React.isValidElement(child)) return false;
238
+ const name =
239
+ (child.type as { displayName?: string }).displayName ?? "";
240
+ return name === "AppBarTitle" || name === "AppBarContent";
241
+ });
242
+
243
+ // Anything not matched falls into the top row
244
+ const otherKids = kids.filter((child) => {
245
+ if (!React.isValidElement(child)) return true;
246
+ const name =
247
+ (child.type as { displayName?: string }).displayName ?? "";
248
+ return ![
249
+ "AppBarPrepend",
250
+ "AppBarAppend",
251
+ "AppBarTitle",
252
+ "AppBarContent",
253
+ ].includes(name);
254
+ });
255
+
256
+ return (
257
+ <span className="app-bar-prominent-inner">
258
+ <span className="app-bar-prominent-top">
259
+ {topKids}
260
+ {otherKids}
261
+ </span>
262
+ <span className="app-bar-prominent-bottom">{bottomKids}</span>
263
+ </span>
264
+ );
265
+ }
266
+
267
+ // ─── Internal helpers ────────────────────────────────────────────────────────
268
+
269
+ function HamburgerIcon() {
270
+ return (
271
+ <svg
272
+ xmlns="http://www.w3.org/2000/svg"
273
+ width="24"
274
+ height="24"
275
+ viewBox="0 0 24 24"
276
+ fill="none"
277
+ stroke="currentColor"
278
+ strokeWidth="2"
279
+ strokeLinecap="round"
280
+ strokeLinejoin="round"
281
+ aria-hidden="true"
282
+ >
283
+ <line x1="4" y1="6" x2="20" y2="6" />
284
+ <line x1="4" y1="12" x2="20" y2="12" />
285
+ <line x1="4" y1="18" x2="20" y2="18" />
286
+ </svg>
287
+ );
288
+ }
289
+
290
+ // ─── Exports ─────────────────────────────────────────────────────────────────
291
+
292
+ export {
293
+ AppBar,
294
+ AppBarNavIcon,
295
+ AppBarTitle,
296
+ AppBarPrepend,
297
+ AppBarAppend,
298
+ AppBarContent,
299
+ };
@@ -0,0 +1,108 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ // ─── Variants ─────────────────────────────────────────────────────────────────
6
+
7
+ const bannerVariants = cva("banner", {
8
+ variants: {
9
+ variant: {
10
+ default: "banner--default",
11
+ info: "banner--info",
12
+ success: "banner--success",
13
+ warning: "banner--warning",
14
+ destructive: "banner--destructive",
15
+ },
16
+ },
17
+ defaultVariants: {
18
+ variant: "default",
19
+ },
20
+ });
21
+
22
+ // ─── Banner Root ──────────────────────────────────────────────────────────────
23
+
24
+ export interface BannerProps
25
+ extends React.HTMLAttributes<HTMLDivElement>,
26
+ VariantProps<typeof bannerVariants> {
27
+ /** Renders the banner fixed to the top of the viewport */
28
+ sticky?: boolean;
29
+ /** Called when the close button is clicked — also renders the close button */
30
+ onClose?: () => void;
31
+ }
32
+
33
+ const Banner = React.forwardRef<HTMLDivElement, BannerProps>(
34
+ ({ className, variant, sticky = false, onClose, children, ...props }, ref) => (
35
+ <div
36
+ ref={ref}
37
+ role="banner"
38
+ className={cn(bannerVariants({ variant }), sticky && "banner--sticky", className)}
39
+ {...props}
40
+ >
41
+ <div className="banner-content">{children}</div>
42
+ {onClose && (
43
+ <button
44
+ type="button"
45
+ className="banner-close"
46
+ aria-label="Dismiss banner"
47
+ onClick={onClose}
48
+ >
49
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
50
+ <line x1="18" y1="6" x2="6" y2="18" />
51
+ <line x1="6" y1="6" x2="18" y2="18" />
52
+ </svg>
53
+ </button>
54
+ )}
55
+ </div>
56
+ )
57
+ );
58
+ Banner.displayName = "Banner";
59
+
60
+ // ─── BannerIcon ───────────────────────────────────────────────────────────────
61
+
62
+ const BannerIcon = React.forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>(
63
+ ({ className, ...props }, ref) => (
64
+ <span ref={ref} className={cn("banner-icon", className)} aria-hidden="true" {...props} />
65
+ )
66
+ );
67
+ BannerIcon.displayName = "BannerIcon";
68
+
69
+ // ─── BannerTitle ──────────────────────────────────────────────────────────────
70
+
71
+ const BannerTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
72
+ ({ className, ...props }, ref) => (
73
+ <p ref={ref} className={cn("banner-title", className)} {...props} />
74
+ )
75
+ );
76
+ BannerTitle.displayName = "BannerTitle";
77
+
78
+ // ─── BannerDescription ────────────────────────────────────────────────────────
79
+
80
+ const BannerDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
81
+ ({ className, ...props }, ref) => (
82
+ <p ref={ref} className={cn("banner-description", className)} {...props} />
83
+ )
84
+ );
85
+ BannerDescription.displayName = "BannerDescription";
86
+
87
+ // ─── BannerText ───────────────────────────────────────────────────────────────
88
+ // Wraps BannerTitle + BannerDescription so they stack vertically (2-line / 3-line layouts).
89
+
90
+ const BannerText = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
91
+ ({ className, ...props }, ref) => (
92
+ <div ref={ref} className={cn("banner-text", className)} {...props} />
93
+ )
94
+ );
95
+ BannerText.displayName = "BannerText";
96
+
97
+ // ─── BannerAction ─────────────────────────────────────────────────────────────
98
+
99
+ const BannerAction = React.forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>(
100
+ ({ className, ...props }, ref) => (
101
+ <button ref={ref} type="button" className={cn("banner-action", className)} {...props} />
102
+ )
103
+ );
104
+ BannerAction.displayName = "BannerAction";
105
+
106
+ // ─── Exports ──────────────────────────────────────────────────────────────────
107
+
108
+ export { Banner, BannerIcon, BannerTitle, BannerText, BannerDescription, BannerAction, bannerVariants };
@@ -0,0 +1,223 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ // ─── Context ─────────────────────────────────────────────────────────────────
5
+
6
+ interface BottomNavContextValue {
7
+ value: string | number | null;
8
+ onChange: (val: string | number) => void;
9
+ grow: boolean;
10
+ horizontal: boolean;
11
+ shift: boolean;
12
+ activeColor: string | undefined;
13
+ }
14
+
15
+ const BottomNavContext = React.createContext<BottomNavContextValue>({
16
+ value: null,
17
+ onChange: () => {},
18
+ grow: false,
19
+ horizontal: false,
20
+ shift: false,
21
+ activeColor: undefined,
22
+ });
23
+
24
+ // ─── BottomNavigation Root ───────────────────────────────────────────────────
25
+
26
+ export interface BottomNavigationProps
27
+ extends Omit<React.HTMLAttributes<HTMLElement>, "onChange"> {
28
+ /**
29
+ * The currently active item value.
30
+ * Controlled: pass value + onChange together.
31
+ */
32
+ value?: string | number;
33
+ /** Default selected value for uncontrolled usage */
34
+ defaultValue?: string | number;
35
+ /** Fires when the active item changes */
36
+ onChange?: (value: string | number) => void;
37
+ /**
38
+ * Active item highlight color (CSS color or var token).
39
+ * Applies to icon + label of the active BottomNavigationItem.
40
+ */
41
+ color?: string;
42
+ /**
43
+ * Forces each item to fill all available width (max 168px per MD spec).
44
+ */
45
+ grow?: boolean;
46
+ /**
47
+ * Positions icon and label side-by-side (inline) instead of stacked.
48
+ */
49
+ horizontal?: boolean;
50
+ /**
51
+ * Hides labels of inactive items; only the active item shows its label.
52
+ * Labels must be wrapped in a <span> inside BottomNavigationItem.
53
+ */
54
+ shift?: boolean;
55
+ /**
56
+ * Controls visibility of the bar.
57
+ * When false the bar slides off-screen (bottom).
58
+ */
59
+ active?: boolean;
60
+ /** Elevates with a shadow */
61
+ elevated?: boolean;
62
+ }
63
+
64
+ const BottomNavigation = React.forwardRef<
65
+ HTMLElement,
66
+ BottomNavigationProps
67
+ >(
68
+ (
69
+ {
70
+ className,
71
+ children,
72
+ value: valueProp,
73
+ defaultValue,
74
+ onChange,
75
+ color,
76
+ grow = false,
77
+ horizontal = false,
78
+ shift = false,
79
+ active = true,
80
+ elevated = false,
81
+ ...props
82
+ },
83
+ ref
84
+ ) => {
85
+ const [internalValue, setInternalValue] = React.useState<
86
+ string | number | null
87
+ >(defaultValue ?? null);
88
+
89
+ const isControlled = valueProp !== undefined;
90
+ const currentValue = isControlled ? valueProp : internalValue;
91
+
92
+ const handleChange = React.useCallback(
93
+ (val: string | number) => {
94
+ if (!isControlled) setInternalValue(val);
95
+ onChange?.(val);
96
+ },
97
+ [isControlled, onChange]
98
+ );
99
+
100
+ return (
101
+ <BottomNavContext.Provider
102
+ value={{
103
+ value: currentValue ?? null,
104
+ onChange: handleChange,
105
+ grow,
106
+ horizontal,
107
+ shift,
108
+ activeColor: color,
109
+ }}
110
+ >
111
+ <nav
112
+ ref={ref}
113
+ role="navigation"
114
+ aria-label="Bottom navigation"
115
+ className={cn(
116
+ "bottom-nav",
117
+ grow && "bottom-nav--grow",
118
+ horizontal && "bottom-nav--horizontal",
119
+ shift && "bottom-nav--shift",
120
+ !active && "bottom-nav--hidden",
121
+ elevated && "bottom-nav--elevated",
122
+ className
123
+ )}
124
+ {...props}
125
+ >
126
+ {children}
127
+ </nav>
128
+ </BottomNavContext.Provider>
129
+ );
130
+ }
131
+ );
132
+ BottomNavigation.displayName = "BottomNavigation";
133
+
134
+ // ─── BottomNavigationItem ─────────────────────────────────────────────────────
135
+
136
+ export interface BottomNavigationItemProps
137
+ extends React.ButtonHTMLAttributes<HTMLButtonElement> {
138
+ /**
139
+ * Unique value for this item. If omitted, the item's index is used
140
+ * (assigned automatically by BottomNavigationGroup, see below).
141
+ */
142
+ value?: string | number;
143
+ /** Index injected by BottomNavigationGroup — do not set manually */
144
+ _index?: number;
145
+ }
146
+
147
+ const BottomNavigationItem = React.forwardRef<
148
+ HTMLButtonElement,
149
+ BottomNavigationItemProps
150
+ >(({ className, children, value: valueProp, _index, style, ...props }, ref) => {
151
+ const { value, onChange, grow, horizontal, shift, activeColor } =
152
+ React.useContext(BottomNavContext);
153
+
154
+ const itemValue = valueProp ?? _index ?? 0;
155
+ const isActive = value === itemValue;
156
+
157
+ const activeStyle =
158
+ isActive && activeColor ? ({ "--bottom-nav-active-color": activeColor } as React.CSSProperties) : {};
159
+
160
+ return (
161
+ <button
162
+ ref={ref}
163
+ type="button"
164
+ role="tab"
165
+ aria-selected={isActive}
166
+ aria-current={isActive ? "page" : undefined}
167
+ onClick={() => onChange(itemValue)}
168
+ style={{ ...activeStyle, ...style }}
169
+ className={cn(
170
+ "bottom-nav-item",
171
+ isActive && "bottom-nav-item--active",
172
+ grow && "bottom-nav-item--grow",
173
+ horizontal && "bottom-nav-item--horizontal",
174
+ shift && "bottom-nav-item--shift",
175
+ className
176
+ )}
177
+ {...props}
178
+ >
179
+ {children}
180
+ </button>
181
+ );
182
+ });
183
+ BottomNavigationItem.displayName = "BottomNavigationItem";
184
+
185
+ // ─── BottomNavigationGroup ───────────────────────────────────────────────────
186
+ // Thin wrapper that injects _index into each BottomNavigationItem child
187
+ // so unvalued items still participate in selection by index.
188
+
189
+ interface BottomNavigationGroupProps
190
+ extends React.HTMLAttributes<HTMLDivElement> {}
191
+
192
+ const BottomNavigationGroup = React.forwardRef<
193
+ HTMLDivElement,
194
+ BottomNavigationGroupProps
195
+ >(({ children, className, ...props }, ref) => {
196
+ const indexedChildren = React.Children.map(children, (child, index) => {
197
+ if (!React.isValidElement(child)) return child;
198
+ const el = child as React.ReactElement<BottomNavigationItemProps>;
199
+ if (
200
+ (el.type as { displayName?: string }).displayName ===
201
+ "BottomNavigationItem"
202
+ ) {
203
+ return React.cloneElement(el, { _index: index });
204
+ }
205
+ return child;
206
+ });
207
+
208
+ return (
209
+ <div
210
+ ref={ref}
211
+ role="tablist"
212
+ className={cn("bottom-nav-group", className)}
213
+ {...props}
214
+ >
215
+ {indexedChildren}
216
+ </div>
217
+ );
218
+ });
219
+ BottomNavigationGroup.displayName = "BottomNavigationGroup";
220
+
221
+ // ─── Exports ─────────────────────────────────────────────────────────────────
222
+
223
+ export { BottomNavigation, BottomNavigationItem, BottomNavigationGroup };