@meycult/core 0.1.0 → 0.2.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 (43) hide show
  1. package/package.json +3 -6
  2. package/src/brand/BrandName.tsx +4 -2
  3. package/src/brand/LaunchButton.tsx +5 -3
  4. package/src/brand/Logo.tsx +25 -6
  5. package/src/brand/PyramidName.tsx +10 -0
  6. package/src/brand/Sigil.tsx +10 -5
  7. package/src/brand/TokenIcons.tsx +12 -6
  8. package/src/brand.ts +4 -1
  9. package/src/components/CultCountdown.tsx +89 -14
  10. package/src/game/categories.ts +23 -0
  11. package/src/game/cults.ts +88 -0
  12. package/src/game/quests.ts +62 -0
  13. package/src/game/types.ts +27 -0
  14. package/src/index.ts +64 -21
  15. package/src/product.ts +17 -0
  16. package/src/styles/theme.css +97 -6
  17. package/src/tokens.ts +10 -2
  18. package/src/ui/CultButton.tsx +55 -0
  19. package/src/ui/RarityBadge.tsx +18 -0
  20. package/src/ui/animated-shiny-text.tsx +2 -2
  21. package/src/ui/badges.tsx +43 -0
  22. package/src/ui/card.tsx +20 -92
  23. package/src/ui/shimmer-button.tsx +5 -3
  24. package/src/ui/virtues.tsx +66 -0
  25. package/src/unveil/LaunchStage.tsx +3 -2
  26. package/src/unveil/bg.ts +20 -11
  27. package/src/unveil/bgStyles.ts +0 -1
  28. package/src/unveil/blocks.tsx +51 -14
  29. package/src/unveil/effects.tsx +182 -86
  30. package/src/unveil.ts +2 -21
  31. package/src/auth/Gate.tsx +0 -14
  32. package/src/auth/types.ts +0 -12
  33. package/src/brand/GodEmperor.tsx +0 -8
  34. package/src/cn.ts +0 -6
  35. package/src/lib/images.ts +0 -10
  36. package/src/lib/money.ts +0 -12
  37. package/src/lib/store.ts +0 -19
  38. package/src/shell/types.ts +0 -13
  39. package/src/supabase/browser.ts +0 -103
  40. package/src/supabase/callback.ts +0 -15
  41. package/src/supabase/proxy.ts +0 -36
  42. package/src/supabase/server.ts +0 -31
  43. package/src/ui/Callout.tsx +0 -33
package/src/unveil/bg.ts CHANGED
@@ -2,18 +2,28 @@
2
2
  * MeyCult Unveil background state. Kept set: glyphs, veil, ash, breath.
3
3
  * The six retired layers live in @meyvoid/core backgrounds for reuse.
4
4
  */
5
- import { createPersistStore } from '../lib/store'
5
+ import { createPersistStore } from '@meyvoid/core'
6
6
 
7
- export type BgEffect = 'runes' | 'veil' | 'ash' | 'breath'
7
+ export type BgEffect = 'wall' | 'runes' | 'veil' | 'ash' | 'breath'
8
8
 
9
9
  export const BG_EFFECTS: Array<{ id: BgEffect; label: string }> = [
10
+ { id: 'wall', label: 'Pyramid Wall' },
10
11
  { id: 'runes', label: 'Egyptian Glyphs' },
11
12
  { id: 'veil', label: 'Veil Fog' },
12
13
  { id: 'ash', label: 'Ash Flow-Field' },
13
14
  { id: 'breath', label: 'Breath Vignette' },
14
15
  ]
15
16
 
16
- export const BG_DEFAULTS: BgEffect[] = ['runes', 'veil', 'ash', 'breath']
17
+ export const BG_DEFAULTS: BgEffect[] = ['wall', 'runes', 'veil', 'ash', 'breath']
18
+
19
+ /** Fresh defaults — also used to reset stale persisted sets on version bump. */
20
+ const BG_INITIAL = {
21
+ effects: BG_DEFAULTS,
22
+ density: 75,
23
+ speed: 0.6,
24
+ opacity: 0.8,
25
+ grain: true,
26
+ } as const
17
27
 
18
28
  interface BgState {
19
29
  effects: BgEffect[]
@@ -29,13 +39,9 @@ interface BgState {
29
39
 
30
40
  export const useUnveilBg = createPersistStore<BgState>(
31
41
  'meycult-unveil-bg-v1',
32
- 1,
42
+ 3,
33
43
  (set) => ({
34
- effects: BG_DEFAULTS,
35
- density: 50,
36
- speed: 0.6,
37
- opacity: 0.8,
38
- grain: true,
44
+ ...BG_INITIAL,
39
45
  toggle: (e) =>
40
46
  set((s) => ({
41
47
  effects: s.effects.includes(e)
@@ -44,9 +50,12 @@ export const useUnveilBg = createPersistStore<BgState>(
44
50
  })),
45
51
  setMany: (effects) => set({ effects }),
46
52
  set: (p) => set(p),
47
- reset: () =>
48
- set({ effects: BG_DEFAULTS, density: 50, speed: 0.6, opacity: 0.8, grain: true }),
53
+ reset: () => set({ ...BG_INITIAL }),
49
54
  }),
55
+ {
56
+ // version bumps reset to fresh defaults (new effects ship enabled)
57
+ migrate: () => ({ ...BG_INITIAL }) as unknown as BgState,
58
+ },
50
59
  )
51
60
 
52
61
  /** Read-only ?bg=veil,runes share param (applied once on mount). */
@@ -8,6 +8,5 @@ export const BG_STYLES = `
8
8
  @keyframes mey-veil-drift{from{transform:translateX(-8vw)}to{transform:translateX(8vw)}}
9
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
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
11
  @media (prefers-reduced-motion:reduce){.mey-veil i:nth-child(2),.mey-breath{animation:none}}
13
12
  `
@@ -10,26 +10,56 @@ import CultEye from '../brand/CultEye'
10
10
  import { CultCountdown } from '../components/CultCountdown'
11
11
  import { UNVEIL_AT, unveilDayLabel, unveilLocalLabel } from '../unveil'
12
12
 
13
- export function UnveilHero() {
13
+ export interface UnveilHeroSizes {
14
+ /** px overrides for live tuning — undefined keeps the class defaults */
15
+ mey?: number
16
+ unveil?: number
17
+ gap?: number
18
+ }
19
+
20
+ export interface UnveilHeroRoutes {
21
+ countdown?: string
22
+ crowdfunding?: string
23
+ }
24
+
25
+ export function UnveilHero({
26
+ sizes,
27
+ routes,
28
+ }: {
29
+ sizes?: UnveilHeroSizes
30
+ /** Composition — hosts override when paths differ. Defaults are the web routes. */
31
+ routes?: UnveilHeroRoutes
32
+ }) {
33
+ const countdownHref = routes?.countdown ?? '/unveil/countdown'
34
+ const crowdfundingHref = routes?.crowdfunding ?? '/#crowdfunding'
14
35
  return (
15
36
  <section className="section section--hero">
16
- <a href="/unveil/countdown" aria-label="Open the full-page countdown" data-testid="unveil-sigil">
37
+ <a href={countdownHref} aria-label="Open the full-page countdown" data-testid="unveil-sigil">
17
38
  <div className="w-[200px] sm:w-[280px]">
18
39
  <CultEye fluid />
19
40
  </div>
20
41
  </a>
21
42
 
22
43
  <div className="mt-2 flex flex-col items-center leading-none">
23
- <span className="text-6xl sm:text-7xl font-bold tracking-wider">
44
+ <span
45
+ className="text-[64px] sm:text-[96px] font-bold tracking-wider"
46
+ style={sizes?.mey !== undefined ? { fontSize: sizes.mey } : undefined}
47
+ >
24
48
  <span className="brand-mey">Mey</span>
25
49
  <span className="brand-cult">Cult</span>
26
50
  </span>
27
- <span className="brand-unveil unveil-glow mt-1 text-5xl sm:text-6xl">Unveil</span>
51
+ <span
52
+ className="brand-unveil unveil-glow mt-[-12px] sm:mt-[-19px] text-[56px] sm:text-[85px]"
53
+ style={{
54
+ ...(sizes?.unveil !== undefined ? { fontSize: sizes.unveil } : undefined),
55
+ ...(sizes?.gap !== undefined ? { marginTop: sizes.gap } : undefined),
56
+ }}
57
+ >Unveil</span>
28
58
  </div>
29
59
  <p className="mt-3 text-sm text-text-muted tracking-wide">
30
60
  {unveilDayLabel(UNVEIL_AT)} SAST · Stellenbosch
31
61
  </p>
32
- <p className="text-xs text-text-muted">{unveilLocalLabel(UNVEIL_AT)} your time</p>
62
+ <p className="text-xs text-text-muted" suppressHydrationWarning>{unveilLocalLabel(UNVEIL_AT)} your time</p>
33
63
 
34
64
  <div className="mt-8 w-full max-w-2xl">
35
65
  <CultCountdown />
@@ -39,13 +69,13 @@ export function UnveilHero() {
39
69
  <div className="mt-8 flex flex-wrap justify-center gap-3">
40
70
  <a
41
71
  className="px-6 py-2.5 rounded-lg bg-accent text-bg font-bold text-sm uppercase tracking-wider"
42
- href="/#crowdfunding"
72
+ href={crowdfundingHref}
43
73
  >
44
74
  Notify me
45
75
  </a>
46
76
  <a
47
77
  className="px-6 py-2.5 rounded-lg border border-line/40 text-sm uppercase tracking-wider text-text"
48
- href="/unveil/countdown"
78
+ href={countdownHref}
49
79
  >
50
80
  Full countdown
51
81
  </a>
@@ -78,7 +108,7 @@ export function WhereWhen({
78
108
  <a href={href} target="_blank" rel="noreferrer" className="block w-full group">
79
109
  <div
80
110
  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)' }}
111
+ style={{ border: '1px solid var(--color-line)', boxShadow: '0 0 40px rgba(255,230,0,0.12)' }}
82
112
  >
83
113
  {/* eslint-disable-next-line @next/next/no-img-element */}
84
114
  <img src={map.src} alt={map.alt} className="w-full h-full object-cover" />
@@ -90,7 +120,7 @@ export function WhereWhen({
90
120
  ) : (
91
121
  <div
92
122
  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)' }}
123
+ style={{ border: '1px solid var(--color-line)', background: 'rgba(0,0,0,0.3)' }}
94
124
  >
95
125
  <div className="w-[64px]">
96
126
  <CultEye fluid />
@@ -138,7 +168,14 @@ export function CardGrid({ cards, cols = 3 }: { cards: UnveilCard[]; cols?: 2 |
138
168
  )
139
169
  }
140
170
 
141
- export function HypeRow({ qrUrl }: { qrUrl: string }) {
171
+ export interface HypeRowRoutes {
172
+ crowdfunding?: string
173
+ whitepaper?: string
174
+ }
175
+
176
+ export function HypeRow({ qrUrl, routes }: { qrUrl: string; routes?: HypeRowRoutes }) {
177
+ const crowdfundingHref = routes?.crowdfunding ?? '/#crowdfunding'
178
+ const whitepaperHref = routes?.whitepaper ?? '/whitepaper'
142
179
  return (
143
180
  <div className="grid grid-cols-1 sm:grid-cols-[2fr_1fr] gap-8 items-center max-w-3xl mx-auto">
144
181
  <div>
@@ -153,10 +190,10 @@ export function HypeRow({ qrUrl }: { qrUrl: string }) {
153
190
  Free to play. Real USDC cash-out. Sweepstakes-legal.
154
191
  </p>
155
192
  <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">
193
+ <a className="px-6 py-2.5 rounded-lg bg-accent text-bg font-bold text-sm uppercase tracking-wider" href={crowdfundingHref}>
157
194
  Get notified
158
195
  </a>
159
- <a className="px-6 py-2.5 rounded-lg border border-line/40 text-sm uppercase tracking-wider text-text" href="/whitepaper">
196
+ <a className="px-6 py-2.5 rounded-lg border border-line/40 text-sm uppercase tracking-wider text-text" href={whitepaperHref}>
160
197
  Read the whitepaper
161
198
  </a>
162
199
  </div>
@@ -181,7 +218,7 @@ export function HypeRow({ qrUrl }: { qrUrl: string }) {
181
218
  )
182
219
  }
183
220
 
184
- export function NotifyPanel() {
221
+ export function NotifyPanel({ crowdfundingHref = '/#crowdfunding' }: { crowdfundingHref?: string }) {
185
222
  return (
186
223
  <div className="landing-card p-8 max-w-lg mx-auto text-center">
187
224
  <p className="text-accent font-bold text-lg mb-2">Be there at 20:00.</p>
@@ -190,7 +227,7 @@ export function NotifyPanel() {
190
227
  </p>
191
228
  <a
192
229
  className="inline-block px-6 py-2.5 rounded-lg bg-accent text-bg font-bold text-sm uppercase tracking-wider"
193
- href="/#crowdfunding"
230
+ href={crowdfundingHref}
194
231
  >
195
232
  Join the list
196
233
  </a>
@@ -2,74 +2,7 @@
2
2
 
3
3
  /** MeyCult Unveil background effects — 4 canvas + 6 CSS layers. */
4
4
  import { useEffect, useRef } from 'react'
5
-
6
- export interface FxProps {
7
- density?: number // 0-100
8
- speed?: number // 0-2 multiplier
9
- opacity?: number // 0-1 master
10
- }
11
-
12
- function reducedMotion(): boolean {
13
- return (
14
- typeof window !== 'undefined' &&
15
- window.matchMedia('(prefers-reduced-motion: reduce)').matches
16
- )
17
- }
18
-
19
- /** Shared rAF canvas loop: DPR cap, resize, hidden-tab + offscreen pause. */
20
- function useCanvas(
21
- ref: React.RefObject<HTMLCanvasElement | null>,
22
- render: (ctx: CanvasRenderingContext2D, w: number, h: number, t: number) => void,
23
- active: boolean,
24
- ) {
25
- useEffect(() => {
26
- if (!active) return
27
- const canvas = ref.current
28
- if (!canvas || reducedMotion()) return
29
- const ctx = canvas.getContext('2d')
30
- if (!ctx) return
31
- let raf = 0
32
- let w = 0
33
- let h = 0
34
- const resize = () => {
35
- const dpr = Math.min(window.devicePixelRatio || 1, 1.5)
36
- const rect = canvas.getBoundingClientRect()
37
- w = Math.max(1, Math.floor(rect.width * dpr))
38
- h = Math.max(1, Math.floor(rect.height * dpr))
39
- canvas.width = w
40
- canvas.height = h
41
- }
42
- resize()
43
- const ro = new ResizeObserver(resize)
44
- const parent = canvas.parentElement
45
- if (parent) ro.observe(parent)
46
- let visible = true
47
- const io = new IntersectionObserver(([e]) => {
48
- visible = e.isIntersecting
49
- if (visible) last = performance.now()
50
- })
51
- io.observe(canvas)
52
- let last = performance.now()
53
- const loop = (t: number) => {
54
- raf = requestAnimationFrame(loop)
55
- if (!visible || document.hidden) {
56
- last = t
57
- return
58
- }
59
- void last
60
- last = t
61
- ctx.clearRect(0, 0, w, h)
62
- render(ctx, w, h, t)
63
- }
64
- raf = requestAnimationFrame(loop)
65
- return () => {
66
- cancelAnimationFrame(raf)
67
- ro.disconnect()
68
- io.disconnect()
69
- }
70
- // eslint-disable-next-line react-hooks/exhaustive-deps
71
- }, [active])
72
- }
5
+ import { useCanvas, reducedMotion, type FxProps } from '@meyvoid/core/backgrounds'
73
6
 
74
7
  /**
75
8
  * Old-Egypt / illuminati glyph ascension — all cult yellow. Authentic
@@ -90,18 +23,34 @@ const GLYPHS = [
90
23
  '◉', // the eye
91
24
  ]
92
25
 
93
- export function RuneCanvas({ density = 50, speed = 0.6, opacity = 0.8 }: FxProps) {
26
+ interface RunePart {
27
+ x: number
28
+ y: number
29
+ vy: number
30
+ tw: number
31
+ size: number
32
+ glyph: string
33
+ }
34
+
35
+ /** Glyph budget scales with viewport area: 150 particles at 1080p. */
36
+ const RUNE_REF_AREA = 1920 * 1080
37
+ const RUNE_REF_COUNT = 150
38
+
39
+ function makeRuneParts(n: number): RunePart[] {
40
+ return Array.from({ length: n }, (_, i) => ({
41
+ x: Math.random(),
42
+ y: Math.random(),
43
+ vy: 0.0002 + Math.random() * 0.0006,
44
+ tw: Math.random() * Math.PI * 2,
45
+ size: 10 + Math.random() * 12,
46
+ glyph: GLYPHS[i % GLYPHS.length],
47
+ }))
48
+ }
49
+
50
+ export function RuneCanvas({ density = 50, speed = 0.6, opacity = 0.8, z = 3 }: FxProps) {
94
51
  const ref = useRef<HTMLCanvasElement>(null)
95
- const parts = useRef(
96
- Array.from({ length: 60 }, (_, i) => ({
97
- x: Math.random(),
98
- y: Math.random(),
99
- vy: 0.0002 + Math.random() * 0.0006,
100
- tw: Math.random() * Math.PI * 2,
101
- size: 10 + Math.random() * 12,
102
- glyph: GLYPHS[i % GLYPHS.length],
103
- })),
104
- )
52
+ const parts = useRef<RunePart[]>([])
53
+ const sized = useRef({ w: 0, h: 0 })
105
54
  useEffect(() => {
106
55
  // ensure the hieroglyph webfont is ready before first glyphs drift in
107
56
  try {
@@ -113,6 +62,20 @@ export function RuneCanvas({ density = 50, speed = 0.6, opacity = 0.8 }: FxProps
113
62
  useCanvas(
114
63
  ref,
115
64
  (ctx, w, h, t) => {
65
+ // rebuild the pool when the viewport bucket changes (CSS px, so DPR
66
+ // never inflates the count) — cheap compare, layout read only on resize
67
+ if (w !== sized.current.w || h !== sized.current.h) {
68
+ sized.current = { w, h }
69
+ const cw = ref.current?.clientWidth || w
70
+ const ch = ref.current?.clientHeight || h
71
+ const want = Math.max(
72
+ 40,
73
+ Math.min(300, Math.round((RUNE_REF_COUNT * cw * ch) / RUNE_REF_AREA)),
74
+ )
75
+ if (Math.abs(want - parts.current.length) > 8) {
76
+ parts.current = makeRuneParts(want)
77
+ }
78
+ }
116
79
  const n = Math.floor((density / 100) * parts.current.length)
117
80
  for (let i = 0; i < n; i++) {
118
81
  const p = parts.current[i]
@@ -124,6 +87,22 @@ export function RuneCanvas({ density = 50, speed = 0.6, opacity = 0.8 }: FxProps
124
87
  }
125
88
  const x = (p.x + Math.sin(p.tw) * 0.008) * w
126
89
  const y = p.y * h
90
+ // mini stone rect: the glyph rides a tiny carved block, same
91
+ // treatment as the wall at glyph scale
92
+ const rw = p.size * 1.15
93
+ const rh = p.size * 1.15
94
+ const rx = x - rw / 2
95
+ const ry = y - rh / 2 - p.size * 0.08
96
+ ctx.globalAlpha = (0.5 + 0.5 * Math.abs(Math.sin(p.tw))) * opacity
97
+ ctx.fillStyle = '#141003'
98
+ ctx.fillRect(rx, ry, rw, rh)
99
+ ctx.fillStyle = 'rgba(255,230,0,0.10)'
100
+ ctx.fillRect(rx, ry, rw, 1)
101
+ ctx.fillStyle = 'rgba(0,0,0,0.6)'
102
+ ctx.fillRect(rx, ry + rh - 1, rw, 1)
103
+ ctx.strokeStyle = 'rgba(0,0,0,0.7)'
104
+ ctx.lineWidth = 1
105
+ ctx.strokeRect(rx + 0.5, ry + 0.5, rw - 1, rh - 1)
127
106
  ctx.globalAlpha = (0.15 + 0.5 * Math.abs(Math.sin(p.tw))) * opacity
128
107
  ctx.font = `${p.size}px ${GLYPH_FONT}`
129
108
  ctx.fillStyle = '#FFE600'
@@ -134,10 +113,10 @@ export function RuneCanvas({ density = 50, speed = 0.6, opacity = 0.8 }: FxProps
134
113
  },
135
114
  true,
136
115
  )
137
- return <canvas ref={ref} className="mey-bg-canvas" aria-hidden="true" />
116
+ return <canvas ref={ref} className="mey-bg-canvas" style={{ zIndex: z }} aria-hidden="true" />
138
117
  }
139
118
 
140
- export function AshCanvas({ density = 50, speed = 0.5, opacity = 0.8 }: FxProps) {
119
+ export function AshCanvas({ density = 50, speed = 0.5, opacity = 0.8, z = 2 }: FxProps) {
141
120
  const ref = useRef<HTMLCanvasElement>(null)
142
121
  const parts = useRef(
143
122
  Array.from({ length: 220 }, () => ({
@@ -182,12 +161,129 @@ export function AshCanvas({ density = 50, speed = 0.5, opacity = 0.8 }: FxProps)
182
161
  },
183
162
  true,
184
163
  )
185
- return <canvas ref={ref} className="mey-bg-canvas" aria-hidden="true" />
164
+ return <canvas ref={ref} className="mey-bg-canvas" style={{ zIndex: z }} aria-hidden="true" />
165
+ }
166
+
167
+ /**
168
+ * Pyramid wall — baked dark-stone texture. Static: rendered once per resize
169
+ * to an offscreen canvas, then blitted. Running-bond courses, top-lit
170
+ * bevels, per-block tonal jitter, speckle noise, baked vignette.
171
+ */
172
+ function mulberry32(seed: number): () => number {
173
+ let a = seed >>> 0
174
+ return () => {
175
+ a |= 0
176
+ a = (a + 0x6d2b79f5) | 0
177
+ let t = Math.imul(a ^ (a >>> 15), 1 | a)
178
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
179
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
180
+ }
181
+ }
182
+
183
+ export function WallCanvas({ opacity = 0.9, z = 0 }: FxProps) {
184
+ const ref = useRef<HTMLCanvasElement>(null)
185
+ useEffect(() => {
186
+ const canvas = ref.current
187
+ if (!canvas || reducedMotion()) return
188
+ const ctx = canvas.getContext('2d')
189
+ if (!ctx) return
190
+ const bake = () => {
191
+ const dpr = Math.min(window.devicePixelRatio || 1, 1.5)
192
+ const rect = canvas.getBoundingClientRect()
193
+ const w = Math.max(1, Math.floor(rect.width * dpr))
194
+ const h = Math.max(1, Math.floor(rect.height * dpr))
195
+ canvas.width = w
196
+ canvas.height = h
197
+ const off = document.createElement('canvas')
198
+ off.width = w
199
+ off.height = h
200
+ const c = off.getContext('2d')
201
+ if (!c) return
202
+ const rand = mulberry32((w * 73856093) ^ (h * 19349663))
203
+ // base: near-black warm basalt
204
+ c.fillStyle = '#0e0b06'
205
+ c.fillRect(0, 0, w, h)
206
+ // courses of small blocks, running bond — fixed 88 CSS px course
207
+ // height (not viewport- or page-derived), so bricks read the same
208
+ // size on every display. Block width capped at course height:
209
+ // more blocks on wide screens, never slabs.
210
+ const bh = 88 * dpr
211
+ const rowCount = Math.ceil(h / bh)
212
+ const MAX_BLOCK_RATIO = 1.0
213
+ for (let r = 0; r < rowCount; r++) {
214
+ const y = r * bh
215
+ const rawBlocks = 5 + ((r * 7) % 4)
216
+ const bw = Math.min(w / rawBlocks, bh * MAX_BLOCK_RATIO)
217
+ const span = bw * Math.ceil(w / bw)
218
+ const x0 = (w - span) / 2
219
+ const bond = r % 2 === 0 ? 0 : -bw / 2
220
+ for (let x = x0 + bond; x < w; x += bw) {
221
+ const tone = (rand() - 0.5) * 14
222
+ const base = 14 + tone
223
+ // block body: subtle vertical falloff, lighter crown
224
+ const g = c.createLinearGradient(0, y, 0, y + bh)
225
+ g.addColorStop(0, `rgb(${base + 9},${base + 1},${Math.max(0, base - 9)})`)
226
+ g.addColorStop(0.12, `rgb(${base + 4},${base - 2},${Math.max(0, base - 10)})`)
227
+ g.addColorStop(1, `rgb(${Math.max(0, base - 6)},${Math.max(0, base - 9)},${Math.max(0, base - 13)})`)
228
+ c.fillStyle = g
229
+ c.fillRect(x + 1.5, y + 1.5, bw - 3, bh - 3)
230
+ // top bevel catches the cult light
231
+ c.fillStyle = 'rgba(255,230,0,0.055)'
232
+ c.fillRect(x + 1.5, y + 1.5, bw - 3, Math.max(1, dpr))
233
+ c.fillStyle = 'rgba(255,255,255,0.03)'
234
+ c.fillRect(x + 1.5, y + 1.5 + dpr, bw - 3, Math.max(1, dpr * 0.5))
235
+ // foot shadow
236
+ c.fillStyle = 'rgba(0,0,0,0.55)'
237
+ c.fillRect(x + 1.5, y + bh - 1.5 - dpr * 1.5, bw - 3, dpr * 1.5)
238
+ // occasional stained block
239
+ if (rand() < 0.18) {
240
+ c.fillStyle = `rgba(0,0,0,${0.12 + rand() * 0.18})`
241
+ c.fillRect(x + 1.5, y + 1.5, bw - 3, bh - 3)
242
+ }
243
+ }
244
+ }
245
+ // mortar shadow grid
246
+ c.fillStyle = 'rgba(0,0,0,0.8)'
247
+ for (let r = 0; r <= rowCount; r++) c.fillRect(0, r * bh - 1, w, 2)
248
+ // speckle noise
249
+ const specks = Math.floor((w * h) / 900)
250
+ for (let i = 0; i < specks; i++) {
251
+ const sx = rand() * w
252
+ const sy = rand() * h
253
+ c.fillStyle = rand() < 0.12 ? 'rgba(255,230,0,0.05)' : 'rgba(0,0,0,0.35)'
254
+ c.fillRect(sx, sy, dpr, dpr)
255
+ }
256
+ // baked vignette
257
+ const v = c.createRadialGradient(
258
+ w / 2, h / 2, Math.min(w, h) * 0.35,
259
+ w / 2, h / 2, Math.max(w, h) * 0.75,
260
+ )
261
+ v.addColorStop(0, 'rgba(0,0,0,0)')
262
+ v.addColorStop(1, 'rgba(0,0,0,0.55)')
263
+ c.fillStyle = v
264
+ c.fillRect(0, 0, w, h)
265
+ ctx.clearRect(0, 0, w, h)
266
+ ctx.drawImage(off, 0, 0)
267
+ }
268
+ bake()
269
+ const ro = new ResizeObserver(bake)
270
+ if (canvas.parentElement) ro.observe(canvas.parentElement)
271
+ return () => ro.disconnect()
272
+ // eslint-disable-next-line react-hooks/exhaustive-deps
273
+ }, [])
274
+ return (
275
+ <canvas
276
+ ref={ref}
277
+ className="mey-bg-canvas"
278
+ style={{ opacity, zIndex: z }}
279
+ aria-hidden="true"
280
+ />
281
+ )
186
282
  }
187
283
 
188
- export function VeilFog({ opacity = 0.7 }: FxProps) {
284
+ export function VeilFog({ opacity = 0.7, z = 1 }: FxProps) {
189
285
  return (
190
- <div className="mey-veil" style={{ opacity }} aria-hidden="true">
286
+ <div className="mey-veil" style={{ opacity, zIndex: z }} aria-hidden="true">
191
287
  <i />
192
288
  <i />
193
289
  </div>
@@ -195,6 +291,6 @@ export function VeilFog({ opacity = 0.7 }: FxProps) {
195
291
  }
196
292
 
197
293
 
198
- export function BreathVignette() {
199
- return <div className="mey-breath" aria-hidden="true" />
294
+ export function BreathVignette({ z = 4 }: FxProps) {
295
+ return <div className="mey-breath" style={{ zIndex: z }} aria-hidden="true" />
200
296
  }
package/src/unveil.ts CHANGED
@@ -55,24 +55,5 @@ export function unveilLocalLabel(at: number = UNVEIL_AT): string {
55
55
  }).format(new Date(at))
56
56
  }
57
57
 
58
- export interface CountdownParts {
59
- days: number
60
- hours: number
61
- minutes: number
62
- seconds: number
63
- done: boolean
64
- }
65
-
66
- export function diffParts(target: number, now: number = Date.now()): CountdownParts {
67
- const delta = target - now
68
- if (delta <= 0) return { days: 0, hours: 0, minutes: 0, seconds: 0, done: true }
69
- return {
70
- days: Math.floor(delta / 86_400_000),
71
- hours: Math.floor(delta / 3_600_000) % 24,
72
- minutes: Math.floor(delta / 60_000) % 60,
73
- seconds: Math.floor(delta / 1_000) % 60,
74
- done: false,
75
- }
76
- }
77
-
78
- export const pad2 = (n: number) => String(n).padStart(2, '0')
58
+ // Pure time math lives in @meyvoid/core — re-exported here for compat.
59
+ export { diffParts, pad2, type CountdownParts } from '@meyvoid/core'
package/src/auth/Gate.tsx DELETED
@@ -1,14 +0,0 @@
1
- import type { RequireAuthProps } from './types'
2
-
3
- /** Framework-free auth gate — host passes user/loading from its own auth wiring. */
4
- export function RequireAuth({
5
- user,
6
- loading,
7
- loadingFallback = null,
8
- loginFallback = null,
9
- children,
10
- }: RequireAuthProps) {
11
- if (loading) return <>{loadingFallback}</>
12
- if (!user) return <>{loginFallback}</>
13
- return <>{children}</>
14
- }
package/src/auth/types.ts DELETED
@@ -1,12 +0,0 @@
1
- export interface SessionProfile {
2
- id: string
3
- email?: string | null
4
- }
5
-
6
- export interface RequireAuthProps {
7
- user: SessionProfile | null
8
- loading: boolean
9
- loadingFallback?: React.ReactNode
10
- loginFallback?: React.ReactNode
11
- children: React.ReactNode
12
- }
@@ -1,8 +0,0 @@
1
- export default function GodEmperorBrand() {
2
- return (
3
- <span style={{ fontFamily: 'var(--font-logo)', fontWeight: 700, textTransform: 'none' }}>
4
- <span style={{ color: '#ffffff' }}>God</span>
5
- <span style={{ color: 'var(--color-premium-400)' }}>Emperor</span>
6
- </span>
7
- )
8
- }
package/src/cn.ts DELETED
@@ -1,6 +0,0 @@
1
- import { clsx, type ClassValue } from 'clsx'
2
- import { twMerge } from 'tailwind-merge'
3
-
4
- export function cn(...inputs: ClassValue[]): string {
5
- return twMerge(clsx(...inputs))
6
- }
package/src/lib/images.ts DELETED
@@ -1,10 +0,0 @@
1
- export function isAbsoluteUrl(src: string): boolean {
2
- return /^https?:\/\//.test(src) || src.startsWith('data:') || src.startsWith('blob:')
3
- }
4
-
5
- /** Resolve a product/asset src against an optional storage base. */
6
- export function resolveImage(src: string, base?: string): string {
7
- if (!src) return src
8
- if (isAbsoluteUrl(src) || !base) return src
9
- return `${base.replace(/\/$/, '')}/${src.replace(/^\//, '')}`
10
- }
package/src/lib/money.ts DELETED
@@ -1,12 +0,0 @@
1
- const SYMBOLS: Record<string, string> = {
2
- USD: '$',
3
- ZAR: 'R',
4
- EUR: '€',
5
- GBP: '£',
6
- }
7
-
8
- /** Format integer minor units (cents) — e.g. formatPrice(1999) → '$19.99'. */
9
- export function formatPrice(cents: number, currency = 'USD'): string {
10
- const symbol = SYMBOLS[currency] ?? `${currency} `
11
- return `${symbol}${(cents / 100).toFixed(2)}`
12
- }
package/src/lib/store.ts DELETED
@@ -1,19 +0,0 @@
1
- import { create, type StateCreator } from 'zustand'
2
- import { persist, type PersistOptions } from 'zustand/middleware'
3
-
4
- /** Persisted zustand store with a stable shape — mirrors the meybuddy store pattern. */
5
- export function createPersistStore<T>(
6
- name: string,
7
- version: number,
8
- initializer: StateCreator<T, [], []>,
9
- options?: Partial<PersistOptions<T, T>>,
10
- ) {
11
- return create<T>()(
12
- persist(initializer, {
13
- name,
14
- version,
15
- skipHydration: true,
16
- ...options,
17
- }),
18
- )
19
- }
@@ -1,13 +0,0 @@
1
- import type { ComponentType, AnchorHTMLAttributes } from 'react'
2
-
3
- export interface NavLink {
4
- label: string
5
- href: string
6
- external?: boolean
7
- branded?: boolean
8
- }
9
-
10
- /** Injectable link — host passes next/link (web) or react-router Link (app). Defaults to <a>. */
11
- export type LinkComponent = ComponentType<
12
- AnchorHTMLAttributes<HTMLAnchorElement> & { href: string }
13
- >