@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,102 @@
1
+ import * as React from "react"
2
+ import { cn } from "../cn"
3
+
4
+ function Card({
5
+ className,
6
+ size = "default",
7
+ ...props
8
+ }: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
9
+ return (
10
+ <div
11
+ data-slot="card"
12
+ data-size={size}
13
+ className={cn(
14
+ "group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
15
+ className
16
+ )}
17
+ {...props}
18
+ />
19
+ )
20
+ }
21
+
22
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
23
+ return (
24
+ <div
25
+ data-slot="card-header"
26
+ className={cn(
27
+ "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
28
+ className
29
+ )}
30
+ {...props}
31
+ />
32
+ )
33
+ }
34
+
35
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
36
+ return (
37
+ <div
38
+ data-slot="card-title"
39
+ className={cn(
40
+ "font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
41
+ className
42
+ )}
43
+ {...props}
44
+ />
45
+ )
46
+ }
47
+
48
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
49
+ return (
50
+ <div
51
+ data-slot="card-description"
52
+ className={cn("text-sm text-muted-foreground", className)}
53
+ {...props}
54
+ />
55
+ )
56
+ }
57
+
58
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
59
+ return (
60
+ <div
61
+ data-slot="card-action"
62
+ className={cn(
63
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
64
+ className
65
+ )}
66
+ {...props}
67
+ />
68
+ )
69
+ }
70
+
71
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
72
+ return (
73
+ <div
74
+ data-slot="card-content"
75
+ className={cn("px-(--card-spacing)", className)}
76
+ {...props}
77
+ />
78
+ )
79
+ }
80
+
81
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
82
+ return (
83
+ <div
84
+ data-slot="card-footer"
85
+ className={cn(
86
+ "flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)",
87
+ className
88
+ )}
89
+ {...props}
90
+ />
91
+ )
92
+ }
93
+
94
+ export {
95
+ Card,
96
+ CardHeader,
97
+ CardFooter,
98
+ CardTitle,
99
+ CardAction,
100
+ CardDescription,
101
+ CardContent,
102
+ }
@@ -0,0 +1,81 @@
1
+ import React, { type ComponentPropsWithoutRef, type CSSProperties } from "react";
2
+
3
+ import { cn } from "../cn";
4
+
5
+ export interface ShimmerButtonProps extends ComponentPropsWithoutRef<"button"> {
6
+ shimmerColor?: string;
7
+ shimmerSize?: string;
8
+ borderRadius?: string;
9
+ shimmerDuration?: string;
10
+ background?: string;
11
+ className?: string;
12
+ children?: React.ReactNode;
13
+ }
14
+
15
+ export const ShimmerButton = React.forwardRef<HTMLButtonElement, ShimmerButtonProps>(
16
+ (
17
+ {
18
+ shimmerColor = "#b6ff7a",
19
+ shimmerSize = "0.05em",
20
+ shimmerDuration = "3s",
21
+ borderRadius = "100px",
22
+ background = "rgba(4, 16, 7, 1)",
23
+ className,
24
+ children,
25
+ ...props
26
+ },
27
+ ref,
28
+ ) => {
29
+ return (
30
+ <button
31
+ style={
32
+ {
33
+ "--spread": "90deg",
34
+ "--shimmer-color": shimmerColor,
35
+ "--radius": borderRadius,
36
+ "--speed": shimmerDuration,
37
+ "--cut": shimmerSize,
38
+ "--bg": background,
39
+ } as CSSProperties
40
+ }
41
+ className={cn(
42
+ "group relative z-0 flex cursor-pointer items-center justify-center overflow-hidden [border-radius:var(--radius)] border border-white/10 px-6 py-3 whitespace-nowrap text-white [background:var(--bg)]",
43
+ "transform-gpu transition-transform duration-300 ease-in-out active:translate-y-px",
44
+ className,
45
+ )}
46
+ ref={ref}
47
+ {...props}
48
+ >
49
+ <div
50
+ className={cn(
51
+ "-z-30 blur-[2px]",
52
+ "[container-type:size] absolute inset-0 overflow-visible",
53
+ )}
54
+ >
55
+ <div className="animate-shimmer-slide absolute inset-0 [aspect-ratio:1] h-[100cqh] [border-radius:0] [mask:none]">
56
+ <div className="animate-spin-around absolute -inset-full w-auto [translate:0_0] rotate-0 [background:conic-gradient(from_calc(270deg-(var(--spread)*0.5)),transparent_0,var(--shimmer-color)_var(--spread),transparent_var(--spread))]" />
57
+ </div>
58
+ </div>
59
+ {children}
60
+
61
+ <div
62
+ className={cn(
63
+ "absolute inset-0 size-full",
64
+ "rounded-2xl px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#ffffff1f]",
65
+ "transform-gpu transition-all duration-300 ease-in-out",
66
+ "group-hover:shadow-[inset_0_-6px_10px_#ffffff3f]",
67
+ "group-active:shadow-[inset_0_-10px_10px_#ffffff3f]",
68
+ )}
69
+ />
70
+
71
+ <div
72
+ className={cn(
73
+ "absolute [inset:var(--cut)] -z-20 [border-radius:var(--radius)] [background:var(--bg)]",
74
+ )}
75
+ />
76
+ </button>
77
+ );
78
+ },
79
+ );
80
+
81
+ ShimmerButton.displayName = "ShimmerButton";
@@ -0,0 +1,32 @@
1
+ 'use client'
2
+
3
+ /** Fixed ambient stage behind the Unveil countdown. Kept set only. */
4
+ import { useEffect } from 'react'
5
+ import { GrainVeil } from '@meyvoid/core/backgrounds'
6
+ import { BG_STYLES } from './bgStyles'
7
+ import { parseBgQuery, useUnveilBg } from './bg'
8
+ import { AshCanvas, BreathVignette, RuneCanvas, VeilFog } from './effects'
9
+
10
+ export function LaunchStage() {
11
+ const { effects, density, speed, opacity, grain, setMany } = useUnveilBg()
12
+
13
+ useEffect(() => {
14
+ useUnveilBg.persist.rehydrate()
15
+ const next = parseBgQuery(window.location.search)
16
+ if (next) setMany(next)
17
+ // eslint-disable-next-line react-hooks/exhaustive-deps
18
+ }, [])
19
+
20
+ const fx = { density, speed, opacity }
21
+
22
+ return (
23
+ <div className="pointer-events-none fixed inset-0 z-0 overflow-hidden" aria-hidden="true">
24
+ <style>{BG_STYLES}</style>
25
+ {effects.includes('veil') && <VeilFog opacity={opacity} />}
26
+ {effects.includes('ash') && <AshCanvas {...fx} />}
27
+ {effects.includes('runes') && <RuneCanvas {...fx} />}
28
+ {effects.includes('breath') && <BreathVignette />}
29
+ {grain && <GrainVeil />}
30
+ </div>
31
+ )
32
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * TabletPanel — yellow engraved tablet enclosing Unveil details.
3
+ * Dark bronze body, gold border, engraved hieroglyph frieze top + bottom
4
+ * (letterpress technique: darker-than-surface glyphs, light up-left edge).
5
+ */
6
+ import type { ReactNode } from 'react'
7
+
8
+ const FRIEZE =
9
+ '𓂀 𓆣 𓅓 𓇳 𓊵 𓆙 𓆄 ☥ △ ◉ 𓂀 𓆣 𓅓 𓇳 𓊵 𓆙 𓆄 ☥ △ ◉ 𓂀 𓆣 𓅓 𓇳 𓊵 𓆙 𓆄 ☥ △ ◉'
10
+
11
+ export function TabletPanel({ children }: { children: ReactNode }) {
12
+ return (
13
+ <div className="tablet-panel">
14
+ <div className="tablet-frieze tablet-frieze--top" aria-hidden="true">
15
+ {FRIEZE}
16
+ </div>
17
+ {children}
18
+ <div className="tablet-frieze tablet-frieze--bottom" aria-hidden="true">
19
+ {FRIEZE}
20
+ </div>
21
+ </div>
22
+ )
23
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * MeyCult Unveil background state. Kept set: glyphs, veil, ash, breath.
3
+ * The six retired layers live in @meyvoid/core backgrounds for reuse.
4
+ */
5
+ import { createPersistStore } from '../lib/store'
6
+
7
+ export type BgEffect = 'runes' | 'veil' | 'ash' | 'breath'
8
+
9
+ export const BG_EFFECTS: Array<{ id: BgEffect; label: string }> = [
10
+ { id: 'runes', label: 'Egyptian Glyphs' },
11
+ { id: 'veil', label: 'Veil Fog' },
12
+ { id: 'ash', label: 'Ash Flow-Field' },
13
+ { id: 'breath', label: 'Breath Vignette' },
14
+ ]
15
+
16
+ export const BG_DEFAULTS: BgEffect[] = ['runes', 'veil', 'ash', 'breath']
17
+
18
+ interface BgState {
19
+ effects: BgEffect[]
20
+ density: number // 0-100
21
+ speed: number // 0-2
22
+ opacity: number // 0-1
23
+ grain: boolean
24
+ toggle: (e: BgEffect) => void
25
+ setMany: (e: BgEffect[]) => void
26
+ set: (p: Partial<Pick<BgState, 'density' | 'speed' | 'opacity' | 'grain'>>) => void
27
+ reset: () => void
28
+ }
29
+
30
+ export const useUnveilBg = createPersistStore<BgState>(
31
+ 'meycult-unveil-bg-v1',
32
+ 1,
33
+ (set) => ({
34
+ effects: BG_DEFAULTS,
35
+ density: 50,
36
+ speed: 0.6,
37
+ opacity: 0.8,
38
+ grain: true,
39
+ toggle: (e) =>
40
+ set((s) => ({
41
+ effects: s.effects.includes(e)
42
+ ? s.effects.filter((x) => x !== e)
43
+ : [...s.effects, e],
44
+ })),
45
+ setMany: (effects) => set({ effects }),
46
+ set: (p) => set(p),
47
+ reset: () =>
48
+ set({ effects: BG_DEFAULTS, density: 50, speed: 0.6, opacity: 0.8, grain: true }),
49
+ }),
50
+ )
51
+
52
+ /** Read-only ?bg=veil,runes share param (applied once on mount). */
53
+ export function parseBgQuery(search: string): BgEffect[] | null {
54
+ const m = new URLSearchParams(search).get('bg')
55
+ if (!m) return null
56
+ const ids = m.split(',').map((s) => s.trim()) as BgEffect[]
57
+ const valid = ids.filter((id) => BG_EFFECTS.some((e) => e.id === id))
58
+ return valid.length ? valid : null
59
+ }
@@ -0,0 +1,13 @@
1
+ /** CSS-only Unveil background layers (injected via <style> by LaunchStage).
2
+ * Kept set: glyphs canvas, veil fog, ash flow, breath vignette. The six
3
+ * retired layers live in @meyvoid/core backgrounds for future reuse. */
4
+ export const BG_STYLES = `
5
+ .mey-veil{position:absolute;inset:auto 0 0 0;height:44vh;filter:blur(60px);pointer-events:none}
6
+ .mey-veil i{position:absolute;width:55vw;height:22vh;border-radius:50%;background:radial-gradient(closest-side,rgba(255,230,0,.16),transparent 70%)}
7
+ .mey-veil i:nth-child(2){background:radial-gradient(closest-side,rgba(255,230,0,.18),transparent 70%);animation:mey-veil-drift 38s ease-in-out infinite alternate}
8
+ @keyframes mey-veil-drift{from{transform:translateX(-8vw)}to{transform:translateX(8vw)}}
9
+ .mey-breath{position:absolute;inset:0;pointer-events:none;box-shadow:inset 0 0 180px 60px rgba(0,0,0,.75);animation:mey-breath 7s ease-in-out infinite}
10
+ @keyframes mey-breath{50%{box-shadow:inset 0 0 240px 90px rgba(0,0,0,.9)}}
11
+ .mey-bg-canvas{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}
12
+ @media (prefers-reduced-motion:reduce){.mey-veil i:nth-child(2),.mey-breath{animation:none}}
13
+ `
@@ -0,0 +1,199 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Unveil page blocks — MeyGod festival anatomy in cult tokens:
5
+ * hero (eye + countdown), where/when (map), cards, hype+QR, notify.
6
+ */
7
+ import type { ReactNode } from 'react'
8
+ import { MeyQR } from '@meyvoid/core/qr'
9
+ import CultEye from '../brand/CultEye'
10
+ import { CultCountdown } from '../components/CultCountdown'
11
+ import { UNVEIL_AT, unveilDayLabel, unveilLocalLabel } from '../unveil'
12
+
13
+ export function UnveilHero() {
14
+ return (
15
+ <section className="section section--hero">
16
+ <a href="/unveil/countdown" aria-label="Open the full-page countdown" data-testid="unveil-sigil">
17
+ <div className="w-[200px] sm:w-[280px]">
18
+ <CultEye fluid />
19
+ </div>
20
+ </a>
21
+
22
+ <div className="mt-2 flex flex-col items-center leading-none">
23
+ <span className="text-6xl sm:text-7xl font-bold tracking-wider">
24
+ <span className="brand-mey">Mey</span>
25
+ <span className="brand-cult">Cult</span>
26
+ </span>
27
+ <span className="brand-unveil unveil-glow mt-1 text-5xl sm:text-6xl">Unveil</span>
28
+ </div>
29
+ <p className="mt-3 text-sm text-text-muted tracking-wide">
30
+ {unveilDayLabel(UNVEIL_AT)} SAST · Stellenbosch
31
+ </p>
32
+ <p className="text-xs text-text-muted">{unveilLocalLabel(UNVEIL_AT)} your time</p>
33
+
34
+ <div className="mt-8 w-full max-w-2xl">
35
+ <CultCountdown />
36
+ </div>
37
+ <p className="mt-3 text-xs text-text-muted">Touch the sigil for the full-page countdown.</p>
38
+
39
+ <div className="mt-8 flex flex-wrap justify-center gap-3">
40
+ <a
41
+ className="px-6 py-2.5 rounded-lg bg-accent text-bg font-bold text-sm uppercase tracking-wider"
42
+ href="/#crowdfunding"
43
+ >
44
+ Notify me
45
+ </a>
46
+ <a
47
+ className="px-6 py-2.5 rounded-lg border border-line/40 text-sm uppercase tracking-wider text-text"
48
+ href="/unveil/countdown"
49
+ >
50
+ Full countdown
51
+ </a>
52
+ </div>
53
+ </section>
54
+ )
55
+ }
56
+
57
+ export interface WhereWhenProps {
58
+ venue?: string
59
+ city?: string
60
+ mapsUrl?: string
61
+ footnote?: string
62
+ map?: { src: string; alt: string }
63
+ }
64
+
65
+ export function WhereWhen({
66
+ venue = 'Venue TBA — Stellenbosch',
67
+ city = 'Stellenbosch, Western Cape',
68
+ mapsUrl,
69
+ footnote = 'Doors 19:30. Free to attend. Bring a friend; leave as Cult.',
70
+ map,
71
+ }: WhereWhenProps) {
72
+ const href =
73
+ mapsUrl ?? 'https://www.google.com/maps/search/?api=1&query=-33.9347,18.8607'
74
+ return (
75
+ <div className="grid grid-cols-1 sm:grid-cols-[2fr_1fr] gap-8 sm:gap-12 items-center">
76
+ <div className="justify-self-start w-full">
77
+ {map ? (
78
+ <a href={href} target="_blank" rel="noreferrer" className="block w-full group">
79
+ <div
80
+ className="w-full aspect-[2/1] overflow-hidden rounded-xl transition-transform duration-300 group-hover:scale-[1.02]"
81
+ style={{ border: '1px solid rgba(255,230,0,0.18)', boxShadow: '0 0 40px rgba(255,230,0,0.12)' }}
82
+ >
83
+ {/* eslint-disable-next-line @next/next/no-img-element */}
84
+ <img src={map.src} alt={map.alt} className="w-full h-full object-cover" />
85
+ </div>
86
+ <p className="mt-2 text-[10px] uppercase tracking-[0.25em] text-text-muted/60 text-center">
87
+ © OpenStreetMap contributors
88
+ </p>
89
+ </a>
90
+ ) : (
91
+ <div
92
+ className="w-full aspect-[2/1] rounded-xl flex items-center justify-center"
93
+ style={{ border: '1px solid rgba(255,230,0,0.18)', background: 'rgba(0,0,0,0.3)' }}
94
+ >
95
+ <div className="w-[64px]">
96
+ <CultEye fluid />
97
+ </div>
98
+ </div>
99
+ )}
100
+ </div>
101
+ <div className="text-center sm:text-right sm:border-l sm:border-dashed sm:pl-12 justify-self-end w-full border-line">
102
+ <h2 className="section-title" style={{ fontSize: '1.875rem', letterSpacing: '0.08em' }}>
103
+ Where and When
104
+ </h2>
105
+ <div className="mt-4 text-sm sm:text-base tracking-[0.2em] text-accent" style={{ fontFamily: 'var(--font-heading)' }}>
106
+ {unveilDayLabel(UNVEIL_AT).toUpperCase()}
107
+ </div>
108
+ <div className="mt-1 text-base sm:text-lg text-text font-medium">{venue}</div>
109
+ <div className="text-sm text-text-muted">{city}</div>
110
+ <div className="mt-4 text-[10px] uppercase tracking-[0.35em] text-text-muted">The Hour</div>
111
+ <div className="mt-0.5 text-4xl sm:text-5xl font-bold leading-none text-text" style={{ fontFamily: 'var(--font-heading)' }}>
112
+ 20:00
113
+ </div>
114
+ {footnote && <p className="mt-4 text-xs sm:text-sm text-text-muted italic leading-relaxed">{footnote}</p>}
115
+ </div>
116
+ </div>
117
+ )
118
+ }
119
+
120
+ export interface UnveilCard {
121
+ title: ReactNode
122
+ body: string
123
+ }
124
+
125
+ export function CardGrid({ cards, cols = 3 }: { cards: UnveilCard[]; cols?: 2 | 3 }) {
126
+ const colCls = cols === 2 ? 'md:grid-cols-2' : 'md:grid-cols-2 lg:grid-cols-3'
127
+ return (
128
+ <div className={`grid grid-cols-1 ${colCls} gap-3 max-w-3xl mx-auto`}>
129
+ {cards.map((c, i) => (
130
+ <div key={i} className="landing-card p-4 text-center">
131
+ <h3 className="text-sm font-bold text-text uppercase tracking-wider" style={{ fontFamily: 'var(--font-heading)' }}>
132
+ {c.title}
133
+ </h3>
134
+ <p className="text-[11px] text-text-muted mt-1 leading-relaxed">{c.body}</p>
135
+ </div>
136
+ ))}
137
+ </div>
138
+ )
139
+ }
140
+
141
+ export function HypeRow({ qrUrl }: { qrUrl: string }) {
142
+ return (
143
+ <div className="grid grid-cols-1 sm:grid-cols-[2fr_1fr] gap-8 items-center max-w-3xl mx-auto">
144
+ <div>
145
+ <h2 className="section-title">What is the Unveil?</h2>
146
+ <p className="text-text-muted text-sm sm:text-base leading-relaxed">
147
+ The first <span className="text-text font-semibold">playable prophecy market</span>. Not
148
+ watching — <span className="text-text font-semibold">playing</span>. Quests seeded hourly,
149
+ dual tokens, heroes that level as you wager. On stage in Stellenbosch, live on YouTube —
150
+ then in your pocket.
151
+ </p>
152
+ <p className="text-text-muted text-xs sm:text-sm leading-relaxed mt-3">
153
+ Free to play. Real USDC cash-out. Sweepstakes-legal.
154
+ </p>
155
+ <div className="mt-4 flex flex-wrap gap-3">
156
+ <a className="px-6 py-2.5 rounded-lg bg-accent text-bg font-bold text-sm uppercase tracking-wider" href="/#crowdfunding">
157
+ Get notified
158
+ </a>
159
+ <a className="px-6 py-2.5 rounded-lg border border-line/40 text-sm uppercase tracking-wider text-text" href="/whitepaper">
160
+ Read the whitepaper
161
+ </a>
162
+ </div>
163
+ </div>
164
+ <div className="justify-self-center w-[180px]">
165
+ <MeyQR
166
+ url={qrUrl}
167
+ brand="cult"
168
+ fg="#ffffff"
169
+ accent="#FFE600"
170
+ bg={null}
171
+ plaque="none"
172
+ style="styled"
173
+ size={180}
174
+ label="QR code — MeyCult Unveil"
175
+ />
176
+ <p className="mt-2 text-[10px] uppercase tracking-[0.25em] text-text-muted/60 text-center">
177
+ Pierce the veil
178
+ </p>
179
+ </div>
180
+ </div>
181
+ )
182
+ }
183
+
184
+ export function NotifyPanel() {
185
+ return (
186
+ <div className="landing-card p-8 max-w-lg mx-auto text-center">
187
+ <p className="text-accent font-bold text-lg mb-2">Be there at 20:00.</p>
188
+ <p className="text-sm text-text-muted mb-6">
189
+ One email when the veil lifts. No spam. No password.
190
+ </p>
191
+ <a
192
+ className="inline-block px-6 py-2.5 rounded-lg bg-accent text-bg font-bold text-sm uppercase tracking-wider"
193
+ href="/#crowdfunding"
194
+ >
195
+ Join the list
196
+ </a>
197
+ </div>
198
+ )
199
+ }