fix: add light theme support to connection page

Add isLight-aware styling to all block renderers (cards, timeline,
accordion, minimal), BlockButtons, and InstallationGuide. Update
colorParser gradient functions to generate light-appropriate backgrounds.
Reduce wave blob opacity on light theme for better content readability.
This commit is contained in:
c0mrade
2026-02-06 18:30:19 +03:00
parent 5171890745
commit 88d9377adb
9 changed files with 83 additions and 21 deletions

View File

@@ -8,6 +8,7 @@ import type {
RemnawavePlatformData, RemnawavePlatformData,
RemnawaveButtonClient, RemnawaveButtonClient,
} from '@/types'; } from '@/types';
import { useTheme } from '@/hooks/useTheme';
import { CardsBlock, TimelineBlock, AccordionBlock, MinimalBlock, BlockButtons } from './blocks'; import { CardsBlock, TimelineBlock, AccordionBlock, MinimalBlock, BlockButtons } from './blocks';
import type { BlockRendererProps } from './blocks'; import type { BlockRendererProps } from './blocks';
@@ -51,6 +52,7 @@ export default function InstallationGuide({
onGoBack, onGoBack,
}: Props) { }: Props) {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const { isLight } = useTheme();
const detectedPlatform = useMemo(() => detectPlatform(), []); const detectedPlatform = useMemo(() => detectPlatform(), []);
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768; const isMobile = typeof window !== 'undefined' && window.innerWidth < 768;
@@ -127,6 +129,7 @@ export default function InstallationGuide({
<BlockButtons <BlockButtons
buttons={buttons} buttons={buttons}
variant={variant} variant={variant}
isLight={isLight}
subscriptionUrl={appConfig.subscriptionUrl} subscriptionUrl={appConfig.subscriptionUrl}
hideLink={appConfig.hideLink} hideLink={appConfig.hideLink}
deepLink={selectedApp?.deepLink} deepLink={selectedApp?.deepLink}
@@ -140,6 +143,7 @@ export default function InstallationGuide({
appConfig.subscriptionUrl, appConfig.subscriptionUrl,
appConfig.hideLink, appConfig.hideLink,
selectedApp?.deepLink, selectedApp?.deepLink,
isLight,
getLocalizedText, getLocalizedText,
getBaseTranslation, getBaseTranslation,
getSvgHtml, getSvgHtml,
@@ -221,9 +225,11 @@ export default function InstallationGuide({
if (app) setSelectedApp(app); if (app) setSelectedApp(app);
} }
}} }}
className={`appearance-none rounded-xl border border-dark-700 bg-dark-800 py-2 pr-8 text-sm font-medium text-dark-200 outline-none transition-colors hover:border-dark-600 ${ className={`appearance-none rounded-xl border py-2 pr-8 text-sm font-medium outline-none transition-colors ${
currentPlatformSvg ? 'pl-10' : 'pl-4' isLight
}`} ? 'border-dark-700/60 bg-white/80 text-dark-200 shadow-sm hover:border-dark-600'
: 'border-dark-700 bg-dark-800 text-dark-200 hover:border-dark-600'
} ${currentPlatformSvg ? 'pl-10' : 'pl-4'}`}
> >
{availablePlatforms.map((p) => ( {availablePlatforms.map((p) => (
<option key={p} value={p}> <option key={p} value={p}>
@@ -258,8 +264,12 @@ export default function InstallationGuide({
onClick={() => setSelectedApp(app)} onClick={() => setSelectedApp(app)}
className={`relative flex min-w-[calc(50%-0.25rem)] items-center gap-2 overflow-hidden rounded-xl px-4 py-2 text-sm font-medium transition-all active:scale-[0.97] ${ className={`relative flex min-w-[calc(50%-0.25rem)] items-center gap-2 overflow-hidden rounded-xl px-4 py-2 text-sm font-medium transition-all active:scale-[0.97] ${
isSelected isSelected
? 'bg-accent-500/15 text-accent-400 ring-1 ring-accent-500/40' ? isLight
: 'border border-dark-700/50 bg-dark-800/80 text-dark-200 hover:border-dark-600/50 hover:bg-dark-700/80' ? 'bg-accent-500/15 text-accent-600 ring-1 ring-accent-500/40'
: 'bg-accent-500/15 text-accent-400 ring-1 ring-accent-500/40'
: isLight
? 'border border-dark-700/60 bg-white/80 text-dark-200 shadow-sm hover:border-dark-600/50 hover:bg-white'
: 'border border-dark-700/50 bg-dark-800/80 text-dark-200 hover:border-dark-600/50 hover:bg-dark-700/80'
}`} }`}
> >
{app.featured && <span className="h-2 w-2 shrink-0 rounded-full bg-amber-400" />} {app.featured && <span className="h-2 w-2 shrink-0 rounded-full bg-amber-400" />}
@@ -306,6 +316,7 @@ export default function InstallationGuide({
<Renderer <Renderer
blocks={selectedApp.blocks} blocks={selectedApp.blocks}
isMobile={isMobile} isMobile={isMobile}
isLight={isLight}
getLocalizedText={getLocalizedText} getLocalizedText={getLocalizedText}
getSvgHtml={getSvgHtml} getSvgHtml={getSvgHtml}
renderBlockButtons={renderBlockButtons} renderBlockButtons={renderBlockButtons}

View File

@@ -6,6 +6,7 @@ import type { BlockRendererProps } from './types';
export function AccordionBlock({ export function AccordionBlock({
blocks, blocks,
isMobile, isMobile,
isLight,
getLocalizedText, getLocalizedText,
getSvgHtml, getSvgHtml,
renderBlockButtons, renderBlockButtons,
@@ -15,14 +16,20 @@ export function AccordionBlock({
return ( return (
<div className="space-y-2"> <div className="space-y-2">
{blocks.map((block, index) => { {blocks.map((block, index) => {
const gradientStyle = getColorGradient(block.svgIconColor || 'cyan'); const gradientStyle = getColorGradient(block.svgIconColor || 'cyan', isLight);
const isOpen = openIndex === index; const isOpen = openIndex === index;
return ( return (
<div <div
key={index} key={index}
className={`overflow-hidden rounded-2xl border bg-dark-800/50 transition-colors ${ className={`overflow-hidden rounded-2xl border transition-colors ${
isOpen ? 'border-accent-500/30' : 'border-dark-700/50' isLight
? isOpen
? 'border-accent-500/30 bg-white/80 shadow-sm'
: 'border-dark-700/60 bg-white/60'
: isOpen
? 'border-accent-500/30 bg-dark-800/50'
: 'border-dark-700/50 bg-dark-800/50'
}`} }`}
> >
{/* Control */} {/* Control */}

View File

@@ -38,6 +38,7 @@ const CheckIcon = () => (
interface BlockButtonsProps { interface BlockButtonsProps {
buttons: RemnawaveButtonClient[] | undefined; buttons: RemnawaveButtonClient[] | undefined;
variant: 'light' | 'subtle'; variant: 'light' | 'subtle';
isLight?: boolean;
subscriptionUrl: string | null; subscriptionUrl: string | null;
hideLink?: boolean; hideLink?: boolean;
deepLink?: string | null; deepLink?: string | null;
@@ -50,6 +51,7 @@ interface BlockButtonsProps {
export function BlockButtons({ export function BlockButtons({
buttons, buttons,
variant, variant,
isLight,
subscriptionUrl, subscriptionUrl,
hideLink, hideLink,
deepLink, deepLink,
@@ -80,8 +82,12 @@ export function BlockButtons({
const baseClass = const baseClass =
variant === 'light' variant === 'light'
? 'rounded-xl border border-accent-500/40 px-4 py-2 text-sm font-medium text-accent-400 transition-all hover:bg-accent-500/10' ? isLight
: 'rounded-xl px-3 py-1.5 text-sm font-medium text-dark-300 transition-all hover:bg-dark-700/50'; ? 'rounded-xl border border-accent-500/50 px-4 py-2 text-sm font-medium text-accent-600 shadow-sm transition-all hover:bg-accent-500/10'
: 'rounded-xl border border-accent-500/40 px-4 py-2 text-sm font-medium text-accent-400 transition-all hover:bg-accent-500/10'
: isLight
? 'rounded-xl px-3 py-1.5 text-sm font-medium text-dark-300 transition-all hover:bg-dark-700/30'
: 'rounded-xl px-3 py-1.5 text-sm font-medium text-dark-300 transition-all hover:bg-dark-700/50';
return ( return (
<div className="mt-3 flex flex-wrap gap-2"> <div className="mt-3 flex flex-wrap gap-2">
@@ -120,7 +126,7 @@ export function BlockButtons({
onClick={() => handleCopy(url)} onClick={() => handleCopy(url)}
className={`flex items-center gap-2 ${ className={`flex items-center gap-2 ${
copied copied
? 'rounded-xl border border-success-500 bg-success-500/10 px-4 py-2 text-sm font-medium text-success-400' ? `rounded-xl border border-success-500 bg-success-500/10 px-4 py-2 text-sm font-medium ${isLight ? 'text-success-600' : 'text-success-400'}`
: baseClass : baseClass
}`} }`}
> >

View File

@@ -5,6 +5,7 @@ import type { BlockRendererProps } from './types';
export function CardsBlock({ export function CardsBlock({
blocks, blocks,
isMobile, isMobile,
isLight,
getLocalizedText, getLocalizedText,
getSvgHtml, getSvgHtml,
renderBlockButtons, renderBlockButtons,
@@ -12,12 +13,16 @@ export function CardsBlock({
return ( return (
<div className="space-y-3"> <div className="space-y-3">
{blocks.map((block, index) => { {blocks.map((block, index) => {
const gradientStyle = getColorGradient(block.svgIconColor || 'cyan'); const gradientStyle = getColorGradient(block.svgIconColor || 'cyan', isLight);
return ( return (
<div <div
key={index} key={index}
className="rounded-2xl border border-dark-700/50 bg-dark-800/50 p-4 sm:p-5" className={`rounded-2xl border p-4 sm:p-5 ${
isLight
? 'border-dark-700/60 bg-white/80 shadow-sm'
: 'border-dark-700/50 bg-dark-800/50'
}`}
> >
<div className="flex items-start gap-3 sm:gap-4"> <div className="flex items-start gap-3 sm:gap-4">
<ThemeIcon <ThemeIcon

View File

@@ -5,6 +5,7 @@ import type { BlockRendererProps } from './types';
export function MinimalBlock({ export function MinimalBlock({
blocks, blocks,
isMobile, isMobile,
isLight,
getLocalizedText, getLocalizedText,
getSvgHtml, getSvgHtml,
renderBlockButtons, renderBlockButtons,
@@ -12,11 +13,18 @@ export function MinimalBlock({
return ( return (
<div> <div>
{blocks.map((block, index) => { {blocks.map((block, index) => {
const gradientStyle = getColorGradient(block.svgIconColor || 'cyan'); const gradientStyle = getColorGradient(block.svgIconColor || 'cyan', isLight);
const isLast = index === blocks.length - 1; const isLast = index === blocks.length - 1;
return ( return (
<div key={index} className={isLast ? 'pb-4' : 'mb-4 border-b border-dark-700/50 pb-4'}> <div
key={index}
className={
isLast
? 'pb-4'
: `mb-4 border-b pb-4 ${isLight ? 'border-dark-700/40' : 'border-dark-700/50'}`
}
>
<div className="mb-2 flex items-center gap-3"> <div className="mb-2 flex items-center gap-3">
<ThemeIcon <ThemeIcon
getSvgHtml={getSvgHtml} getSvgHtml={getSvgHtml}

View File

@@ -5,6 +5,7 @@ import type { BlockRendererProps } from './types';
export function TimelineBlock({ export function TimelineBlock({
blocks, blocks,
isMobile, isMobile,
isLight,
getLocalizedText, getLocalizedText,
getSvgHtml, getSvgHtml,
renderBlockButtons, renderBlockButtons,
@@ -12,7 +13,7 @@ export function TimelineBlock({
return ( return (
<div className="space-y-0"> <div className="space-y-0">
{blocks.map((block, index) => { {blocks.map((block, index) => {
const gradientStyle = getColorGradientSolid(block.svgIconColor || 'cyan'); const gradientStyle = getColorGradientSolid(block.svgIconColor || 'cyan', isLight);
const isLast = index === blocks.length - 1; const isLast = index === blocks.length - 1;
return ( return (
@@ -25,7 +26,9 @@ export function TimelineBlock({
gradientStyle={gradientStyle} gradientStyle={gradientStyle}
isMobile={isMobile} isMobile={isMobile}
/> />
{!isLast && <div className="w-0.5 flex-1 bg-dark-700" />} {!isLast && (
<div className={`w-0.5 flex-1 ${isLight ? 'bg-dark-700/40' : 'bg-dark-700'}`} />
)}
</div> </div>
{/* Right column: content */} {/* Right column: content */}
<div className={`min-w-0 flex-1 ${isLast ? '' : 'pb-6'}`}> <div className={`min-w-0 flex-1 ${isLast ? '' : 'pb-6'}`}>

View File

@@ -3,6 +3,7 @@ import type { RemnawaveBlockClient, RemnawaveButtonClient, LocalizedText } from
export interface BlockRendererProps { export interface BlockRendererProps {
blocks: RemnawaveBlockClient[]; blocks: RemnawaveBlockClient[];
isMobile: boolean; isMobile: boolean;
isLight: boolean;
getLocalizedText: (text: LocalizedText | undefined) => string; getLocalizedText: (text: LocalizedText | undefined) => string;
getSvgHtml: (key: string | undefined) => string; getSvgHtml: (key: string | undefined) => string;
renderBlockButtons: ( renderBlockButtons: (

View File

@@ -1512,8 +1512,8 @@ input[type='checkbox']:hover:not(:checked) {
/* Light theme adjustments */ /* Light theme adjustments */
.light .wave-blob { .light .wave-blob {
opacity: 0.6; opacity: 0.35;
filter: blur(70px); filter: blur(80px);
} }
.light .wave-blob-1 { .light .wave-blob-1 {

View File

@@ -31,16 +31,37 @@ export interface ColorGradientStyle {
boxShadow?: string; boxShadow?: string;
} }
export const getColorGradient = (color: string): ColorGradientStyle => { export const getColorGradient = (color: string, light?: boolean): ColorGradientStyle => {
const [r, g, b] = getRgb(color); const [r, g, b] = getRgb(color);
if (light) {
return {
background: `linear-gradient(135deg, rgba(${r},${g},${b},0.12) 0%, rgba(${r},${g},${b},0.05) 100%)`,
border: `1px solid rgba(${r},${g},${b},0.2)`,
boxShadow: `0 1px 3px rgba(${r},${g},${b},0.1)`,
};
}
return { return {
background: `linear-gradient(135deg, rgba(${r},${g},${b},0.15) 0%, rgba(${r},${g},${b},0.08) 100%)`, background: `linear-gradient(135deg, rgba(${r},${g},${b},0.15) 0%, rgba(${r},${g},${b},0.08) 100%)`,
border: `1px solid rgba(${r},${g},${b},0.3)`, border: `1px solid rgba(${r},${g},${b},0.3)`,
}; };
}; };
export const getColorGradientSolid = (color: string): ColorGradientStyle => { export const getColorGradientSolid = (color: string, light?: boolean): ColorGradientStyle => {
const [r, g, b] = getRgb(color); const [r, g, b] = getRgb(color);
if (light) {
// Light theme: soft tinted background instead of dark solid
const light1 = [245 + r * 0.02, 243 + g * 0.02, 240 + b * 0.02].map((v) =>
Math.min(255, Math.floor(v)),
);
const light2 = [240 + r * 0.03, 237 + g * 0.03, 233 + b * 0.03].map((v) =>
Math.min(255, Math.floor(v)),
);
return {
background: `linear-gradient(135deg, rgb(${light1}) 0%, rgb(${light2}) 100%)`,
border: `1px solid rgba(${r},${g},${b},0.25)`,
boxShadow: `0 1px 4px rgba(${r},${g},${b},0.12)`,
};
}
const dark1 = [22 + r * 0.08, 27 + g * 0.08, 35 + b * 0.08].map(Math.floor); const dark1 = [22 + r * 0.08, 27 + g * 0.08, 35 + b * 0.08].map(Math.floor);
const dark2 = [20 + r * 0.05, 24 + g * 0.05, 30 + b * 0.05].map(Math.floor); const dark2 = [20 + r * 0.05, 24 + g * 0.05, 30 + b * 0.05].map(Math.floor);