void
+ setChannelSubscription: (info: ChannelSubscriptionInfo) => void
+ clearBlocking: () => void
+}
+
+export const useBlockingStore = create((set) => ({
+ blockingType: null,
+ maintenanceInfo: null,
+ channelInfo: null,
+
+ setMaintenance: (info) => set({
+ blockingType: 'maintenance',
+ maintenanceInfo: info,
+ channelInfo: null,
+ }),
+
+ setChannelSubscription: (info) => set({
+ blockingType: 'channel_subscription',
+ channelInfo: info,
+ maintenanceInfo: null,
+ }),
+
+ clearBlocking: () => set({
+ blockingType: null,
+ maintenanceInfo: null,
+ channelInfo: null,
+ }),
+}))
diff --git a/src/styles/globals.css b/src/styles/globals.css
index 408b8e1..4f14a5d 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -1,4 +1,4 @@
-@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap');
@tailwind base;
@tailwind components;
@@ -8,6 +8,14 @@
:root {
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
+ /* Bento Design System */
+ --bento-radius: 24px;
+ --bento-radius-lg: 32px;
+ --bento-gap: 16px;
+ --bento-gap-lg: 24px;
+ --bento-padding: 24px;
+ --bento-padding-lg: 32px;
+
/* Theme colors - RGB format for opacity support */
/* Dark palette (RGB values) */
--color-dark-50: 248, 250, 252;
@@ -102,6 +110,34 @@
html {
overflow-x: hidden;
+ scrollbar-width: thin;
+ scrollbar-color: rgb(var(--color-dark-700)) transparent;
+ }
+
+ ::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+ }
+
+ ::-webkit-scrollbar-track {
+ background: transparent;
+ }
+
+ ::-webkit-scrollbar-thumb {
+ background: rgb(var(--color-dark-700));
+ border-radius: 3px;
+ }
+
+ ::-webkit-scrollbar-thumb:hover {
+ background: rgb(var(--color-dark-600));
+ }
+
+ .light ::-webkit-scrollbar-thumb {
+ background: rgb(var(--color-champagne-400));
+ }
+
+ .light ::-webkit-scrollbar-thumb:hover {
+ background: rgb(var(--color-champagne-500));
}
/* Smooth scroll only on desktop - mobile uses native */
@@ -117,6 +153,19 @@
overscroll-behavior-y: contain;
/* iOS smooth scrolling */
-webkit-overflow-scrolling: touch;
+ position: relative;
+ }
+
+ /* Global Noise Texture */
+ body::before {
+ content: "";
+ position: fixed;
+ inset: 0;
+ z-index: 0;
+ pointer-events: none;
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
+ opacity: 0.03;
+ mix-blend-mode: overlay;
}
/* Optimize main scrollable content */
@@ -203,6 +252,10 @@
background-color: rgba(254, 249, 240, 0.5) !important; /* champagne-100/50 */
}
+ .light .bg-dark-900\/95 {
+ background-color: rgba(254, 249, 240, 0.95) !important; /* champagne-100/95 - mobile menu sticky header */
+ }
+
.light .bg-dark-950 {
background-color: #F7E7CE !important; /* champagne-200 */
}
@@ -323,6 +376,135 @@
}
@layer components {
+ /* ========== BENTO DESIGN SYSTEM ========== */
+
+ .bento-card {
+ @apply bg-dark-900/70 border border-dark-700/40;
+ border-radius: var(--bento-radius);
+ padding: var(--bento-padding);
+ transform: translateZ(0);
+ /* Glass Border - Inset Highlight */
+ box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.05);
+ /* Stagger animation support */
+ animation: bentoFadeIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
+ animation-delay: calc(var(--stagger, 0) * 50ms);
+ position: relative;
+ overflow: hidden;
+ }
+
+ @media (min-width: 1024px) {
+ .bento-card {
+ @apply bg-dark-900/50 backdrop-blur-sm;
+ }
+ }
+
+ /* Disable animation if user prefers reduced motion */
+ @media (prefers-reduced-motion: reduce) {
+ .bento-card {
+ animation: none;
+ }
+ }
+
+ .bento-card-hover {
+ @apply bento-card cursor-pointer;
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ }
+
+ .bento-card-hover:hover {
+ @apply bg-dark-800/60 border-dark-600/50 shadow-lg;
+ transform: translateY(-4px);
+ /* Intensify Glass Border & Add Top Spotlight */
+ box-shadow:
+ inset 0 1px 0 0 rgba(255, 255, 255, 0.1),
+ 0 10px 40px -10px rgba(0,0,0,0.5);
+ }
+
+ /* CSS Spotlight Effect via pseudo-element */
+ .bento-card-hover::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background: radial-gradient(circle at top, rgba(255, 255, 255, 0.06), transparent 60%);
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ pointer-events: none;
+ z-index: 10;
+ }
+
+ .bento-card-hover:hover::after {
+ opacity: 1;
+ }
+
+ .bento-card-hover:active {
+ transform: scale(0.98);
+ transition-duration: 0.15s;
+ }
+
+ .bento-card-glow {
+ @apply bento-card cursor-pointer;
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ }
+
+ .bento-card-glow:hover {
+ @apply shadow-glow border-accent-500/30;
+ transform: translateY(-4px);
+ }
+
+ .bento-card-glow:active {
+ transform: scale(0.98);
+ transition-duration: 0.15s;
+ }
+
+ .bento-grid {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: var(--bento-gap);
+ }
+
+ @media (min-width: 375px) {
+ .bento-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+ }
+
+ @media (min-width: 768px) {
+ .bento-grid {
+ grid-template-columns: repeat(4, 1fr);
+ gap: var(--bento-gap-lg);
+ }
+ }
+
+ .light .bento-card {
+ @apply bg-white/90 border-champagne-300/50 shadow-sm;
+ /* Light Theme Glass Border */
+ box-shadow: inset 0 1px 0 0 rgba(0, 0, 0, 0.03);
+ }
+
+ @media (min-width: 1024px) {
+ .light .bento-card {
+ @apply bg-white/80 backdrop-blur-sm;
+ }
+ }
+
+ .light .bento-card-hover:hover {
+ @apply bg-white border-champagne-400/50 shadow-md;
+ transform: translateY(-4px);
+ /* Intensify Light Theme Glass Border */
+ box-shadow:
+ inset 0 1px 0 0 rgba(0, 0, 0, 0.06),
+ 0 10px 40px -10px rgba(160, 139, 94, 0.15);
+ }
+
+ /* Adjust spotlight for light theme to be a warm glow */
+ .light .bento-card-hover::after {
+ background: radial-gradient(circle at top, rgba(255, 253, 249, 0.8), transparent 70%);
+ }
+
+ .light .bento-card-glow:hover {
+ @apply shadow-lg border-accent-400/40;
+ transform: translateY(-4px);
+ }
+
/* ========== DARK THEME COMPONENTS (default) ========== */
/* Cards - Dark (optimized for mobile) */
@@ -489,12 +671,17 @@
@apply nav-item text-accent-400 bg-accent-500/10;
}
- /* Bottom nav - Dark (optimized for mobile) */
.bottom-nav {
- @apply fixed bottom-0 left-0 right-0 z-50
- bg-dark-900/95 border-t border-dark-800/50
- safe-area-pb;
+ @apply fixed z-50 bg-dark-900/95;
+ bottom: 16px;
+ left: 16px;
+ right: 16px;
+ border-radius: var(--bento-radius);
+ padding: 8px 4px;
+ padding-bottom: calc(8px + var(--safe-area-inset-bottom));
transform: translateZ(0);
+ box-shadow: 0 4px 30px rgba(0, 0, 0, 0.4),
+ 0 0 0 1px rgba(255, 255, 255, 0.05) inset;
}
@media (min-width: 1024px) {
@@ -504,12 +691,17 @@
}
.bottom-nav-item {
- @apply flex flex-col items-center justify-center py-2 px-2 flex-1 min-w-[60px]
- text-dark-500 transition-colors duration-200 shrink-0;
+ @apply flex flex-col items-center justify-center py-2.5 px-3 flex-1 min-w-[56px]
+ text-dark-500 transition-all duration-200 shrink-0 rounded-2xl;
+ }
+
+ .bottom-nav-item:hover {
+ @apply text-dark-300;
}
.bottom-nav-item-active {
- @apply bottom-nav-item text-accent-400;
+ @apply flex flex-col items-center justify-center py-2.5 px-3 flex-1 min-w-[56px]
+ text-accent-400 bg-accent-500/15 rounded-2xl transition-all duration-200 shrink-0;
}
/* Divider - Dark */
@@ -664,17 +856,28 @@
@apply text-champagne-800 bg-champagne-200/70;
}
- /* Bottom nav - Light */
.light .bottom-nav {
- @apply bg-white/90 backdrop-blur-xl border-t border-champagne-200;
+ @apply bg-white/95;
+ box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1),
+ 0 0 0 1px rgba(0, 0, 0, 0.05);
+ }
+
+ @media (min-width: 1024px) {
+ .light .bottom-nav {
+ @apply bg-white/80 backdrop-blur-xl;
+ }
}
.light .bottom-nav-item {
@apply text-champagne-500;
}
+ .light .bottom-nav-item:hover {
+ @apply text-champagne-700;
+ }
+
.light .bottom-nav-item-active {
- @apply text-champagne-800;
+ @apply text-champagne-800 bg-champagne-300/40;
}
/* Divider - Light */
@@ -920,6 +1123,17 @@
}
/* Animations */
+@keyframes bentoFadeIn {
+ 0% {
+ opacity: 0;
+ transform: translateY(16px);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
@@ -1246,6 +1460,42 @@ input[type="range"]::-moz-range-thumb {
background: radial-gradient(circle, rgba(var(--color-accent-500), 0.7) 0%, transparent 70%);
}
+/* Mobile: brighter and faster animations */
+@media (max-width: 768px) {
+ .wave-blob {
+ opacity: 0.7;
+ filter: blur(50px);
+ }
+
+ .wave-blob-1 {
+ width: 300px;
+ height: 300px;
+ background: radial-gradient(circle, rgba(var(--color-accent-500), 0.8) 0%, transparent 70%);
+ animation: wave1-mobile 12s ease-in-out infinite;
+ }
+
+ .wave-blob-2 {
+ width: 280px;
+ height: 280px;
+ background: radial-gradient(circle, rgba(59, 130, 246, 0.8) 0%, transparent 70%);
+ animation: wave2-mobile 15s ease-in-out infinite;
+ }
+
+ @keyframes wave1-mobile {
+ 0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
+ 25% { transform: translate3d(15%, 20%, 0) scale(1.1); }
+ 50% { transform: translate3d(5%, 35%, 0) scale(1.15); }
+ 75% { transform: translate3d(20%, 10%, 0) scale(1.05); }
+ }
+
+ @keyframes wave2-mobile {
+ 0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
+ 25% { transform: translate3d(-20%, -15%, 0) scale(1.15); }
+ 50% { transform: translate3d(-10%, -30%, 0) scale(1.1); }
+ 75% { transform: translate3d(-25%, -5%, 0) scale(1.05); }
+ }
+}
+
/* Disable animations for reduced motion preference */
@media (prefers-reduced-motion: reduce) {
.wave-blob {
diff --git a/src/types/index.ts b/src/types/index.ts
index 480fd88..2154f86 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -65,6 +65,7 @@ export interface Subscription {
autopay_enabled: boolean
autopay_days_before: number
subscription_url: string | null
+ hide_subscription_link: boolean
is_active: boolean
is_expired: boolean
traffic_purchases?: TrafficPurchase[]
diff --git a/src/utils/colorConversion.ts b/src/utils/colorConversion.ts
new file mode 100644
index 0000000..acb39fb
--- /dev/null
+++ b/src/utils/colorConversion.ts
@@ -0,0 +1,102 @@
+export interface HSLColor {
+ h: number
+ s: number
+ l: number
+}
+
+export interface RGBColor {
+ r: number
+ g: number
+ b: number
+}
+
+export function hexToRgb(hex: string): RGBColor {
+ if (hex.length === 4) {
+ hex = '#' + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3]
+ }
+ const r = parseInt(hex.slice(1, 3), 16)
+ const g = parseInt(hex.slice(3, 5), 16)
+ const b = parseInt(hex.slice(5, 7), 16)
+ return { r, g, b }
+}
+
+export function rgbToHex(r: number, g: number, b: number): string {
+ return (
+ '#' +
+ [r, g, b]
+ .map((x) => {
+ const hex = Math.round(Math.max(0, Math.min(255, x))).toString(16)
+ return hex.length === 1 ? '0' + hex : hex
+ })
+ .join('')
+ )
+}
+
+export function hexToHsl(hex: string): HSLColor {
+ const { r, g, b } = hexToRgb(hex)
+ const rNorm = r / 255
+ const gNorm = g / 255
+ const bNorm = b / 255
+
+ const max = Math.max(rNorm, gNorm, bNorm)
+ const min = Math.min(rNorm, gNorm, bNorm)
+ let h = 0
+ let s = 0
+ const l = (max + min) / 2
+
+ if (max !== min) {
+ const d = max - min
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
+
+ switch (max) {
+ case rNorm:
+ h = ((gNorm - bNorm) / d + (gNorm < bNorm ? 6 : 0)) / 6
+ break
+ case gNorm:
+ h = ((bNorm - rNorm) / d + 2) / 6
+ break
+ case bNorm:
+ h = ((rNorm - gNorm) / d + 4) / 6
+ break
+ }
+ }
+
+ return {
+ h: Math.round(h * 360),
+ s: Math.round(s * 100),
+ l: Math.round(l * 100),
+ }
+}
+
+export function hslToRgb(h: number, s: number, l: number): RGBColor {
+ const sNorm = s / 100
+ const lNorm = l / 100
+
+ const a = sNorm * Math.min(lNorm, 1 - lNorm)
+ const f = (n: number) => {
+ const k = (n + h / 30) % 12
+ const color = lNorm - a * Math.max(Math.min(k - 3, 9 - k, 1), -1)
+ return Math.round(255 * color)
+ }
+
+ return { r: f(0), g: f(8), b: f(4) }
+}
+
+export function hslToHex(h: number, s: number, l: number): string {
+ const { r, g, b } = hslToRgb(h, s, l)
+ return rgbToHex(r, g, b)
+}
+
+export function isValidHex(hex: string): boolean {
+ return /^#[0-9A-Fa-f]{6}$/.test(hex)
+}
+
+export function normalizeHex(hex: string): string {
+ if (!hex.startsWith('#')) {
+ hex = '#' + hex
+ }
+ if (hex.length === 4) {
+ hex = '#' + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3]
+ }
+ return hex.toLowerCase()
+}
diff --git a/tailwind.config.js b/tailwind.config.js
index b6d30c7..e3b947b 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -106,7 +106,15 @@ export default {
},
},
fontFamily: {
- sans: ['Inter', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'],
+ sans: ['Manrope', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'],
+ },
+ borderRadius: {
+ 'bento': '24px',
+ '4xl': '32px',
+ },
+ spacing: {
+ 'bento': '16px',
+ 'bento-lg': '24px',
},
fontSize: {
'2xs': ['0.625rem', { lineHeight: '0.875rem' }],