@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.
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 MeyCult
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # @meycult/core
2
+
3
+ Shared core for the MeyCult monorepo — single source of truth for theme, brand,
4
+ Supabase/auth factories, and lib helpers. **Source-only, no build step.**
5
+ Consumers compile it in-process (`transpilePackages` in web, direct Vite import in app).
6
+
7
+ ## Consume
8
+
9
+ ```ts
10
+ import { Sigil, Logo, BrandName, LaunchButton, CornerGlow } from '@meycult/core'
11
+ import { COLORS } from '@meycult/core/tokens'
12
+ import { cn } from '@meycult/core/cn'
13
+ ```
14
+
15
+ ```css
16
+ /* app entry CSS (web/src/app/globals.css, app/src/index.css) */
17
+ @import "@meycult/core/styles/theme.css";
18
+ @source "../../../core/src"; /* Tailwind v4 class detection — path relative to the CSS file */
19
+ ```
20
+
21
+ ```ts
22
+ // next.config.ts (web only)
23
+ transpilePackages: ['@meycult/core']
24
+ ```
25
+
26
+ ## What's inside
27
+
28
+ | Path | Owns |
29
+ |---|---|
30
+ | `styles/theme.css` | Full yellow theme: `@theme` tokens, base, utilities, `.brand-cult`, corner glows, sections, cards. Self-hosts New Rocker via `@font-face` (`--font-cult`) — identical in Next.js and Vite |
31
+ | `fonts/` | `NewRocker-Regular.woff2` (self-hosted, no Google Fonts request) |
32
+ | `tokens.ts` | TS mirror (`COLORS`, `GLOW`, `FONTS`) for canvas/tests |
33
+ | `brand/` | `Sigil`, `Logo`, `BrandName`, `TokenIcons` (`basePath` prop, default `/products`), `LaunchButton`, `CornerGlow`, `GodEmperorBrand` |
34
+ | `ui/` | `card`, `shimmer-button`, `animated-shiny-text`, `Callout` (react/cn/lucide only — no motion/cva/radix) |
35
+ | `shell/types.ts` | `NavLink`, injectable `LinkComponent` (next/link vs react-router) |
36
+ | `supabase/` | `browser` (chunked cross-subdomain cookies), `server` + `proxy` + `callback` (Next.js) |
37
+ | `auth/` | `SessionProfile`, framework-free `RequireAuth` gate |
38
+ | `lib/` | `formatPrice(cents, currency)`, `isAbsoluteUrl`/`resolveImage`, `createPersistStore` (zustand) |
39
+
40
+ ## Conventions (match @meygod/core, @meybuddy/core)
41
+
42
+ - `main`/`types` point at `./src/index.ts` — never build, never publish.
43
+ - Every new module must be re-exported from `src/index.ts`.
44
+ - Brand PNGs (`influence.png`, `fate.png`) stay in each host's `public/products/`.
45
+ - `pnpm --filter @meycult/core typecheck`
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@meycult/core",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "MeyCult shared core — yellow theme, brand, supabase/auth factories, lib. Source-only, no build.",
6
+ "license": "ISC",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://gitlab.com/meyvoid/meycult/core-ts.git"
10
+ },
11
+ "publishConfig": {
12
+ "registry": "https://registry.npmjs.org",
13
+ "access": "public",
14
+ "provenance": true
15
+ },
16
+ "sideEffects": [
17
+ "*.css"
18
+ ],
19
+ "main": "./src/index.ts",
20
+ "types": "./src/index.ts",
21
+ "files": [
22
+ "src"
23
+ ],
24
+ "exports": {
25
+ ".": "./src/index.ts",
26
+ "./tokens": "./src/tokens.ts",
27
+ "./cn": "./src/cn.ts",
28
+ "./brand": "./src/brand.ts",
29
+ "./supabase/server": "./src/supabase/server.ts",
30
+ "./supabase/proxy": "./src/supabase/proxy.ts",
31
+ "./supabase/callback": "./src/supabase/callback.ts",
32
+ "./styles/theme.css": "./src/styles/theme.css"
33
+ },
34
+ "scripts": {
35
+ "typecheck": "tsc --noEmit",
36
+ "release": "bash scripts/release.sh"
37
+ },
38
+ "dependencies": {
39
+ "@meyvoid/core": "^0.1.0",
40
+ "clsx": "^2.1.1",
41
+ "tailwind-merge": "^3.6.0",
42
+ "zustand": "^5.0.14"
43
+ },
44
+ "peerDependencies": {
45
+ "@supabase/ssr": "^0.5.0",
46
+ "@supabase/supabase-js": "^2.110.0",
47
+ "lucide-react": "^1.23.0",
48
+ "react": "19.2.8",
49
+ "react-dom": "19.2.8"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "@supabase/ssr": {
53
+ "optional": true
54
+ },
55
+ "lucide-react": {
56
+ "optional": true
57
+ }
58
+ },
59
+ "engines": {
60
+ "node": ">=20"
61
+ },
62
+ "devDependencies": {
63
+ "@supabase/ssr": "^0.5.0",
64
+ "@supabase/supabase-js": "^2.110.0",
65
+ "@types/node": "^24.13.2",
66
+ "@types/qrcode": "^1.5.6",
67
+ "@types/react": "^19.2.3",
68
+ "@types/react-dom": "^19.2.3",
69
+ "next": "^15.4.0",
70
+ "typescript": "~6.0.2"
71
+ }
72
+ }
@@ -0,0 +1,14 @@
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
+ }
@@ -0,0 +1,12 @@
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
+ }
@@ -0,0 +1,12 @@
1
+ export default function BrandName({ className }: { className?: string }) {
2
+ return (
3
+ <span className={className} style={{ fontFamily: 'var(--font-logo)', fontWeight: 700, textTransform: 'none' }}>
4
+ <span className="brand-mey" style={{
5
+ textShadow: '0 0 40px rgba(255,255,255,0.35), 0 0 80px rgba(255,255,255,0.15), 0 0 120px rgba(255,255,255,0.08)',
6
+ }}>Mey</span>
7
+ <span className="brand-cult" style={{
8
+ textShadow: '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)',
9
+ }}>Cult</span>
10
+ </span>
11
+ )
12
+ }
@@ -0,0 +1,14 @@
1
+ 'use client'
2
+
3
+ export default function CornerGlow() {
4
+ return (
5
+ <>
6
+ <div className="corner-glow corner-glow--tl" />
7
+ <div className="corner-glow corner-glow--tr" />
8
+ <div className="corner-glow corner-glow--bl" />
9
+ <div className="corner-glow corner-glow--br" />
10
+ <div className="corner-glow corner-glow--ct" />
11
+ <div className="corner-glow corner-glow--cb" />
12
+ </>
13
+ )
14
+ }
@@ -0,0 +1,14 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * CultEye — MeyCult hero eye. Thin preset over MeyEye (variant="cult");
5
+ * tracking math, blink, and positions come from @meyvoid/core geometry.
6
+ */
7
+ import { MeyEye, type MeyEyeProps } from '@meyvoid/core/eye'
8
+
9
+ export default function CultEye(props: Omit<MeyEyeProps, 'variant'>) {
10
+ return <MeyEye variant="cult" label="MeyCult watchful eye" {...props} />
11
+ }
12
+
13
+ export { GazeProvider, useGaze } from '@meyvoid/core/eye'
14
+ export type { EyeVariant } from '@meyvoid/core/eye'
@@ -0,0 +1,8 @@
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
+ }
@@ -0,0 +1,27 @@
1
+ interface LaunchButtonProps {
2
+ label?: string
3
+ href?: string
4
+ className?: string
5
+ small?: boolean
6
+ }
7
+
8
+ export default function LaunchButton({ label = 'Launch', href = 'https://app.meycult.com', className = '', small }: LaunchButtonProps) {
9
+ return (
10
+ <span className={`inline-block rounded-lg p-[2px] hover:scale-[1.02] transition-all ${className}`}
11
+ style={{ background: 'linear-gradient(135deg, #FFE600, #FFF7A1)' }}>
12
+ <a
13
+ href={href}
14
+ className={`block font-bold tracking-wider rounded-[6px] hover:opacity-90 transition-all whitespace-nowrap ${small ? 'px-3 py-1.5 text-[10px]' : 'px-8 py-2.5 text-sm'}`}
15
+ style={{
16
+ background: 'linear-gradient(135deg, #0B0A02, #4A3D00)',
17
+ color: '#ffffff',
18
+ boxShadow: '0 0 80px rgba(255,230,0,0.08), 0 0 120px rgba(255,230,0,0.05)',
19
+ }}
20
+ >
21
+ <span style={{ fontFamily: 'var(--font-logo)', fontWeight: 700 }}>{label} </span>
22
+ <span className="brand-mey" style={{ textTransform: 'none', color: '#ffffff' }}>Mey</span>
23
+ <span className="brand-cult" style={{ fontWeight: 700 }}>Cult</span>
24
+ </a>
25
+ </span>
26
+ )
27
+ }
@@ -0,0 +1,24 @@
1
+ import Sigil from './Sigil'
2
+
3
+ export default function Logo({ size = 24, className }: { size?: number; className?: string }) {
4
+ return (
5
+ <a href="/" className={`flex items-center gap-3 ${className || ''}`}>
6
+ <Sigil size={size} />
7
+ <div>
8
+ <div
9
+ className="text-lg font-bold tracking-wider leading-none"
10
+ style={{ fontFamily: 'var(--font-logo)' }}
11
+ >
12
+ <span className="brand-mey">Mey</span>
13
+ <span className="brand-cult" style={{ textShadow: '0 0 12px rgba(255,255,255,0.5), 0 0 30px rgba(255,255,255,0.2)' }}>Cult</span>
14
+ </div>
15
+ <div
16
+ className="text-[10px] uppercase tracking-widest mt-0.5"
17
+ style={{ color: 'var(--color-text-muted)', fontFamily: 'var(--font-logo)' }}
18
+ >
19
+ Wager &amp; Join
20
+ </div>
21
+ </div>
22
+ </a>
23
+ )
24
+ }
@@ -0,0 +1,28 @@
1
+ export default function Sigil({ size = 24 }: { size?: number }) {
2
+ const s = size * 1.5
3
+
4
+ return (
5
+ <svg
6
+ width={s}
7
+ viewBox="0 0 200.56923 175.72475"
8
+ style={{ filter: 'drop-shadow(var(--glow))' }}
9
+ >
10
+ <g transform="translate(-341.95552,-53.816747)">
11
+ <g transform="translate(15.212189,33.321951)" style={{ fill: 'var(--color-accent)' }}>
12
+ <path
13
+ style={{ fill: 'var(--color-accent)', fillOpacity: 1 }}
14
+ d="m 426.49541,20.513477 c -2.53292,0.177108 -4.81223,1.601364 -6.0818,3.800284 l -52.93268,91.681229 -39.703,68.76789 c -2.93967,5.09188 0.73505,11.45661 6.61458,11.45667 h 105.86485 79.40601 c 5.87953,-6e-5 9.55425,-6.36479 6.61458,-11.45667 l -52.93217,-91.681223 -39.703,-68.767896 c -1.46009,-2.528918 -4.23433,-4.003991 -7.14737,-3.800284 z"
15
+ />
16
+ </g>
17
+ <path
18
+ style={{ fill: '#ffffff', fillOpacity: 1 }}
19
+ d="m 442.44151,112.59785 c 10.89456,-0.0294 21.77374,0.33291 24.9048,1.10024 18.16066,4.45082 35.71071,13.53969 47.98578,24.85243 8.87021,8.17481 15.22133,14.18661 16.21657,15.34937 2.27217,2.65448 3.21391,6.04251 2.29265,8.24739 -0.487,1.16559 -8.37889,9.64325 -17.45502,18.75132 -6.49799,6.52082 -11.77836,9.84655 -23.09636,14.5445 -2.43619,1.01128 -8.55868,3.59204 -13.60505,5.73526 -5.04636,2.14322 -10.71996,4.40195 -12.60866,5.0198 l -13.80921,3.00538 c -3.83618,0.48165 -13.15749,0.64819 -17.24367,0.30803 -7.65437,-0.63718 -17.39937,-2.6625 -24.94182,-5.1824 -14.40439,-4.81246 -26.96098,-11.29515 -36.43673,-18.81095 -2.63565,-2.09048 -13.55541,-12.206 -19.18402,-17.77121 -2.76668,-2.73554 -4.34874,-5.49272 -4.61149,-8.03873 -0.231,-2.2383 0.26498,-3.06562 3.73616,-6.23205 1.59134,-1.45165 4.70534,-4.84775 6.91952,-7.54642 6.74191,-8.21714 9.10658,-10.11388 20.73231,-16.62942 18.29568,-10.25362 23.48708,-12.53282 35.20639,-15.45595 3.19292,-0.79635 14.10326,-1.21717 24.99785,-1.24659 z"
20
+ />
21
+ <path
22
+ style={{ fill: 'var(--color-bg)' }}
23
+ d="m 442.49623,120.3747 c -22.51598,-4e-5 -40.76826,18.25286 -40.76822,40.76884 -10e-6,22.51597 18.25225,40.76887 40.76822,40.76883 22.51558,-5.3e-4 40.76824,-18.25326 40.76823,-40.76883 3e-5,-22.51557 -18.25265,-40.76832 -40.76823,-40.76884 z"
24
+ />
25
+ </g>
26
+ </svg>
27
+ )
28
+ }
@@ -0,0 +1,48 @@
1
+ export interface TokenIconProps {
2
+ size?: number
3
+ /** Asset base — host's public dir mount. Defaults to '/products'. */
4
+ basePath?: string
5
+ className?: string
6
+ }
7
+
8
+ export function InfluenceIcon({ size = 14, basePath = '/products', className }: TokenIconProps) {
9
+ return (
10
+ <img
11
+ src={`${basePath}/influence.png`}
12
+ alt=""
13
+ className={className ? `inline-block align-middle ${className}` : 'inline-block align-middle'}
14
+ style={{ width: size, height: size }}
15
+ />
16
+ )
17
+ }
18
+
19
+ export function FateIcon({ size = 14, basePath = '/products', className }: TokenIconProps) {
20
+ return (
21
+ <img
22
+ src={`${basePath}/fate.png`}
23
+ alt=""
24
+ className={className ? `inline-block align-middle ${className}` : 'inline-block align-middle'}
25
+ style={{ width: size, height: size }}
26
+ />
27
+ )
28
+ }
29
+
30
+ export function InfluenceText({ size = 13, basePath = '/products' }: TokenIconProps) {
31
+ return (
32
+ <span style={{ fontFamily: 'var(--font-logo)', fontWeight: 700, whiteSpace: 'nowrap' }}>
33
+ <InfluenceIcon size={size} basePath={basePath} />{' '}
34
+ <span className="brand-mey">Mey</span>
35
+ <span style={{ color: 'var(--color-accent)' }}>Influence</span>
36
+ </span>
37
+ )
38
+ }
39
+
40
+ export function FateText({ size = 13, basePath = '/products' }: TokenIconProps) {
41
+ return (
42
+ <span style={{ fontFamily: 'var(--font-logo)', fontWeight: 700, whiteSpace: 'nowrap' }}>
43
+ <FateIcon size={size} basePath={basePath} />{' '}
44
+ <span className="brand-mey">Mey</span>
45
+ <span style={{ color: 'var(--color-accent)' }}>Fate</span>
46
+ </span>
47
+ )
48
+ }
package/src/brand.ts ADDED
@@ -0,0 +1,7 @@
1
+ export const BRAND = {
2
+ name: 'MeyCult',
3
+ first: 'Mey',
4
+ second: 'Cult',
5
+ tagline: 'Wager & Join',
6
+ description: 'Where Foresight Becomes Fortune. Gamified prediction market on Base L2.',
7
+ } as const
package/src/cn.ts ADDED
@@ -0,0 +1,6 @@
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
+ }
@@ -0,0 +1,119 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * CultCountdown — ticking DD:HH:MM:SS to UNVEIL_AT.
5
+ * SSR-safe (skeleton until mount, no hydration mismatch), screen-reader
6
+ * friendly (role=timer stays silent; separate polite status per minute).
7
+ */
8
+ import { useEffect, useState, type CSSProperties } from 'react'
9
+ import { angelRuns } from '@meyvoid/core/angel'
10
+ import { cn } from '../cn'
11
+ import {
12
+ UNVEIL_AT,
13
+ diffParts,
14
+ unveilDayLabel,
15
+ pad2,
16
+ type CountdownParts,
17
+ } from '../unveil'
18
+
19
+ export interface CultCountdownProps {
20
+ target?: number
21
+ className?: string
22
+ doneLabel?: string
23
+ }
24
+
25
+ export function CultCountdown({
26
+ target,
27
+ className,
28
+ doneLabel = 'THE VEIL LIFTS',
29
+ }: CultCountdownProps) {
30
+ const resolved = target ?? UNVEIL_AT
31
+ const [parts, setParts] = useState<CountdownParts | null>(null)
32
+
33
+ useEffect(() => {
34
+ setParts(diffParts(resolved))
35
+ const id = setInterval(() => setParts(diffParts(resolved)), 1000)
36
+ const onVis = () => {
37
+ if (!document.hidden) setParts(diffParts(resolved))
38
+ }
39
+ document.addEventListener('visibilitychange', onVis)
40
+ return () => {
41
+ clearInterval(id)
42
+ document.removeEventListener('visibilitychange', onVis)
43
+ }
44
+ }, [resolved])
45
+
46
+ const [srMinute, setSrMinute] = useState('')
47
+ const day = parts?.days ?? -1
48
+ const hour = parts?.hours ?? -1
49
+ const minute = parts?.minutes ?? -1
50
+ useEffect(() => {
51
+ if (!parts || parts.done) return
52
+ setSrMinute(
53
+ `${parts.days} days, ${parts.hours} hours, ${parts.minutes} minutes until the Unveil`,
54
+ )
55
+ // eslint-disable-next-line react-hooks/exhaustive-deps
56
+ }, [day, hour, minute])
57
+
58
+ if (!parts) {
59
+ return (
60
+ <div aria-hidden="true" className={cn('animate-pulse', className)}>
61
+ <div className="text-5xl tracking-widest text-accent/40" style={{ fontFamily: 'var(--font-cult)' }}>00:00:00:00</div>
62
+ </div>
63
+ )
64
+ }
65
+
66
+ if (parts.done) {
67
+ return (
68
+ <div role="alert" className={cn('text-center', className)}>
69
+ <p className="text-5xl tracking-widest text-accent" style={{ fontFamily: 'var(--font-cult)' }}>{doneLabel}</p>
70
+ </div>
71
+ )
72
+ }
73
+
74
+ const cells: Array<[string, string]> = [
75
+ [pad2(parts.days), 'days'],
76
+ [pad2(parts.hours), 'hrs'],
77
+ [pad2(parts.minutes), 'min'],
78
+ [pad2(parts.seconds), 'sec'],
79
+ ]
80
+ // angel numbers glow yellow; everything else stays white
81
+ const angels = angelRuns([parts.days, parts.hours, parts.minutes, parts.seconds])
82
+ const digitStyle = (angelic: boolean): CSSProperties =>
83
+ angelic
84
+ ? {
85
+ fontFamily: 'var(--font-cult)',
86
+ color: 'var(--color-accent)',
87
+ textShadow: '0 0 24px rgba(255,230,0,0.45)',
88
+ }
89
+ : { fontFamily: 'var(--font-cult)', color: '#ffffff' }
90
+
91
+ return (
92
+ <div className={cn('text-center', className)}>
93
+ <div
94
+ role="timer"
95
+ aria-label={`Time until MeyCult Unveil, ${unveilDayLabel(resolved)}`}
96
+ aria-live="off"
97
+ aria-atomic="true"
98
+ className="flex items-start justify-center gap-2 sm:gap-4"
99
+ >
100
+ {cells.map(([v, l], i) => (
101
+ <div key={l} className="flex items-start gap-2 sm:gap-4">
102
+ <div>
103
+ <div aria-hidden="true" className="text-5xl sm:text-7xl font-bold tracking-wider" style={digitStyle(angels[i])}>
104
+ {v}
105
+ </div>
106
+ <div aria-hidden="true" className="ui-label text-text-muted mt-1">{l}</div>
107
+ </div>
108
+ {i < cells.length - 1 && (
109
+ <div aria-hidden="true" className="text-5xl sm:text-7xl text-accent/40" style={{ fontFamily: 'var(--font-cult)' }}>:</div>
110
+ )}
111
+ </div>
112
+ ))}
113
+ </div>
114
+ <span role="status" aria-live="polite" className="sr-only">
115
+ {srMinute}
116
+ </span>
117
+ </div>
118
+ )
119
+ }
package/src/index.ts ADDED
@@ -0,0 +1,61 @@
1
+ export { cn } from './cn'
2
+ export { COLORS, GLOW, FONTS } from './tokens'
3
+ export { BRAND } from './brand'
4
+
5
+ export { default as Sigil } from './brand/Sigil'
6
+ export { default as CultEye, GazeProvider, useGaze } from './brand/CultEye'
7
+ export { default as Logo } from './brand/Logo'
8
+ export { default as BrandName } from './brand/BrandName'
9
+ export {
10
+ InfluenceIcon,
11
+ FateIcon,
12
+ InfluenceText,
13
+ FateText,
14
+ } from './brand/TokenIcons'
15
+ export { default as LaunchButton } from './brand/LaunchButton'
16
+ export { default as CornerGlow } from './brand/CornerGlow'
17
+ export { default as GodEmperorBrand } from './brand/GodEmperor'
18
+
19
+ export { Card } from './ui/card'
20
+ export { CultCountdown } from './components/CultCountdown'
21
+ export {
22
+ UNVEIL_AT,
23
+ UNVEIL_TZ,
24
+ LIVE_WINDOW_MS,
25
+ unveilPhase,
26
+ unveilDayLabel,
27
+ unveilLocalLabel,
28
+ diffParts,
29
+ } from './unveil'
30
+ export type { UnveilPhase, CountdownParts } from './unveil'
31
+ export { LaunchStage } from './unveil/LaunchStage'
32
+ export {
33
+ UnveilHero,
34
+ WhereWhen,
35
+ CardGrid,
36
+ HypeRow,
37
+ NotifyPanel,
38
+ } from './unveil/blocks'
39
+ export type { WhereWhenProps, UnveilCard } from './unveil/blocks'
40
+ export { TabletPanel } from './unveil/Tablet'
41
+ export { useUnveilBg, BG_EFFECTS, BG_DEFAULTS, parseBgQuery } from './unveil/bg'
42
+ export type { BgEffect } from './unveil/bg'
43
+ export { ShimmerButton } from './ui/shimmer-button'
44
+ export { AnimatedShinyText } from './ui/animated-shiny-text'
45
+ export { Callout } from './ui/Callout'
46
+
47
+ export type { NavLink, LinkComponent } from './shell/types'
48
+
49
+ export { createSupabaseBrowser, chunkedCookieStorage } from './supabase/browser'
50
+ // NOTE: server-only helpers (next/headers, next/server) are intentionally
51
+ // NOT re-exported here — the barrel ships to client components. Deep-import:
52
+ // import { createSupabaseServerClient } from '@meycult/core/supabase/server'
53
+ // import { updateSession } from '@meycult/core/supabase/proxy'
54
+ // import { exchangeCodeForSession } from '@meycult/core/supabase/callback'
55
+
56
+ export type { SessionProfile } from './auth/types'
57
+ export { RequireAuth } from './auth/Gate'
58
+
59
+ export { formatPrice } from './lib/money'
60
+ export { isAbsoluteUrl, resolveImage } from './lib/images'
61
+ export { createPersistStore } from './lib/store'
@@ -0,0 +1,10 @@
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
+ }
@@ -0,0 +1,12 @@
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
+ }
@@ -0,0 +1,19 @@
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
+ }
@@ -0,0 +1,13 @@
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
+ >