@meycult/core 0.1.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,396 @@
1
+ @import "tailwindcss";
2
+
3
+ /* Self-hosted brand font — identical in Next.js and Vite, no next/font needed. */
4
+ @font-face {
5
+ font-family: 'New Rocker';
6
+ font-style: normal;
7
+ font-weight: 400;
8
+ font-display: swap;
9
+ src: url('../fonts/NewRocker-Regular.woff2') format('woff2');
10
+ }
11
+
12
+ /* Old-Egypt glyphs for the ascension background (subset: hieroglyph block). */
13
+ @font-face {
14
+ font-family: 'Noto Sans Egyptian Hieroglyphs';
15
+ font-style: normal;
16
+ font-weight: 400;
17
+ font-display: swap;
18
+ src: url('../fonts/NotoSansEgyptianHieroglyphs-Regular.woff2') format('woff2');
19
+ }
20
+
21
+ @theme {
22
+ --color-bg: #141003;
23
+ --color-surface: #171402;
24
+ --color-panel: rgba(26, 22, 3, 0.55);
25
+ --color-accent: #FFE600;
26
+ --color-accent-2: #FFF7A1;
27
+ --color-text: #FFF9DB;
28
+ --color-text-muted: rgba(255, 249, 219, 0.55);
29
+ --color-line: rgba(255, 230, 0, 0.18);
30
+ --color-yes: #FFE600;
31
+ --color-no: #e04a4a;
32
+ --color-yes-bg: rgba(255, 230, 0, 0.21);
33
+ --color-no-bg: rgba(224, 74, 74, 0.12);
34
+ --color-success: #FFE600;
35
+ --color-danger: #e04a4a;
36
+ --color-warning: #FF9D00;
37
+ --color-info: #FFE600;
38
+ --color-premium-400: #FFB000;
39
+ --color-premium-500: #9C7500;
40
+ --color-premium-600: #6B4E00;
41
+ --font-heading: "Rajdhani", sans-serif;
42
+ --font-body: "Space Grotesk", sans-serif;
43
+ --font-mono: "Space Mono", monospace;
44
+ --font-logo: "Chakra Petch", sans-serif;
45
+ --font-cult: "New Rocker", "Chakra Petch", sans-serif;
46
+ --radius: 4px;
47
+ --glow: 0 0 20px rgba(255, 230, 0, 0.18);
48
+ }
49
+
50
+ @layer base {
51
+ * {
52
+ border-color: var(--color-line);
53
+ }
54
+ body {
55
+ background: var(--color-bg);
56
+ color: var(--color-text);
57
+ font-family: var(--font-body);
58
+ -webkit-font-smoothing: antialiased;
59
+ -moz-osx-font-smoothing: grayscale;
60
+ }
61
+ }
62
+
63
+ @utility scrollbar-thin {
64
+ &::-webkit-scrollbar { width: 6px; height: 6px; }
65
+ &::-webkit-scrollbar-track { background: var(--color-surface); }
66
+ &::-webkit-scrollbar-thumb { background: var(--color-accent); border-radius: 3px; }
67
+ }
68
+
69
+ @utility glass {
70
+ background: rgba(255, 255, 255, 0.03);
71
+ backdrop-filter: blur(12px);
72
+ -webkit-backdrop-filter: blur(12px);
73
+ border: 1px solid rgba(255, 255, 255, 0.06);
74
+ }
75
+
76
+ @utility glow-accent {
77
+ box-shadow: 0 0 20px color-mix(in srgb, var(--color-accent) 30%, transparent),
78
+ 0 0 60px color-mix(in srgb, var(--color-accent) 10%, transparent);
79
+ }
80
+
81
+ @utility glow-premium {
82
+ box-shadow: 0 0 20px color-mix(in srgb, var(--color-premium-400) 30%, transparent),
83
+ 0 0 60px color-mix(in srgb, var(--color-premium-500) 10%, transparent);
84
+ }
85
+
86
+ .landing-card {
87
+ position: relative;
88
+ border-radius: 16px;
89
+ border: 1px solid rgba(255, 230, 0, 0.17);
90
+ background: rgba(255, 255, 255, 0.05);
91
+ backdrop-filter: blur(24px) saturate(160%);
92
+ -webkit-backdrop-filter: blur(24px) saturate(160%);
93
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25),
94
+ inset 0 1px 0 rgba(255, 255, 255, 0.06),
95
+ 0 0 40px rgba(255, 230, 0, 0.17);
96
+ }
97
+ .landing-card > * {
98
+ position: relative;
99
+ z-index: 1;
100
+ }
101
+
102
+ .holo-grid {
103
+ position: relative;
104
+ }
105
+
106
+ .ui-label {
107
+ font-family: var(--font-mono);
108
+ text-transform: uppercase;
109
+ letter-spacing: 0.15em;
110
+ font-size: 10px;
111
+ }
112
+
113
+ .ui-num {
114
+ font-family: var(--font-mono);
115
+ font-variant-numeric: tabular-nums;
116
+ }
117
+
118
+ h2 {
119
+ font-weight: 300;
120
+ text-transform: uppercase;
121
+ letter-spacing: 0.15em;
122
+ text-shadow: 0 0 12px var(--color-accent),
123
+ 0 0 30px color-mix(in srgb, var(--color-accent) 50%, transparent);
124
+ }
125
+
126
+ @keyframes flow {
127
+ 0%, 100% { background-position: 0% 50%; }
128
+ 50% { background-position: 100% 50%; }
129
+ }
130
+
131
+ .xp-fill {
132
+ background: linear-gradient(
133
+ 90deg,
134
+ color-mix(in srgb, var(--color-accent) 20%, var(--color-bg)),
135
+ var(--color-accent),
136
+ var(--color-accent-2),
137
+ var(--color-accent)
138
+ );
139
+ background-size: 300% 100%;
140
+ animation: flow 3s ease infinite;
141
+ }
142
+
143
+ @keyframes virtueShimmer {
144
+ 0%, 100% { background-position: 0% 50%; }
145
+ 50% { background-position: 100% 50%; }
146
+ }
147
+
148
+ .virtue-bar-fill {
149
+ background-size: 200% 100%;
150
+ animation: virtueShimmer 3s ease-in-out infinite;
151
+ transition: width 0.5s ease-out;
152
+ }
153
+
154
+ /* ── Corner Glow Blobs ── */
155
+ .corner-glow {
156
+ position: fixed;
157
+ pointer-events: none;
158
+ z-index: 0;
159
+ border-radius: 50%;
160
+ }
161
+
162
+ .corner-glow--tl {
163
+ top: -25%;
164
+ left: -15%;
165
+ width: 70vw;
166
+ height: 70vh;
167
+ background: radial-gradient(circle, rgba(255, 230, 0, 0.22) 0%, transparent 65%);
168
+ filter: blur(55px);
169
+ animation: glowTL 18s ease-in-out infinite alternate;
170
+ }
171
+
172
+ .corner-glow--tr {
173
+ top: -12%;
174
+ right: -12%;
175
+ width: 40vw;
176
+ height: 40vh;
177
+ background: radial-gradient(circle, rgba(255, 230, 0, 0.17) 0%, transparent 65%);
178
+ filter: blur(55px);
179
+ animation: glowTR 20s ease-in-out 3s infinite alternate;
180
+ }
181
+
182
+ .corner-glow--bl {
183
+ bottom: -15%;
184
+ left: -12%;
185
+ width: 45vw;
186
+ height: 45vh;
187
+ background: radial-gradient(circle, rgba(255, 230, 0, 0.17) 0%, transparent 65%);
188
+ filter: blur(60px);
189
+ animation: glowBL 17s ease-in-out 6s infinite alternate;
190
+ }
191
+
192
+ .corner-glow--br {
193
+ bottom: -25%;
194
+ right: -15%;
195
+ width: 65vw;
196
+ height: 65vh;
197
+ background: radial-gradient(circle, rgba(255, 230, 0, 0.21) 0%, transparent 65%);
198
+ filter: blur(60px);
199
+ animation: glowBR 22s ease-in-out 9s infinite alternate;
200
+ }
201
+
202
+ .corner-glow--ct {
203
+ top: 30%;
204
+ left: 30%;
205
+ width: 50vw;
206
+ height: 50vh;
207
+ background: radial-gradient(circle, rgba(255, 230, 0, 0.11) 0%, transparent 75%);
208
+ filter: blur(55px);
209
+ animation: glowCT 25s ease-in-out 5s infinite alternate;
210
+ }
211
+
212
+ .corner-glow--cb {
213
+ bottom: -10%;
214
+ left: 20%;
215
+ width: 40vw;
216
+ height: 40vh;
217
+ background: radial-gradient(circle, rgba(255, 230, 0, 0.14) 0%, transparent 70%);
218
+ filter: blur(65px);
219
+ animation: glowCB 23s ease-in-out 7s infinite alternate;
220
+ }
221
+
222
+ @keyframes glowTL {
223
+ 0% { opacity: 0.35; transform: translate(-10vw, -5vh) scale(0.9); }
224
+ 25% { opacity: 0.9; transform: translate(25vw, 15vh) scale(1.25); }
225
+ 50% { opacity: 0.5; transform: translate(-15vw, 30vh) scale(1.0); }
226
+ 75% { opacity: 0.85; transform: translate(20vw, -10vh) scale(1.3); }
227
+ 100% { opacity: 0.35; transform: translate(-10vw, -5vh) scale(0.9); }
228
+ }
229
+ @keyframes glowTR {
230
+ 0% { opacity: 0.3; transform: translate(5vw, -10vh) scale(1.05); }
231
+ 25% { opacity: 0.8; transform: translate(-30vw, 10vh) scale(0.75); }
232
+ 50% { opacity: 0.45; transform: translate(10vw, 25vh) scale(1.1); }
233
+ 75% { opacity: 0.85; transform: translate(-20vw, -15vh) scale(0.8); }
234
+ 100% { opacity: 0.3; transform: translate(5vw, -10vh) scale(1.05); }
235
+ }
236
+ @keyframes glowBL {
237
+ 0% { opacity: 0.4; transform: translate(-5vw, 15vh) scale(0.85); }
238
+ 25% { opacity: 0.85; transform: translate(25vw, -20vh) scale(1.2); }
239
+ 50% { opacity: 0.5; transform: translate(-20vw, 10vh) scale(0.9); }
240
+ 75% { opacity: 0.8; transform: translate(15vw, 20vh) scale(1.15); }
241
+ 100% { opacity: 0.4; transform: translate(-5vw, 15vh) scale(0.85); }
242
+ }
243
+ @keyframes glowBR {
244
+ 0% { opacity: 0.35; transform: translate(10vw, 10vh) scale(1.1); }
245
+ 25% { opacity: 0.85; transform: translate(-25vw, -20vh) scale(0.8); }
246
+ 50% { opacity: 0.5; transform: translate(15vw, -15vh) scale(1.2); }
247
+ 75% { opacity: 0.9; transform: translate(-10vw, 20vh) scale(0.85); }
248
+ 100% { opacity: 0.35; transform: translate(10vw, 10vh) scale(1.1); }
249
+ }
250
+ @keyframes glowCT {
251
+ 0% { opacity: 0.25; transform: translate(-20vw, 5vh) scale(0.9); }
252
+ 25% { opacity: 0.7; transform: translate(15vw, -15vh) scale(1.2); }
253
+ 50% { opacity: 0.35; transform: translate(-10vw, 20vh) scale(1.0); }
254
+ 75% { opacity: 0.65; transform: translate(20vw, 10vh) scale(1.15); }
255
+ 100% { opacity: 0.25; transform: translate(-20vw, 5vh) scale(0.9); }
256
+ }
257
+ @keyframes glowCB {
258
+ 0% { opacity: 0.35; transform: translate(0, 0) scale(0.9); }
259
+ 25% { opacity: 0.8; transform: translate(-10vw, -8vh) scale(1.1); }
260
+ 50% { opacity: 0.45; transform: translate(8vw, -12vh) scale(0.95); }
261
+ 75% { opacity: 0.75; transform: translate(-6vw, -4vh) scale(1.15); }
262
+ 100% { opacity: 0.35; transform: translate(0, 0) scale(0.9); }
263
+ }
264
+
265
+ /* ── Section Layout ── */
266
+ .section {
267
+ position: relative;
268
+ z-index: 10;
269
+ padding: 6rem 1.5rem;
270
+ max-width: 1200px;
271
+ margin: 0 auto;
272
+ }
273
+
274
+ .section--hero {
275
+ padding-top: 10rem;
276
+ padding-bottom: 8rem;
277
+ display: flex;
278
+ flex-direction: column;
279
+ align-items: center;
280
+ justify-content: center;
281
+ min-height: 100vh;
282
+ text-align: center;
283
+ }
284
+
285
+ @media (max-width: 640px) {
286
+ .section--hero {
287
+ padding-top: 3rem;
288
+ padding-bottom: 4rem;
289
+ min-height: auto;
290
+ }
291
+ }
292
+
293
+ .section-title {
294
+ font-family: var(--font-heading);
295
+ font-weight: 300;
296
+ text-transform: uppercase;
297
+ letter-spacing: 0.1em;
298
+ font-size: 2rem;
299
+ margin-bottom: 0.5rem;
300
+ text-shadow: 0 0 12px var(--color-accent), 0 0 30px color-mix(in srgb, var(--color-accent) 50%, transparent);
301
+ }
302
+
303
+ /* ── Brand: "Mey" — ALWAYS Chakra Petch, never themed, all projects ── */
304
+ .brand-mey {
305
+ font-family: 'Chakra Petch', sans-serif;
306
+ font-weight: 700;
307
+ color: var(--color-text);
308
+ }
309
+
310
+ /* ── Brand: "Cult" in MeyCult — New Rocker ── */
311
+ .brand-cult {
312
+ font-family: var(--font-cult), var(--font-logo), sans-serif;
313
+ font-size: 1.10em; /* relative to Mey */
314
+ margin-left: -0.04em; /* gap Mey → Cult */
315
+ letter-spacing: -0.01em; /* gaps between Cult letters */
316
+ color: var(--color-accent);
317
+ }
318
+
319
+ /* ── Brand: "Unveil" in MeyCult Unveil — New Rocker, no glow ── */
320
+ .brand-unveil {
321
+ font-family: var(--font-cult), var(--font-logo), sans-serif;
322
+ letter-spacing: 0.02em;
323
+ color: var(--color-accent);
324
+ }
325
+
326
+ /* ── Light glow behind Unveil, same treatment as MeyCult wordmark ── */
327
+ .unveil-glow {
328
+ text-shadow:
329
+ 0 0 40px rgba(255, 255, 255, 0.35),
330
+ 0 0 80px rgba(255, 255, 255, 0.15),
331
+ 0 0 120px rgba(255, 255, 255, 0.08);
332
+ }
333
+
334
+ /* ── Unveil tablet: yellow stone, engraved glyph frieze ── */
335
+ .tablet-panel {
336
+ position: relative;
337
+ border-radius: 18px;
338
+ border: 2px solid rgba(255, 230, 0, 0.35);
339
+ background: linear-gradient(180deg, #2a2003 0%, #171402 100%);
340
+ box-shadow:
341
+ inset 0 2px 24px rgba(0, 0, 0, 0.65),
342
+ inset 0 -1px 0 rgba(255, 247, 161, 0.18),
343
+ 0 0 60px rgba(255, 230, 0, 0.25);
344
+ padding: 2rem 1.5rem;
345
+ }
346
+ @media (min-width: 640px) {
347
+ .tablet-panel { padding: 3rem 3.5rem; }
348
+ }
349
+ .tablet-frieze {
350
+ font-family: 'Noto Sans Egyptian Hieroglyphs', serif;
351
+ font-size: 1.15rem;
352
+ letter-spacing: 0.35em;
353
+ white-space: nowrap;
354
+ overflow: hidden;
355
+ user-select: none;
356
+ color: #6b5d00;
357
+ text-shadow:
358
+ 0 -1px 0 rgba(255, 247, 161, 0.35),
359
+ 0 1px 1px rgba(0, 0, 0, 0.8);
360
+ }
361
+ .tablet-frieze--top { margin-bottom: 1.5rem; }
362
+ .tablet-frieze--bottom { margin-top: 1.5rem; }
363
+
364
+ .section-subtitle {
365
+ color: var(--color-text-muted);
366
+ font-size: 0.95rem;
367
+ margin-bottom: 3rem;
368
+ text-align: center;
369
+ }
370
+
371
+ .table-glass {
372
+ width: 100%;
373
+ border-collapse: separate;
374
+ border-spacing: 0;
375
+ }
376
+
377
+ .table-glass th {
378
+ text-align: left;
379
+ font-family: var(--font-mono);
380
+ font-size: 10px;
381
+ text-transform: uppercase;
382
+ letter-spacing: 0.1em;
383
+ color: var(--color-text-muted);
384
+ padding: 0.75rem 1rem;
385
+ border-bottom: 1px solid var(--color-line);
386
+ }
387
+
388
+ .table-glass td {
389
+ padding: 0.75rem 1rem;
390
+ border-bottom: 1px solid rgba(255, 230, 0, 0.17);
391
+ font-size: 0.875rem;
392
+ }
393
+
394
+ .table-glass tr:last-child td {
395
+ border-bottom: none;
396
+ }
@@ -0,0 +1,103 @@
1
+ import { createClient, type SupabaseClient } from '@supabase/supabase-js'
2
+
3
+ export interface BrowserEnv {
4
+ url: string
5
+ anonKey: string
6
+ }
7
+
8
+ /**
9
+ * Chunked cookie storage — shares the session across subdomains
10
+ * (e.g. app.meycult.com + shop.meycult.com via domain=.meycult.com)
11
+ * while staying under the ~4KB cookie limit.
12
+ */
13
+ export function chunkedCookieStorage(
14
+ storageKey: string,
15
+ cookieDomain: string | null,
16
+ chunkSize = 3200,
17
+ ): Pick<Storage, 'getItem' | 'setItem' | 'removeItem'> {
18
+ const domainAttr = cookieDomain ? `; domain=${cookieDomain}` : ''
19
+
20
+ function readCookie(name: string): string | null {
21
+ if (typeof document === 'undefined') return null
22
+ const match = document.cookie.match(
23
+ new RegExp('(?:^|; )' + name.replace(/([.$?*|{}()[\]\\/+^])/g, '\\$1') + '=([^;]*)'),
24
+ )
25
+ return match ? decodeURIComponent(match[1]) : null
26
+ }
27
+
28
+ function writeCookie(name: string, value: string, maxAge = 60 * 60 * 24 * 30): void {
29
+ document.cookie = `${name}=${encodeURIComponent(value)}; path=/${domainAttr}; max-age=${maxAge}; SameSite=Lax; Secure`
30
+ }
31
+
32
+ function deleteCookie(name: string): void {
33
+ document.cookie = `${name}=; path=/${domainAttr}; max-age=0; SameSite=Lax; Secure`
34
+ }
35
+
36
+ function countKey(): string {
37
+ return `${storageKey}.n`
38
+ }
39
+
40
+ return {
41
+ getItem(key: string): string | null {
42
+ const count = readCookie(countKey())
43
+ if (count) {
44
+ let out = ''
45
+ for (let i = 0; i < Number(count); i++) out += readCookie(`${key}.${i}`) ?? ''
46
+ return out || null
47
+ }
48
+ return readCookie(key)
49
+ },
50
+ setItem(key: string, value: string): void {
51
+ const prev = readCookie(countKey())
52
+ if (prev) for (let i = 0; i < Number(prev); i++) deleteCookie(`${key}.${i}`)
53
+ deleteCookie(key)
54
+ if (value.length <= chunkSize) {
55
+ writeCookie(key, value)
56
+ deleteCookie(countKey())
57
+ return
58
+ }
59
+ const parts = Math.ceil(value.length / chunkSize)
60
+ for (let i = 0; i < parts; i++) writeCookie(`${key}.${i}`, value.slice(i * chunkSize, (i + 1) * chunkSize))
61
+ writeCookie(countKey(), String(parts))
62
+ },
63
+ removeItem(key: string): void {
64
+ const count = readCookie(countKey())
65
+ if (count) for (let i = 0; i < Number(count); i++) deleteCookie(`${key}.${i}`)
66
+ deleteCookie(countKey())
67
+ deleteCookie(key)
68
+ },
69
+ }
70
+ }
71
+
72
+ export interface BrowserOptions extends BrowserEnv {
73
+ /** e.g. 'meycult-auth'. Defaults to 'meycult-auth'. */
74
+ storageKey?: string
75
+ /** e.g. '.meycult.com' on prod, null on localhost. Defaults to hostname heuristic. */
76
+ cookieDomain?: string | null
77
+ detectSessionInUrl?: boolean
78
+ }
79
+
80
+ function defaultCookieDomain(): string | null {
81
+ if (typeof window === 'undefined') return null
82
+ return window.location.hostname.endsWith('meycult.com') ? '.meycult.com' : null
83
+ }
84
+
85
+ export function createSupabaseBrowser(options: BrowserOptions): SupabaseClient {
86
+ const {
87
+ url,
88
+ anonKey,
89
+ storageKey = 'meycult-auth',
90
+ cookieDomain = defaultCookieDomain(),
91
+ detectSessionInUrl = true,
92
+ } = options
93
+ const onDomain = cookieDomain !== null
94
+ return createClient(url, anonKey, {
95
+ auth: {
96
+ storageKey,
97
+ persistSession: true,
98
+ autoRefreshToken: true,
99
+ detectSessionInUrl,
100
+ ...(onDomain ? { storage: chunkedCookieStorage(storageKey, cookieDomain) } : {}),
101
+ },
102
+ })
103
+ }
@@ -0,0 +1,15 @@
1
+ import type { SupabaseClient } from '@supabase/supabase-js'
2
+
3
+ /**
4
+ * OAuth callback helper — host route calls this with its server client,
5
+ * then redirects to `next` (default '/').
6
+ */
7
+ export async function exchangeCodeForSession(
8
+ supabase: SupabaseClient,
9
+ code: string | null,
10
+ next = '/',
11
+ ): Promise<{ next: string; error: string | null }> {
12
+ if (!code) return { next, error: 'missing_code' }
13
+ const { error } = await supabase.auth.exchangeCodeForSession(code)
14
+ return { next, error: error ? error.message : null }
15
+ }
@@ -0,0 +1,36 @@
1
+ import { createServerClient } from '@supabase/ssr'
2
+ import { NextResponse, type NextRequest } from 'next/server'
3
+
4
+ /**
5
+ * Refresh the Supabase session on every request so Server Components
6
+ * always see a valid token. Wire into the host's proxy.ts:
7
+ * export default coreUpdateSession as middleware-style default export
8
+ */
9
+ export async function updateSession(request: NextRequest) {
10
+ let supabaseResponse = NextResponse.next({ request })
11
+
12
+ const supabase = createServerClient(
13
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
14
+ process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
15
+ {
16
+ cookies: {
17
+ getAll() {
18
+ return request.cookies.getAll()
19
+ },
20
+ setAll(
21
+ cookiesToSet: { name: string; value: string; options?: Record<string, unknown> }[],
22
+ ) {
23
+ cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))
24
+ supabaseResponse = NextResponse.next({ request })
25
+ cookiesToSet.forEach(({ name, value, options }) =>
26
+ supabaseResponse.cookies.set(name, value, options),
27
+ )
28
+ },
29
+ },
30
+ },
31
+ )
32
+
33
+ await supabase.auth.getClaims()
34
+
35
+ return supabaseResponse
36
+ }
@@ -0,0 +1,31 @@
1
+ import { createServerClient } from '@supabase/ssr'
2
+ import { cookies } from 'next/headers'
3
+
4
+ /** Next.js Server Component / Route Handler / Server Action client. */
5
+ export async function createSupabaseServerClient() {
6
+ const cookieStore = await cookies()
7
+
8
+ return createServerClient(
9
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
10
+ process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
11
+ {
12
+ cookies: {
13
+ getAll() {
14
+ return cookieStore.getAll()
15
+ },
16
+ setAll(
17
+ cookiesToSet: { name: string; value: string; options?: Record<string, unknown> }[],
18
+ ) {
19
+ try {
20
+ cookiesToSet.forEach(({ name, value, options }) =>
21
+ cookieStore.set(name, value, options),
22
+ )
23
+ } catch {
24
+ // Called from a Server Component — read-only there.
25
+ // The proxy refreshes sessions on every request.
26
+ }
27
+ },
28
+ },
29
+ },
30
+ )
31
+ }
package/src/tokens.ts ADDED
@@ -0,0 +1,37 @@
1
+ // TS mirror of src/styles/theme.css @theme tokens — for canvas/tests/Node use.
2
+
3
+ export const COLORS = {
4
+ bg: '#0B0A02',
5
+ surface: '#171402',
6
+ panel: 'rgba(26, 22, 3, 0.55)',
7
+ accent: '#FFE600',
8
+ accent2: '#FFF7A1',
9
+ text: '#FFF9DB',
10
+ textMuted: 'rgba(255, 249, 219, 0.55)',
11
+ line: 'rgba(255, 230, 0, 0.18)',
12
+ yes: '#FFE600',
13
+ no: '#e04a4a',
14
+ yesBg: 'rgba(255, 230, 0, 0.08)',
15
+ noBg: 'rgba(224, 74, 74, 0.12)',
16
+ success: '#FFE600',
17
+ danger: '#e04a4a',
18
+ warning: '#FF9D00',
19
+ info: '#FFE600',
20
+ premium400: '#FFB000',
21
+ premium500: '#9C7500',
22
+ premium600: '#6B4E00',
23
+ } as const
24
+
25
+ export const GLOW = {
26
+ text: '0 0 20px rgba(255, 230, 0, 0.18)',
27
+ brandCult:
28
+ '0 0 40px rgba(255,230,0,0.35), 0 0 80px rgba(255,230,0,0.18), 0 0 120px rgba(255,230,0,0.08)',
29
+ } as const
30
+
31
+ export const FONTS = {
32
+ heading: 'Rajdhani, sans-serif',
33
+ body: '"Space Grotesk", sans-serif',
34
+ mono: '"Space Mono", monospace',
35
+ logo: '"Chakra Petch", sans-serif',
36
+ cult: '"New Rocker", "Chakra Petch", sans-serif',
37
+ } as const
@@ -0,0 +1,33 @@
1
+ import { AlertTriangle, CheckCircle, Info } from "lucide-react";
2
+
3
+ type CalloutType = "info" | "warning" | "success" | "danger";
4
+
5
+ const icons: Record<CalloutType, React.ReactNode> = {
6
+ info: <Info className="h-5 w-5" />,
7
+ warning: <AlertTriangle className="h-5 w-5" />,
8
+ success: <CheckCircle className="h-5 w-5" />,
9
+ danger: <AlertTriangle className="h-5 w-5" />,
10
+ };
11
+
12
+ const styles: Record<CalloutType, string> = {
13
+ info: "border-accent bg-accent/10 text-text",
14
+ warning: "border-amber-500 bg-amber-950/30 text-amber-100",
15
+ success: "border-green-500 bg-green-950/30 text-green-100",
16
+ danger: "border-red-500 bg-red-950/30 text-red-100",
17
+ };
18
+
19
+ interface CalloutProps {
20
+ type?: CalloutType;
21
+ children: React.ReactNode;
22
+ }
23
+
24
+ export function Callout({ type = "info", children }: CalloutProps) {
25
+ return (
26
+ <div className={`my-6 rounded-lg border-l-4 p-4 not-prose ${styles[type]}`}>
27
+ <div className="flex items-start gap-3">
28
+ <span className="mt-0.5">{icons[type]}</span>
29
+ <div className="[&>p]:m-0">{children}</div>
30
+ </div>
31
+ </div>
32
+ );
33
+ }
@@ -0,0 +1,42 @@
1
+ import { type ComponentPropsWithoutRef, type CSSProperties, type FC } from "react";
2
+
3
+ import { cn } from "../cn";
4
+
5
+ export interface AnimatedShinyTextProps extends ComponentPropsWithoutRef<"span"> {
6
+ shimmerWidth?: number;
7
+ gradientFrom?: string;
8
+ gradientVia?: string;
9
+ gradientTo?: string;
10
+ }
11
+
12
+ export const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({
13
+ children,
14
+ className,
15
+ shimmerWidth = 100,
16
+ gradientFrom = "transparent",
17
+ gradientVia = "rgba(124,255,46,0.65)",
18
+ gradientTo = "transparent",
19
+ ...props
20
+ }) => {
21
+ return (
22
+ <span
23
+ style={
24
+ {
25
+ "--shiny-width": `${shimmerWidth}px`,
26
+ backgroundImage: `linear-gradient(to right, ${gradientFrom}, ${gradientVia}, ${gradientTo})`,
27
+ } as CSSProperties
28
+ }
29
+ className={cn(
30
+ "mx-auto max-w-md",
31
+ "text-neutral-600/70 dark:text-neutral-400/70",
32
+ "animate-shiny-text bg-clip-text bg-no-repeat",
33
+ "[background-size:var(--shiny-width)_100%]",
34
+ "[transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]",
35
+ className,
36
+ )}
37
+ {...props}
38
+ >
39
+ {children}
40
+ </span>
41
+ );
42
+ };