@tuturuuu/ui 0.29.2 → 0.30.1

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,144 @@
1
+ import { ArrowUpRight } from '@tuturuuu/icons';
2
+ import { cn } from '@tuturuuu/utils/format';
3
+ import type { ComponentType, ReactNode } from 'react';
4
+ import { getMarketingAccent, type MarketingAccent } from './accents';
5
+
6
+ interface SurfaceCardProps {
7
+ accent: MarketingAccent;
8
+ icon?: ComponentType<{ className?: string }>;
9
+ title: ReactNode;
10
+ description: ReactNode;
11
+ /** Small mono label above the title (a role, a category). */
12
+ eyebrow?: ReactNode;
13
+ /** Renders the card as a link and reveals a corner arrow. */
14
+ href?: string;
15
+ /** Opens the link in a new tab — used for apps on their own domain. */
16
+ external?: boolean;
17
+ /** `stack` puts the icon above the copy; `inline` places it alongside. */
18
+ layout?: 'stack' | 'inline';
19
+ size?: 'md' | 'lg';
20
+ /** Extra content rendered under the description (a chip row, a mini preview). */
21
+ footer?: ReactNode;
22
+ className?: string;
23
+ }
24
+
25
+ /**
26
+ * The marketing card surface: border, lit top edge, corner bloom, hover lift.
27
+ *
28
+ * Every feature grid, use-case tile and app link on a satellite landing page
29
+ * routes through this, so a change to the treatment lands everywhere at once
30
+ * instead of being re-typed per section.
31
+ */
32
+ export function SurfaceCard({
33
+ accent,
34
+ icon: Icon,
35
+ title,
36
+ description,
37
+ eyebrow,
38
+ href,
39
+ external,
40
+ layout = 'stack',
41
+ size = 'md',
42
+ footer,
43
+ className,
44
+ }: SurfaceCardProps) {
45
+ const styles = getMarketingAccent(accent);
46
+ const Root = href ? 'a' : 'div';
47
+ const isLarge = size === 'lg';
48
+
49
+ return (
50
+ <Root
51
+ {...(href ? { href } : {})}
52
+ {...(href && external
53
+ ? { target: '_blank', rel: 'noopener noreferrer' }
54
+ : {})}
55
+ className={cn(
56
+ 'group relative flex h-full overflow-hidden rounded-2xl border border-foreground/[0.08] bg-foreground/[0.015] transition-all duration-500',
57
+ 'hover:-translate-y-1 hover:border-foreground/15 hover:bg-foreground/[0.03]',
58
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
59
+ layout === 'inline' ? 'items-start gap-3 p-4' : 'flex-col',
60
+ layout === 'stack' && (isLarge ? 'p-6 sm:p-7' : 'p-5'),
61
+ className
62
+ )}
63
+ >
64
+ <span
65
+ aria-hidden
66
+ className={cn(
67
+ 'pointer-events-none absolute inset-x-6 top-0 h-px bg-gradient-to-r from-transparent to-transparent transition-opacity duration-500 group-hover:opacity-100',
68
+ layout === 'inline' ? 'opacity-0' : 'opacity-40',
69
+ styles.rule
70
+ )}
71
+ />
72
+ <span
73
+ aria-hidden
74
+ className={cn(
75
+ 'pointer-events-none absolute -top-16 -right-10 h-40 w-40 rounded-full opacity-0 blur-3xl transition-opacity duration-700 group-hover:opacity-100',
76
+ styles.bloom
77
+ )}
78
+ />
79
+
80
+ {Icon ? (
81
+ layout === 'inline' ? (
82
+ <span
83
+ className={cn(
84
+ 'relative flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border border-foreground/10 bg-foreground/[0.03] transition-transform duration-500 group-hover:scale-105',
85
+ styles.text
86
+ )}
87
+ >
88
+ <Icon className="h-3.5 w-3.5" />
89
+ </span>
90
+ ) : (
91
+ <Icon
92
+ className={cn(
93
+ 'relative transition-transform duration-500 group-hover:scale-110',
94
+ isLarge ? 'h-5 w-5' : 'h-4 w-4',
95
+ styles.text
96
+ )}
97
+ />
98
+ )
99
+ ) : null}
100
+
101
+ <span className={cn('relative min-w-0', layout === 'inline' && 'flex-1')}>
102
+ {eyebrow ? (
103
+ <span
104
+ className={cn(
105
+ 'block font-mono-ui text-[0.6rem] uppercase tracking-[0.18em]',
106
+ layout === 'stack' && Icon && 'mt-4',
107
+ styles.text
108
+ )}
109
+ >
110
+ {eyebrow}
111
+ </span>
112
+ ) : null}
113
+
114
+ <span
115
+ className={cn(
116
+ 'flex items-center gap-1.5 font-display font-semibold tracking-[-0.01em]',
117
+ layout === 'stack' &&
118
+ !eyebrow &&
119
+ Icon &&
120
+ (isLarge ? 'mt-5' : 'mt-4'),
121
+ eyebrow && 'mt-1.5',
122
+ isLarge ? 'text-xl sm:text-2xl' : 'text-[0.95rem]'
123
+ )}
124
+ >
125
+ {title}
126
+ {href ? (
127
+ <ArrowUpRight className="h-3 w-3 shrink-0 text-foreground/25 transition-all duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5 group-hover:text-foreground/50" />
128
+ ) : null}
129
+ </span>
130
+
131
+ <span
132
+ className={cn(
133
+ 'mt-2 block text-foreground/50 leading-relaxed',
134
+ isLarge ? 'max-w-md text-sm' : 'text-xs'
135
+ )}
136
+ >
137
+ {description}
138
+ </span>
139
+
140
+ {footer ? <span className="relative mt-4 block">{footer}</span> : null}
141
+ </span>
142
+ </Root>
143
+ );
144
+ }