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.
Files changed (71) hide show
  1. package/README.md +50 -3
  2. package/dist/index.cjs +1551 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.mts +394 -0
  5. package/dist/index.d.ts +394 -0
  6. package/dist/index.js +1483 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/theme-init.cjs +31 -0
  9. package/dist/theme-init.cjs.map +1 -0
  10. package/dist/theme-init.d.mts +8 -0
  11. package/dist/theme-init.d.ts +8 -0
  12. package/dist/theme-init.js +6 -0
  13. package/dist/theme-init.js.map +1 -0
  14. package/package.json +71 -4
  15. package/src/components/accordion/accordion.css +66 -0
  16. package/src/components/accordion/accordion.tsx +86 -0
  17. package/src/components/alert/alert.css +86 -0
  18. package/src/components/alert/alert.tsx +45 -0
  19. package/src/components/avatar/avatar.css +75 -0
  20. package/src/components/avatar/avatar.tsx +59 -0
  21. package/src/components/badge/badge.css +106 -0
  22. package/src/components/badge/badge.tsx +26 -0
  23. package/src/components/button/button.css +118 -0
  24. package/src/components/button/button.tsx +86 -0
  25. package/src/components/card/card.css +41 -0
  26. package/src/components/card/card.tsx +26 -0
  27. package/src/components/checks/checks.css +157 -0
  28. package/src/components/checks/checks.tsx +167 -0
  29. package/src/components/divider/divider.css +41 -0
  30. package/src/components/divider/divider.tsx +24 -0
  31. package/src/components/dropdown/dropdown.css +182 -0
  32. package/src/components/dropdown/dropdown.tsx +268 -0
  33. package/src/components/input/input.css +73 -0
  34. package/src/components/input/input.tsx +85 -0
  35. package/src/components/kbd/kbd.css +66 -0
  36. package/src/components/kbd/kbd.tsx +14 -0
  37. package/src/components/modal/modal.css +93 -0
  38. package/src/components/modal/modal.tsx +114 -0
  39. package/src/components/navbar/navbar.css +127 -0
  40. package/src/components/navbar/navbar.tsx +64 -0
  41. package/src/components/progress/progress.css +62 -0
  42. package/src/components/progress/progress.tsx +50 -0
  43. package/src/components/range/range.css +68 -0
  44. package/src/components/range/range.tsx +60 -0
  45. package/src/components/select/select.css +58 -0
  46. package/src/components/select/select.tsx +72 -0
  47. package/src/components/skeleton/skeleton.css +43 -0
  48. package/src/components/skeleton/skeleton.tsx +35 -0
  49. package/src/components/spinner/spinner.css +21 -0
  50. package/src/components/spinner/spinner.tsx +10 -0
  51. package/src/components/stack/stack.css +60 -0
  52. package/src/components/stack/stack.tsx +34 -0
  53. package/src/components/stat/stat.css +54 -0
  54. package/src/components/stat/stat.tsx +19 -0
  55. package/src/components/tabs/tabs.css +58 -0
  56. package/src/components/tabs/tabs.tsx +84 -0
  57. package/src/components/toast/toast.css +94 -0
  58. package/src/components/toast/toast.tsx +100 -0
  59. package/src/components/tooltip/tooltip.css +100 -0
  60. package/src/components/tooltip/tooltip.tsx +19 -0
  61. package/src/index.ts +115 -0
  62. package/src/styles/base.css +78 -0
  63. package/src/styles/index.css +27 -0
  64. package/src/styles/theme.css +246 -0
  65. package/src/theme-init.ts +6 -0
  66. package/src/theming/ThemeMenu.tsx +158 -0
  67. package/src/theming/ThemeProvider.tsx +234 -0
  68. package/src/theming/ThemeToggle.tsx +27 -0
  69. package/src/theming/presets.ts +33 -0
  70. package/src/theming/theming.css +155 -0
  71. 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
+ }