marclayui 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.
- package/README.md +50 -3
- package/dist/index.cjs +1551 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +394 -0
- package/dist/index.d.ts +394 -0
- package/dist/index.js +1483 -0
- package/dist/index.js.map +1 -0
- package/dist/theme-init.cjs +31 -0
- package/dist/theme-init.cjs.map +1 -0
- package/dist/theme-init.d.mts +8 -0
- package/dist/theme-init.d.ts +8 -0
- package/dist/theme-init.js +6 -0
- package/dist/theme-init.js.map +1 -0
- package/package.json +71 -4
- package/src/components/accordion/accordion.css +66 -0
- package/src/components/accordion/accordion.tsx +86 -0
- package/src/components/alert/alert.css +86 -0
- package/src/components/alert/alert.tsx +45 -0
- package/src/components/avatar/avatar.css +75 -0
- package/src/components/avatar/avatar.tsx +59 -0
- package/src/components/badge/badge.css +106 -0
- package/src/components/badge/badge.tsx +26 -0
- package/src/components/button/button.css +118 -0
- package/src/components/button/button.tsx +86 -0
- package/src/components/card/card.css +41 -0
- package/src/components/card/card.tsx +26 -0
- package/src/components/checks/checks.css +157 -0
- package/src/components/checks/checks.tsx +167 -0
- package/src/components/divider/divider.css +41 -0
- package/src/components/divider/divider.tsx +24 -0
- package/src/components/dropdown/dropdown.css +182 -0
- package/src/components/dropdown/dropdown.tsx +268 -0
- package/src/components/input/input.css +73 -0
- package/src/components/input/input.tsx +85 -0
- package/src/components/kbd/kbd.css +66 -0
- package/src/components/kbd/kbd.tsx +14 -0
- package/src/components/modal/modal.css +93 -0
- package/src/components/modal/modal.tsx +114 -0
- package/src/components/navbar/navbar.css +127 -0
- package/src/components/navbar/navbar.tsx +64 -0
- package/src/components/progress/progress.css +62 -0
- package/src/components/progress/progress.tsx +50 -0
- package/src/components/range/range.css +68 -0
- package/src/components/range/range.tsx +60 -0
- package/src/components/select/select.css +58 -0
- package/src/components/select/select.tsx +72 -0
- package/src/components/skeleton/skeleton.css +43 -0
- package/src/components/skeleton/skeleton.tsx +35 -0
- package/src/components/spinner/spinner.css +21 -0
- package/src/components/spinner/spinner.tsx +10 -0
- package/src/components/stack/stack.css +60 -0
- package/src/components/stack/stack.tsx +34 -0
- package/src/components/stat/stat.css +54 -0
- package/src/components/stat/stat.tsx +19 -0
- package/src/components/tabs/tabs.css +58 -0
- package/src/components/tabs/tabs.tsx +84 -0
- package/src/components/toast/toast.css +94 -0
- package/src/components/toast/toast.tsx +100 -0
- package/src/components/tooltip/tooltip.css +100 -0
- package/src/components/tooltip/tooltip.tsx +19 -0
- package/src/index.ts +115 -0
- package/src/styles/base.css +78 -0
- package/src/styles/index.css +27 -0
- package/src/styles/theme.css +246 -0
- package/src/theme-init.ts +6 -0
- package/src/theming/ThemeMenu.tsx +158 -0
- package/src/theming/ThemeProvider.tsx +234 -0
- package/src/theming/ThemeToggle.tsx +27 -0
- package/src/theming/presets.ts +33 -0
- package/src/theming/theming.css +155 -0
- package/src/utils/color.ts +58 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/* MarclayUI — Alert */
|
|
2
|
+
.mcl-alert {
|
|
3
|
+
display: flex;
|
|
4
|
+
align-items: flex-start;
|
|
5
|
+
gap: 16px;
|
|
6
|
+
width: 100%;
|
|
7
|
+
padding: 22px 24px;
|
|
8
|
+
border-radius: calc(var(--mcl-radius-lg) - 6px);
|
|
9
|
+
background: var(--mcl-bg);
|
|
10
|
+
box-shadow: var(--mcl-out-sm);
|
|
11
|
+
}
|
|
12
|
+
.mcl-alert-icon {
|
|
13
|
+
width: 40px;
|
|
14
|
+
height: 40px;
|
|
15
|
+
flex: none;
|
|
16
|
+
border-radius: 14px;
|
|
17
|
+
display: grid;
|
|
18
|
+
place-items: center;
|
|
19
|
+
background: var(--mcl-bg);
|
|
20
|
+
box-shadow: var(--mcl-in-sm);
|
|
21
|
+
font-weight: 800;
|
|
22
|
+
font-size: 16px;
|
|
23
|
+
font-style: normal;
|
|
24
|
+
}
|
|
25
|
+
.mcl-alert-success .mcl-alert-icon {
|
|
26
|
+
color: var(--mcl-success);
|
|
27
|
+
}
|
|
28
|
+
.mcl-alert-danger .mcl-alert-icon {
|
|
29
|
+
color: var(--mcl-danger);
|
|
30
|
+
}
|
|
31
|
+
.mcl-alert-warning .mcl-alert-icon {
|
|
32
|
+
color: var(--mcl-warning);
|
|
33
|
+
}
|
|
34
|
+
.mcl-alert-info .mcl-alert-icon {
|
|
35
|
+
color: var(--mcl-info);
|
|
36
|
+
}
|
|
37
|
+
[data-theme="dark"] .mcl-alert-success .mcl-alert-icon {
|
|
38
|
+
color: var(--mcl-success-2);
|
|
39
|
+
}
|
|
40
|
+
[data-theme="dark"] .mcl-alert-danger .mcl-alert-icon {
|
|
41
|
+
color: var(--mcl-danger-2);
|
|
42
|
+
}
|
|
43
|
+
[data-theme="dark"] .mcl-alert-warning .mcl-alert-icon {
|
|
44
|
+
color: var(--mcl-warning-2);
|
|
45
|
+
}
|
|
46
|
+
[data-theme="dark"] .mcl-alert-info .mcl-alert-icon {
|
|
47
|
+
color: var(--mcl-info-2);
|
|
48
|
+
}
|
|
49
|
+
.mcl-alert-content {
|
|
50
|
+
flex: 1;
|
|
51
|
+
min-width: 0;
|
|
52
|
+
font-size: 14px;
|
|
53
|
+
font-weight: 500;
|
|
54
|
+
color: var(--mcl-muted);
|
|
55
|
+
}
|
|
56
|
+
.mcl-alert-title {
|
|
57
|
+
margin: 0 0 2px;
|
|
58
|
+
font-size: 15px;
|
|
59
|
+
font-weight: 700;
|
|
60
|
+
color: var(--mcl-ink);
|
|
61
|
+
}
|
|
62
|
+
.mcl-alert-content > :last-child {
|
|
63
|
+
margin-bottom: 0;
|
|
64
|
+
}
|
|
65
|
+
.mcl-alert-close {
|
|
66
|
+
width: 32px;
|
|
67
|
+
height: 32px;
|
|
68
|
+
flex: none;
|
|
69
|
+
border: 0;
|
|
70
|
+
border-radius: 12px;
|
|
71
|
+
background: var(--mcl-bg);
|
|
72
|
+
box-shadow: var(--mcl-out-xs);
|
|
73
|
+
color: var(--mcl-muted);
|
|
74
|
+
font-family: inherit;
|
|
75
|
+
font-size: 13px;
|
|
76
|
+
font-weight: 700;
|
|
77
|
+
cursor: pointer;
|
|
78
|
+
transition: box-shadow 0.2s;
|
|
79
|
+
}
|
|
80
|
+
.mcl-alert-close:active {
|
|
81
|
+
box-shadow: var(--mcl-in-xs);
|
|
82
|
+
}
|
|
83
|
+
.mcl-alert-close:focus-visible {
|
|
84
|
+
outline: none;
|
|
85
|
+
box-shadow: var(--mcl-out-xs), 0 0 0 2.5px var(--mcl-ring);
|
|
86
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export type AlertVariant = "success" | "danger" | "warning" | "info";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_ICONS: Record<AlertVariant, string> = {
|
|
6
|
+
success: "✓",
|
|
7
|
+
danger: "✕",
|
|
8
|
+
warning: "!",
|
|
9
|
+
info: "i",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export interface AlertProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
|
|
13
|
+
variant?: AlertVariant;
|
|
14
|
+
title?: React.ReactNode;
|
|
15
|
+
/** Overrides the variant's default glyph. */
|
|
16
|
+
icon?: React.ReactNode;
|
|
17
|
+
onClose?: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function Alert({
|
|
21
|
+
variant = "info",
|
|
22
|
+
title,
|
|
23
|
+
icon,
|
|
24
|
+
onClose,
|
|
25
|
+
className = "",
|
|
26
|
+
children,
|
|
27
|
+
...rest
|
|
28
|
+
}: AlertProps) {
|
|
29
|
+
return (
|
|
30
|
+
<div className={`mcl-alert mcl-alert-${variant} ${className}`.trim()} role="alert" {...rest}>
|
|
31
|
+
<span className="mcl-alert-icon" aria-hidden="true">
|
|
32
|
+
{icon ?? DEFAULT_ICONS[variant]}
|
|
33
|
+
</span>
|
|
34
|
+
<div className="mcl-alert-content">
|
|
35
|
+
{title && <p className="mcl-alert-title">{title}</p>}
|
|
36
|
+
{children}
|
|
37
|
+
</div>
|
|
38
|
+
{onClose && (
|
|
39
|
+
<button type="button" className="mcl-alert-close" onClick={onClose} aria-label="Dismiss">
|
|
40
|
+
✕
|
|
41
|
+
</button>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/* MarclayUI — Avatar */
|
|
2
|
+
.mcl-avatar {
|
|
3
|
+
position: relative;
|
|
4
|
+
width: 48px;
|
|
5
|
+
height: 48px;
|
|
6
|
+
flex: none;
|
|
7
|
+
border-radius: var(--mcl-avatar-radius, 44%);
|
|
8
|
+
background: var(--mcl-bg);
|
|
9
|
+
box-shadow: var(--mcl-out-sm);
|
|
10
|
+
display: inline-grid;
|
|
11
|
+
place-items: center;
|
|
12
|
+
font-weight: 700;
|
|
13
|
+
font-size: 16px;
|
|
14
|
+
color: var(--mcl-primary);
|
|
15
|
+
letter-spacing: 0.02em;
|
|
16
|
+
overflow: visible;
|
|
17
|
+
}
|
|
18
|
+
.mcl-avatar-img {
|
|
19
|
+
width: 100%;
|
|
20
|
+
height: 100%;
|
|
21
|
+
border-radius: inherit;
|
|
22
|
+
object-fit: cover;
|
|
23
|
+
}
|
|
24
|
+
.mcl-avatar-gradient {
|
|
25
|
+
background: linear-gradient(145deg, var(--mcl-primary-2), var(--mcl-primary));
|
|
26
|
+
color: var(--mcl-primary-ink);
|
|
27
|
+
}
|
|
28
|
+
.mcl-avatar-xs {
|
|
29
|
+
width: 28px;
|
|
30
|
+
height: 28px;
|
|
31
|
+
font-size: 10.5px;
|
|
32
|
+
}
|
|
33
|
+
.mcl-avatar-sm {
|
|
34
|
+
width: 36px;
|
|
35
|
+
height: 36px;
|
|
36
|
+
font-size: 13px;
|
|
37
|
+
}
|
|
38
|
+
.mcl-avatar-lg {
|
|
39
|
+
width: 64px;
|
|
40
|
+
height: 64px;
|
|
41
|
+
font-size: 21px;
|
|
42
|
+
}
|
|
43
|
+
.mcl-avatar-xl {
|
|
44
|
+
width: 88px;
|
|
45
|
+
height: 88px;
|
|
46
|
+
font-size: 29px;
|
|
47
|
+
}
|
|
48
|
+
.mcl-avatar-status {
|
|
49
|
+
position: absolute;
|
|
50
|
+
right: 0;
|
|
51
|
+
bottom: 1px;
|
|
52
|
+
width: 12px;
|
|
53
|
+
height: 12px;
|
|
54
|
+
border-radius: 50%;
|
|
55
|
+
box-shadow: 0 0 0 3px var(--mcl-bg);
|
|
56
|
+
}
|
|
57
|
+
.mcl-avatar-status-online {
|
|
58
|
+
background: var(--mcl-success);
|
|
59
|
+
}
|
|
60
|
+
.mcl-avatar-status-offline {
|
|
61
|
+
background: var(--mcl-muted);
|
|
62
|
+
}
|
|
63
|
+
.mcl-avatar-status-busy {
|
|
64
|
+
background: var(--mcl-danger);
|
|
65
|
+
}
|
|
66
|
+
.mcl-avatar-group {
|
|
67
|
+
display: inline-flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
}
|
|
70
|
+
.mcl-avatar-group > .mcl-avatar {
|
|
71
|
+
box-shadow: var(--mcl-out-xs);
|
|
72
|
+
}
|
|
73
|
+
.mcl-avatar-group > .mcl-avatar + .mcl-avatar {
|
|
74
|
+
margin-left: -12px;
|
|
75
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export type AvatarSize = "xs" | "sm" | "md" | "lg" | "xl";
|
|
4
|
+
export type AvatarStatus = "online" | "offline" | "busy";
|
|
5
|
+
|
|
6
|
+
export interface AvatarProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
7
|
+
src?: string;
|
|
8
|
+
alt?: string;
|
|
9
|
+
/** Used to derive initials when no src is given. */
|
|
10
|
+
name?: string;
|
|
11
|
+
size?: AvatarSize;
|
|
12
|
+
status?: AvatarStatus;
|
|
13
|
+
/** Filled clay gradient look. */
|
|
14
|
+
gradient?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function initialsOf(name: string): string {
|
|
18
|
+
const parts = name.trim().split(/\s+/).slice(0, 2);
|
|
19
|
+
return parts.map((p) => p[0]?.toUpperCase() ?? "").join("");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Avatar({
|
|
23
|
+
src,
|
|
24
|
+
alt,
|
|
25
|
+
name,
|
|
26
|
+
size = "md",
|
|
27
|
+
status,
|
|
28
|
+
gradient = false,
|
|
29
|
+
className = "",
|
|
30
|
+
children,
|
|
31
|
+
...rest
|
|
32
|
+
}: AvatarProps) {
|
|
33
|
+
const label = alt ?? (name ? `${name}'s avatar` : "avatar");
|
|
34
|
+
return (
|
|
35
|
+
<span
|
|
36
|
+
className={`mcl-avatar mcl-avatar-${size}${gradient ? " mcl-avatar-gradient" : ""} ${className}`.trim()}
|
|
37
|
+
role="img"
|
|
38
|
+
aria-label={label}
|
|
39
|
+
{...rest}
|
|
40
|
+
>
|
|
41
|
+
{src ? (
|
|
42
|
+
<img src={src} alt="" className="mcl-avatar-img" loading="lazy" decoding="async" />
|
|
43
|
+
) : (
|
|
44
|
+
(children ?? initialsOf(name ?? "??") ?? "")
|
|
45
|
+
)}
|
|
46
|
+
{status && <span className={`mcl-avatar-status mcl-avatar-status-${status}`} aria-hidden="true" />}
|
|
47
|
+
</span>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type AvatarGroupProps = React.HTMLAttributes<HTMLSpanElement>;
|
|
52
|
+
|
|
53
|
+
export function AvatarGroup({ className = "", children, ...rest }: AvatarGroupProps) {
|
|
54
|
+
return (
|
|
55
|
+
<span className={`mcl-avatar-group ${className}`.trim()} {...rest}>
|
|
56
|
+
{children}
|
|
57
|
+
</span>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/* MarclayUI — Badge / Chip
|
|
2
|
+
Soft badges = tinted TEXT on an inset well:
|
|
3
|
+
• light mode: base tone (primary/success/danger/info) or the deepened
|
|
4
|
+
tone for pale tones (secondary/accent/warning)
|
|
5
|
+
• dark mode: the bright -2 tone for every variant
|
|
6
|
+
Solid badges = gradient fill with the -ink text (text ON the tone). */
|
|
7
|
+
.mcl-badge {
|
|
8
|
+
display: inline-flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
gap: 6px;
|
|
11
|
+
padding: 6px 14px;
|
|
12
|
+
border-radius: 999px;
|
|
13
|
+
font-size: 11.5px;
|
|
14
|
+
font-weight: 700;
|
|
15
|
+
letter-spacing: 0.02em;
|
|
16
|
+
line-height: 1.4;
|
|
17
|
+
background: var(--mcl-bg);
|
|
18
|
+
box-shadow: var(--mcl-in-xs);
|
|
19
|
+
color: var(--mcl-ink);
|
|
20
|
+
}
|
|
21
|
+
.mcl-badge-primary {
|
|
22
|
+
color: var(--mcl-primary);
|
|
23
|
+
}
|
|
24
|
+
.mcl-badge-secondary {
|
|
25
|
+
color: var(--mcl-secondary-deep);
|
|
26
|
+
}
|
|
27
|
+
.mcl-badge-accent {
|
|
28
|
+
color: var(--mcl-accent-deep);
|
|
29
|
+
}
|
|
30
|
+
.mcl-badge-warning {
|
|
31
|
+
color: var(--mcl-warning-deep);
|
|
32
|
+
}
|
|
33
|
+
.mcl-badge-success {
|
|
34
|
+
color: var(--mcl-success);
|
|
35
|
+
}
|
|
36
|
+
.mcl-badge-danger {
|
|
37
|
+
color: var(--mcl-danger);
|
|
38
|
+
}
|
|
39
|
+
.mcl-badge-info {
|
|
40
|
+
color: var(--mcl-info);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* dark mode: everything reads from the brighter -2 tones */
|
|
44
|
+
[data-theme="dark"] .mcl-badge-primary {
|
|
45
|
+
color: var(--mcl-primary-2);
|
|
46
|
+
}
|
|
47
|
+
[data-theme="dark"] .mcl-badge-secondary {
|
|
48
|
+
color: var(--mcl-secondary-2);
|
|
49
|
+
}
|
|
50
|
+
[data-theme="dark"] .mcl-badge-accent {
|
|
51
|
+
color: var(--mcl-accent-2);
|
|
52
|
+
}
|
|
53
|
+
[data-theme="dark"] .mcl-badge-warning {
|
|
54
|
+
color: var(--mcl-warning-2);
|
|
55
|
+
}
|
|
56
|
+
[data-theme="dark"] .mcl-badge-success {
|
|
57
|
+
color: var(--mcl-success-2);
|
|
58
|
+
}
|
|
59
|
+
[data-theme="dark"] .mcl-badge-danger {
|
|
60
|
+
color: var(--mcl-danger-2);
|
|
61
|
+
}
|
|
62
|
+
[data-theme="dark"] .mcl-badge-info {
|
|
63
|
+
color: var(--mcl-info-2);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.mcl-badge-solid,
|
|
67
|
+
.mcl-badge-solid.mcl-badge-neutral {
|
|
68
|
+
background: var(--mcl-bg);
|
|
69
|
+
color: var(--mcl-ink);
|
|
70
|
+
box-shadow: var(--mcl-out-xs), var(--mcl-in-xs);
|
|
71
|
+
}
|
|
72
|
+
.mcl-badge-solid.mcl-badge-primary {
|
|
73
|
+
background: linear-gradient(145deg, var(--mcl-primary-2), var(--mcl-primary));
|
|
74
|
+
color: var(--mcl-primary-ink);
|
|
75
|
+
box-shadow: var(--mcl-out-xs), var(--mcl-clay-p);
|
|
76
|
+
}
|
|
77
|
+
.mcl-badge-solid.mcl-badge-secondary {
|
|
78
|
+
background: linear-gradient(145deg, var(--mcl-secondary-2), var(--mcl-secondary));
|
|
79
|
+
color: var(--mcl-secondary-ink);
|
|
80
|
+
box-shadow: var(--mcl-out-xs), var(--mcl-clay-s);
|
|
81
|
+
}
|
|
82
|
+
.mcl-badge-solid.mcl-badge-accent {
|
|
83
|
+
background: linear-gradient(145deg, var(--mcl-accent-2), var(--mcl-accent));
|
|
84
|
+
color: var(--mcl-accent-ink);
|
|
85
|
+
box-shadow: var(--mcl-out-xs), var(--mcl-clay-a);
|
|
86
|
+
}
|
|
87
|
+
.mcl-badge-solid.mcl-badge-warning {
|
|
88
|
+
background: linear-gradient(145deg, var(--mcl-warning-2), var(--mcl-warning));
|
|
89
|
+
color: var(--mcl-warning-ink);
|
|
90
|
+
box-shadow: var(--mcl-out-xs), var(--mcl-clay-w);
|
|
91
|
+
}
|
|
92
|
+
.mcl-badge-solid.mcl-badge-success {
|
|
93
|
+
background: linear-gradient(145deg, var(--mcl-success-2), var(--mcl-success));
|
|
94
|
+
color: var(--mcl-success-ink);
|
|
95
|
+
box-shadow: var(--mcl-out-xs), var(--mcl-clay-c);
|
|
96
|
+
}
|
|
97
|
+
.mcl-badge-solid.mcl-badge-danger {
|
|
98
|
+
background: linear-gradient(145deg, var(--mcl-danger-2), var(--mcl-danger));
|
|
99
|
+
color: var(--mcl-danger-ink);
|
|
100
|
+
box-shadow: var(--mcl-out-xs), var(--mcl-clay-d);
|
|
101
|
+
}
|
|
102
|
+
.mcl-badge-solid.mcl-badge-info {
|
|
103
|
+
background: linear-gradient(145deg, var(--mcl-info-2), var(--mcl-info));
|
|
104
|
+
color: var(--mcl-info-ink);
|
|
105
|
+
box-shadow: var(--mcl-out-xs), var(--mcl-clay-i);
|
|
106
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export type BadgeVariant =
|
|
4
|
+
| "primary"
|
|
5
|
+
| "secondary"
|
|
6
|
+
| "accent"
|
|
7
|
+
| "success"
|
|
8
|
+
| "danger"
|
|
9
|
+
| "warning"
|
|
10
|
+
| "info"
|
|
11
|
+
| "neutral";
|
|
12
|
+
|
|
13
|
+
export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
14
|
+
variant?: BadgeVariant;
|
|
15
|
+
/** Solid = filled clay gradient; soft (default) = inset well with colored text. */
|
|
16
|
+
solid?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Badge({ variant = "neutral", solid = false, className = "", ...rest }: BadgeProps) {
|
|
20
|
+
return (
|
|
21
|
+
<span
|
|
22
|
+
className={`mcl-badge mcl-badge-${variant}${solid ? " mcl-badge-solid" : ""} ${className}`.trim()}
|
|
23
|
+
{...rest}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/* MarclayUI — Button */
|
|
2
|
+
.mcl-btn {
|
|
3
|
+
display: inline-flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
justify-content: center;
|
|
6
|
+
gap: 10px;
|
|
7
|
+
padding: 15px 34px;
|
|
8
|
+
border: 0;
|
|
9
|
+
border-radius: var(--mcl-radius);
|
|
10
|
+
background: var(--mcl-bg);
|
|
11
|
+
color: var(--mcl-ink);
|
|
12
|
+
box-shadow: var(--mcl-out-sm);
|
|
13
|
+
font-family: inherit;
|
|
14
|
+
font-weight: 700;
|
|
15
|
+
font-size: 15px;
|
|
16
|
+
line-height: 1.2;
|
|
17
|
+
letter-spacing: 0.01em;
|
|
18
|
+
text-decoration: none;
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
user-select: none;
|
|
21
|
+
-webkit-tap-highlight-color: transparent;
|
|
22
|
+
transition: box-shadow 0.2s, transform 0.2s, filter 0.2s;
|
|
23
|
+
}
|
|
24
|
+
.mcl-btn:hover {
|
|
25
|
+
transform: translateY(-2px);
|
|
26
|
+
}
|
|
27
|
+
.mcl-btn:active {
|
|
28
|
+
box-shadow: var(--mcl-in-sm);
|
|
29
|
+
transform: none;
|
|
30
|
+
}
|
|
31
|
+
.mcl-btn:disabled {
|
|
32
|
+
opacity: 0.55;
|
|
33
|
+
cursor: not-allowed;
|
|
34
|
+
transform: none;
|
|
35
|
+
}
|
|
36
|
+
.mcl-btn:focus-visible {
|
|
37
|
+
outline: none;
|
|
38
|
+
box-shadow: var(--mcl-out-sm), 0 0 0 2.5px var(--mcl-ring);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.mcl-btn-sm {
|
|
42
|
+
padding: 10px 22px;
|
|
43
|
+
font-size: 13.5px;
|
|
44
|
+
border-radius: calc(var(--mcl-radius) - 4px);
|
|
45
|
+
}
|
|
46
|
+
.mcl-btn-lg {
|
|
47
|
+
padding: 18px 42px;
|
|
48
|
+
font-size: 16.5px;
|
|
49
|
+
border-radius: calc(var(--mcl-radius) + 4px);
|
|
50
|
+
}
|
|
51
|
+
.mcl-btn-block {
|
|
52
|
+
display: flex;
|
|
53
|
+
width: 100%;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.mcl-btn-primary {
|
|
57
|
+
background: linear-gradient(145deg, var(--mcl-primary-2), var(--mcl-primary));
|
|
58
|
+
color: var(--mcl-primary-ink);
|
|
59
|
+
box-shadow: 8px 8px 16px var(--mcl-sd), -6px -6px 14px var(--mcl-sl), var(--mcl-clay-p);
|
|
60
|
+
}
|
|
61
|
+
.mcl-btn-primary:active {
|
|
62
|
+
box-shadow: var(--mcl-in-sm);
|
|
63
|
+
}
|
|
64
|
+
.mcl-btn-secondary {
|
|
65
|
+
background: linear-gradient(145deg, var(--mcl-secondary-2), var(--mcl-secondary));
|
|
66
|
+
color: var(--mcl-secondary-ink);
|
|
67
|
+
box-shadow: 8px 8px 16px var(--mcl-sd), -6px -6px 14px var(--mcl-sl), var(--mcl-clay-s);
|
|
68
|
+
}
|
|
69
|
+
.mcl-btn-secondary:active {
|
|
70
|
+
box-shadow: var(--mcl-in-sm);
|
|
71
|
+
}
|
|
72
|
+
.mcl-btn-accent {
|
|
73
|
+
background: linear-gradient(145deg, var(--mcl-accent-2), var(--mcl-accent));
|
|
74
|
+
color: var(--mcl-accent-ink);
|
|
75
|
+
box-shadow: 8px 8px 16px var(--mcl-sd), -6px -6px 14px var(--mcl-sl), var(--mcl-clay-a);
|
|
76
|
+
}
|
|
77
|
+
.mcl-btn-accent:active {
|
|
78
|
+
box-shadow: var(--mcl-in-sm);
|
|
79
|
+
}
|
|
80
|
+
.mcl-btn-danger {
|
|
81
|
+
background: linear-gradient(145deg, var(--mcl-danger-2), var(--mcl-danger));
|
|
82
|
+
color: var(--mcl-danger-ink);
|
|
83
|
+
box-shadow: 8px 8px 16px var(--mcl-sd), -6px -6px 14px var(--mcl-sl), var(--mcl-clay-d);
|
|
84
|
+
}
|
|
85
|
+
.mcl-btn-danger:active {
|
|
86
|
+
box-shadow: var(--mcl-in-sm);
|
|
87
|
+
}
|
|
88
|
+
.mcl-btn-ghost {
|
|
89
|
+
background: transparent;
|
|
90
|
+
box-shadow: none;
|
|
91
|
+
color: var(--mcl-primary);
|
|
92
|
+
}
|
|
93
|
+
.mcl-btn-ghost:hover {
|
|
94
|
+
background: var(--mcl-bg);
|
|
95
|
+
box-shadow: var(--mcl-out-xs);
|
|
96
|
+
}
|
|
97
|
+
.mcl-btn-ghost:active {
|
|
98
|
+
box-shadow: var(--mcl-in-xs);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.mcl-btn-loading {
|
|
102
|
+
pointer-events: none;
|
|
103
|
+
}
|
|
104
|
+
.mcl-btn-spinner {
|
|
105
|
+
width: 16px;
|
|
106
|
+
height: 16px;
|
|
107
|
+
flex: none;
|
|
108
|
+
border-radius: 50%;
|
|
109
|
+
border: 2.5px solid currentColor;
|
|
110
|
+
border-top-color: transparent;
|
|
111
|
+
opacity: 0.9;
|
|
112
|
+
animation: mcl-spin 0.7s linear infinite;
|
|
113
|
+
}
|
|
114
|
+
@keyframes mcl-spin {
|
|
115
|
+
to {
|
|
116
|
+
transform: rotate(360deg);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export type ButtonVariant = "primary" | "secondary" | "accent" | "soft" | "ghost" | "danger";
|
|
4
|
+
export type ButtonSize = "sm" | "md" | "lg";
|
|
5
|
+
|
|
6
|
+
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
7
|
+
variant?: ButtonVariant;
|
|
8
|
+
size?: ButtonSize;
|
|
9
|
+
/** Shows a spinner and blocks interaction. */
|
|
10
|
+
loading?: boolean;
|
|
11
|
+
/** Stretch to fill the container width. */
|
|
12
|
+
block?: boolean;
|
|
13
|
+
/** Renders an anchor with button styling — use with Next `<Link>` styling or plain routes. */
|
|
14
|
+
href?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const Button = React.forwardRef<HTMLAnchorElement & HTMLButtonElement, ButtonProps>(
|
|
18
|
+
function Button(
|
|
19
|
+
{
|
|
20
|
+
variant = "soft",
|
|
21
|
+
size = "md",
|
|
22
|
+
loading = false,
|
|
23
|
+
block = false,
|
|
24
|
+
className = "",
|
|
25
|
+
disabled,
|
|
26
|
+
children,
|
|
27
|
+
type = "button",
|
|
28
|
+
href,
|
|
29
|
+
...rest
|
|
30
|
+
},
|
|
31
|
+
ref
|
|
32
|
+
) {
|
|
33
|
+
const cls = [
|
|
34
|
+
"mcl-btn",
|
|
35
|
+
`mcl-btn-${variant}`,
|
|
36
|
+
`mcl-btn-${size}`,
|
|
37
|
+
block ? "mcl-btn-block" : "",
|
|
38
|
+
loading ? "mcl-btn-loading" : "",
|
|
39
|
+
className,
|
|
40
|
+
]
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
.join(" ");
|
|
43
|
+
|
|
44
|
+
if (href) {
|
|
45
|
+
return (
|
|
46
|
+
<a
|
|
47
|
+
ref={ref as React.Ref<HTMLAnchorElement>}
|
|
48
|
+
href={href}
|
|
49
|
+
className={cls}
|
|
50
|
+
aria-disabled={disabled || undefined}
|
|
51
|
+
{...(rest as unknown as React.AnchorHTMLAttributes<HTMLAnchorElement>)}
|
|
52
|
+
>
|
|
53
|
+
{loading && <span className="mcl-btn-spinner" aria-hidden="true" />}
|
|
54
|
+
<span className="mcl-btn-label">{children}</span>
|
|
55
|
+
</a>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<button
|
|
61
|
+
ref={ref as React.Ref<HTMLButtonElement>}
|
|
62
|
+
type={type}
|
|
63
|
+
className={cls}
|
|
64
|
+
disabled={disabled || loading}
|
|
65
|
+
{...rest}
|
|
66
|
+
>
|
|
67
|
+
{loading && <span className="mcl-btn-spinner" aria-hidden="true" />}
|
|
68
|
+
<span className="mcl-btn-label">{children}</span>
|
|
69
|
+
</button>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
export interface ButtonGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
75
|
+
children: React.ReactNode;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Segmented control: renders children Buttons as flat segments inside an
|
|
79
|
+
* inset clay well — the active variant keeps its gradient. */
|
|
80
|
+
export function ButtonGroup({ children, className = "", ...rest }: ButtonGroupProps) {
|
|
81
|
+
return (
|
|
82
|
+
<div className={`mcl-btn-group ${className}`.trim()} role="group" {...rest}>
|
|
83
|
+
{children}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* MarclayUI — Card */
|
|
2
|
+
.mcl-card {
|
|
3
|
+
background: var(--mcl-bg);
|
|
4
|
+
border-radius: var(--mcl-radius-lg);
|
|
5
|
+
box-shadow: var(--mcl-out);
|
|
6
|
+
padding: 30px;
|
|
7
|
+
transition: transform 0.25s;
|
|
8
|
+
}
|
|
9
|
+
.mcl-card-hover:hover {
|
|
10
|
+
transform: translateY(-5px);
|
|
11
|
+
}
|
|
12
|
+
.mcl-card-header {
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: space-between;
|
|
16
|
+
gap: 12px;
|
|
17
|
+
padding-bottom: 16px;
|
|
18
|
+
margin-bottom: 16px;
|
|
19
|
+
box-shadow: 0 1px 0 var(--mcl-hair);
|
|
20
|
+
}
|
|
21
|
+
.mcl-card-title {
|
|
22
|
+
margin: 0;
|
|
23
|
+
font-size: 18px;
|
|
24
|
+
font-weight: 700;
|
|
25
|
+
letter-spacing: -0.01em;
|
|
26
|
+
color: var(--mcl-ink);
|
|
27
|
+
}
|
|
28
|
+
.mcl-card-body {
|
|
29
|
+
color: var(--mcl-muted);
|
|
30
|
+
font-weight: 500;
|
|
31
|
+
font-size: 14.5px;
|
|
32
|
+
}
|
|
33
|
+
.mcl-card-footer {
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
gap: 12px;
|
|
37
|
+
flex-wrap: wrap;
|
|
38
|
+
margin-top: 20px;
|
|
39
|
+
padding-top: 16px;
|
|
40
|
+
box-shadow: 0 -1px 0 var(--mcl-hair);
|
|
41
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
/** Lifts on hover — nice for clickable cards. */
|
|
5
|
+
hover?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Card({ hover = false, className = "", ...rest }: CardProps) {
|
|
9
|
+
return <div className={`mcl-card${hover ? " mcl-card-hover" : ""} ${className}`.trim()} {...rest} />;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function CardHeader({ className = "", ...rest }: React.HTMLAttributes<HTMLDivElement>) {
|
|
13
|
+
return <div className={`mcl-card-header ${className}`.trim()} {...rest} />;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function CardTitle({ className = "", ...rest }: React.HTMLAttributes<HTMLHeadingElement>) {
|
|
17
|
+
return <h3 className={`mcl-card-title ${className}`.trim()} {...rest} />;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function CardBody({ className = "", ...rest }: React.HTMLAttributes<HTMLDivElement>) {
|
|
21
|
+
return <div className={`mcl-card-body ${className}`.trim()} {...rest} />;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function CardFooter({ className = "", ...rest }: React.HTMLAttributes<HTMLDivElement>) {
|
|
25
|
+
return <div className={`mcl-card-footer ${className}`.trim()} {...rest} />;
|
|
26
|
+
}
|