+
);
}
diff --git a/src/components/layout/LayoutQuestion/LayoutQuestion.tsx b/src/components/layout/LayoutQuestion/LayoutQuestion.tsx
index 50d94b1..8e89e96 100644
--- a/src/components/layout/LayoutQuestion/LayoutQuestion.tsx
+++ b/src/components/layout/LayoutQuestion/LayoutQuestion.tsx
@@ -50,7 +50,7 @@ function LayoutQuestion({
...props.style,
}}
>
-
+ {headerProps && }
{title && (
{
answers: MainButtonProps[];
@@ -25,6 +25,7 @@ function RadioAnswersList({
const [selectedAnswer, setSelectedAnswer] = useState(
activeAnswer
);
+ const isInitialMount = useRef(true);
useEffect(() => {
setSelectedAnswer(activeAnswer ?? null);
@@ -36,6 +37,12 @@ function RadioAnswersList({
};
useEffect(() => {
+ // НЕ вызываем callback при первоначальной загрузке компонента
+ if (isInitialMount.current) {
+ isInitialMount.current = false;
+ return;
+ }
+
onChangeSelectedAnswer?.(selectedAnswer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedAnswer]);
diff --git a/src/components/widgets/SelectAnswersList/SelectAnswersList.tsx b/src/components/widgets/SelectAnswersList/SelectAnswersList.tsx
index a4fa42a..6beebc1 100644
--- a/src/components/widgets/SelectAnswersList/SelectAnswersList.tsx
+++ b/src/components/widgets/SelectAnswersList/SelectAnswersList.tsx
@@ -5,7 +5,7 @@ import {
MainButton,
MainButtonProps,
} from "@/components/ui/MainButton/MainButton";
-import { useEffect, useState } from "react";
+import { useEffect, useState, useRef } from "react";
export interface SelectAnswersListProps extends React.ComponentProps<"div"> {
answers: MainButtonProps[];
@@ -25,6 +25,7 @@ function SelectAnswersList({
const [selectedAnswers, setSelectedAnswers] = useState<
MainButtonProps[] | null
>(activeAnswers);
+ const isInitialMount = useRef(true);
useEffect(() => {
setSelectedAnswers(activeAnswers ?? null);
@@ -42,6 +43,12 @@ function SelectAnswersList({
};
useEffect(() => {
+ // НЕ вызываем callback при первоначальной загрузке компонента
+ if (isInitialMount.current) {
+ isInitialMount.current = false;
+ return;
+ }
+
onChangeSelectedAnswers?.(selectedAnswers);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedAnswers]);
diff --git a/src/lib/funnel/mappers.tsx b/src/lib/funnel/mappers.tsx
index 6af2209..89f34af 100644
--- a/src/lib/funnel/mappers.tsx
+++ b/src/lib/funnel/mappers.tsx
@@ -9,7 +9,6 @@ import type {
TypographyVariant,
BottomActionButtonDefinition,
ScreenDefinition,
- ColorPalette,
} from "./types";
import type { LayoutQuestionProps } from "@/components/layout/LayoutQuestion/LayoutQuestion";
import type { ActionButtonProps } from "@/components/ui/ActionButton/ActionButton";
@@ -137,10 +136,14 @@ export function buildBottomActionButtonProps(
options: BuildActionButtonOptions,
buttonDef?: BottomActionButtonDefinition
): BottomActionButtonProps | undefined {
- if (buttonDef?.show === false) {
+ // Если кнопка отключена и градиент явно отключен
+ if (buttonDef?.show === false && buttonDef?.showGradientBlur === false) {
return undefined;
}
-
+
+ // ВАЖНО: Если мы сюда дошли, значит логика FunnelRuntime уже решила
+ // что кнопка должна показываться (даже при show: false для multi selection)
+ // Поэтому всегда создаем actionButtonProps
const actionButtonProps = buildActionButtonProps(options, buttonDef);
return {
@@ -154,7 +157,8 @@ interface BuildLayoutQuestionOptions {
subtitleDefaults?: TypographyDefaults;
canGoBack: boolean;
onBack: () => void;
- actionButtonOptions: BuildActionButtonOptions;
+ actionButtonOptions?: BuildActionButtonOptions;
+ screenProgress?: { current: number; total: number };
}
export function buildLayoutQuestionProps(
@@ -166,15 +170,26 @@ export function buildLayoutQuestionProps(
subtitleDefaults = { font: "inter", weight: "medium", color: "muted", align: "left" },
canGoBack,
onBack,
- actionButtonOptions
+ actionButtonOptions,
+ screenProgress
} = options;
const showBackButton = shouldShowBackButton(screen.header, canGoBack);
const showHeader = shouldShowHeader(screen.header);
+ const bottomActionButtonProps = actionButtonOptions ? buildBottomActionButtonProps(
+ actionButtonOptions,
+ 'bottomActionButton' in screen ? screen.bottomActionButton : undefined
+ ) : undefined;
+
+
return {
headerProps: showHeader ? {
- progressProps: buildHeaderProgress(screen.header?.progress),
+ progressProps: screenProgress ? buildHeaderProgress({
+ current: screenProgress.current,
+ total: screenProgress.total,
+ label: `${screenProgress.current} of ${screenProgress.total}`
+ }) : buildHeaderProgress(screen.header?.progress),
onBack: showBackButton ? onBack : undefined,
showBackButton,
} : undefined,
@@ -189,117 +204,7 @@ export function buildLayoutQuestionProps(
as: "p",
defaults: subtitleDefaults,
}) : undefined,
- bottomActionButtonProps: buildBottomActionButtonProps(
- actionButtonOptions,
- 'bottomActionButton' in screen ? screen.bottomActionButton : undefined
- ),
+ bottomActionButtonProps,
};
}
-// Color system utilities
-const DEFAULT_COLOR_PALETTE: ColorPalette = {
- text: {
- primary: "#1E293B",
- secondary: "#475569",
- muted: "#64748B",
- accent: "#3B82F6",
- success: "#10B981",
- error: "#EF4444",
- warning: "#F59E0B",
- },
- background: {
- primary: "#FFFFFF",
- secondary: "#F8FAFC",
- accent: "#EFF6FF",
- success: "#ECFDF5",
- error: "#FEF2F2",
- warning: "#FFFBEB",
- },
- button: {
- primary: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
- primaryText: "#FFFFFF",
- secondary: "#F1F5F9",
- secondaryText: "#334155",
- disabled: "#E2E8F0",
- disabledText: "#94A3B8",
- },
- border: {
- primary: "#E2E8F0",
- accent: "#3B82F6",
- success: "#10B981",
- error: "#EF4444",
- },
- shadow: {
- light: "rgba(0, 0, 0, 0.05)",
- medium: "rgba(0, 0, 0, 0.1)",
- heavy: "rgba(0, 0, 0, 0.15)",
- colored: "rgba(59, 130, 246, 0.3)",
- },
-};
-
-export function resolveColorPalette(
- funnelPalette?: ColorPalette,
- screenOverrides?: Partial
-): ColorPalette {
- // Deep merge: Default -> Funnel -> Screen overrides
- const basePalette = {
- text: { ...DEFAULT_COLOR_PALETTE.text, ...funnelPalette?.text },
- background: { ...DEFAULT_COLOR_PALETTE.background, ...funnelPalette?.background },
- button: { ...DEFAULT_COLOR_PALETTE.button, ...funnelPalette?.button },
- border: { ...DEFAULT_COLOR_PALETTE.border, ...funnelPalette?.border },
- shadow: { ...DEFAULT_COLOR_PALETTE.shadow, ...funnelPalette?.shadow },
- };
-
- if (!screenOverrides) return basePalette;
-
- return {
- text: { ...basePalette.text, ...screenOverrides.text },
- background: { ...basePalette.background, ...screenOverrides.background },
- button: { ...basePalette.button, ...screenOverrides.button },
- border: { ...basePalette.border, ...screenOverrides.border },
- shadow: { ...basePalette.shadow, ...screenOverrides.shadow },
- };
-}
-
-export function getCSSVariables(palette: ColorPalette): Record {
- const cssVars: Record = {};
-
- // Text colors
- if (palette.text?.primary) cssVars['--funnel-text-primary'] = palette.text.primary;
- if (palette.text?.secondary) cssVars['--funnel-text-secondary'] = palette.text.secondary;
- if (palette.text?.muted) cssVars['--funnel-text-muted'] = palette.text.muted;
- if (palette.text?.accent) cssVars['--funnel-text-accent'] = palette.text.accent;
- if (palette.text?.success) cssVars['--funnel-text-success'] = palette.text.success;
- if (palette.text?.error) cssVars['--funnel-text-error'] = palette.text.error;
- if (palette.text?.warning) cssVars['--funnel-text-warning'] = palette.text.warning;
-
- // Background colors
- if (palette.background?.primary) cssVars['--funnel-bg-primary'] = palette.background.primary;
- if (palette.background?.secondary) cssVars['--funnel-bg-secondary'] = palette.background.secondary;
- if (palette.background?.accent) cssVars['--funnel-bg-accent'] = palette.background.accent;
- if (palette.background?.success) cssVars['--funnel-bg-success'] = palette.background.success;
- if (palette.background?.error) cssVars['--funnel-bg-error'] = palette.background.error;
- if (palette.background?.warning) cssVars['--funnel-bg-warning'] = palette.background.warning;
-
- // Button colors
- if (palette.button?.primary) cssVars['--funnel-btn-primary'] = palette.button.primary;
- if (palette.button?.primaryText) cssVars['--funnel-btn-primary-text'] = palette.button.primaryText;
- if (palette.button?.secondary) cssVars['--funnel-btn-secondary'] = palette.button.secondary;
- if (palette.button?.secondaryText) cssVars['--funnel-btn-secondary-text'] = palette.button.secondaryText;
- if (palette.button?.disabled) cssVars['--funnel-btn-disabled'] = palette.button.disabled;
- if (palette.button?.disabledText) cssVars['--funnel-btn-disabled-text'] = palette.button.disabledText;
-
- // Border colors
- if (palette.border?.primary) cssVars['--funnel-border-primary'] = palette.border.primary;
- if (palette.border?.accent) cssVars['--funnel-border-accent'] = palette.border.accent;
- if (palette.border?.success) cssVars['--funnel-border-success'] = palette.border.success;
- if (palette.border?.error) cssVars['--funnel-border-error'] = palette.border.error;
-
- // Shadow colors
- if (palette.shadow?.light) cssVars['--funnel-shadow-light'] = palette.shadow.light;
- if (palette.shadow?.medium) cssVars['--funnel-shadow-medium'] = palette.shadow.medium;
- if (palette.shadow?.heavy) cssVars['--funnel-shadow-heavy'] = palette.shadow.heavy;
- if (palette.shadow?.colored) cssVars['--funnel-shadow-colored'] = palette.shadow.colored;
-
- return cssVars;
-}
diff --git a/src/lib/funnel/types.ts b/src/lib/funnel/types.ts
index 493e81b..36f9824 100644
--- a/src/lib/funnel/types.ts
+++ b/src/lib/funnel/types.ts
@@ -62,56 +62,7 @@ export interface DefaultTexts {
continueButton?: string; // "Continue"
}
-// Color system for consistent theming
-export interface TextColors {
- primary?: string; // Main text color - #1E293B
- secondary?: string; // Secondary text - #475569
- muted?: string; // Muted/disabled text - #64748B
- accent?: string; // Accent/highlight text - #3B82F6
- success?: string; // Success messages - #10B981
- error?: string; // Error messages - #EF4444
- warning?: string; // Warning messages - #F59E0B
-}
-export interface BackgroundColors {
- primary?: string; // Main background - #FFFFFF
- secondary?: string; // Secondary background - #F8FAFC
- accent?: string; // Accent background - #EFF6FF
- success?: string; // Success background - #ECFDF5
- error?: string; // Error background - #FEF2F2
- warning?: string; // Warning background - #FFFBEB
-}
-
-export interface ButtonColors {
- primary?: string; // Primary button background - gradient or solid
- primaryText?: string; // Primary button text - #FFFFFF
- secondary?: string; // Secondary button background - #F1F5F9
- secondaryText?: string; // Secondary button text - #334155
- disabled?: string; // Disabled button background - #E2E8F0
- disabledText?: string; // Disabled button text - #94A3B8
-}
-
-export interface BorderColors {
- primary?: string; // Main borders - #E2E8F0
- accent?: string; // Accent borders - #3B82F6
- success?: string; // Success borders - #10B981
- error?: string; // Error borders - #EF4444
-}
-
-export interface ShadowColors {
- light?: string; // Light shadow - rgba(0, 0, 0, 0.05)
- medium?: string; // Medium shadow - rgba(0, 0, 0, 0.1)
- heavy?: string; // Heavy shadow - rgba(0, 0, 0, 0.15)
- colored?: string; // Colored shadow (for buttons) - rgba(59, 130, 246, 0.3)
-}
-
-export interface ColorPalette {
- text?: TextColors;
- background?: BackgroundColors;
- button?: ButtonColors;
- border?: BorderColors;
- shadow?: ShadowColors;
-}
export interface NavigationConditionDefinition {
screenId: string;
@@ -148,7 +99,6 @@ export interface InfoScreenDefinition {
};
bottomActionButton?: BottomActionButtonDefinition;
navigation?: NavigationDefinition;
- colorOverrides?: Partial; // Override colors for this screen
}
export interface DateInputDefinition {
@@ -176,7 +126,6 @@ export interface DateScreenDefinition {
};
bottomActionButton?: BottomActionButtonDefinition;
navigation?: NavigationDefinition;
- colorOverrides?: Partial;
}
export interface CouponDefinition {
@@ -199,7 +148,6 @@ export interface CouponScreenDefinition {
copiedMessage?: string; // "Промокод скопирован!" text
bottomActionButton?: BottomActionButtonDefinition;
navigation?: NavigationDefinition;
- colorOverrides?: Partial;
}
export interface FormFieldDefinition {
@@ -231,19 +179,8 @@ export interface FormScreenDefinition {
validationMessages?: FormValidationMessages;
bottomActionButton?: BottomActionButtonDefinition;
navigation?: NavigationDefinition;
- colorOverrides?: Partial;
}
-export interface TextScreenDefinition {
- id: string;
- template: "text";
- header?: HeaderDefinition;
- title: TypographyVariant;
- content: TypographyVariant;
- bottomActionButton?: BottomActionButtonDefinition;
- navigation?: NavigationDefinition;
- colorOverrides?: Partial;
-}
export interface ListScreenDefinition {
id: string;
@@ -257,11 +194,11 @@ export interface ListScreenDefinition {
options: ListOptionDefinition[];
bottomActionButton?: BottomActionButtonDefinition;
};
+ bottomActionButton?: BottomActionButtonDefinition;
navigation?: NavigationDefinition;
- colorOverrides?: Partial;
}
-export type ScreenDefinition = InfoScreenDefinition | DateScreenDefinition | CouponScreenDefinition | FormScreenDefinition | TextScreenDefinition | ListScreenDefinition;
+export type ScreenDefinition = InfoScreenDefinition | DateScreenDefinition | CouponScreenDefinition | FormScreenDefinition | ListScreenDefinition;
export interface FunnelMetaDefinition {
id: string;
@@ -274,7 +211,6 @@ export interface FunnelMetaDefinition {
export interface FunnelDefinition {
meta: FunnelMetaDefinition;
defaultTexts?: DefaultTexts;
- colorPalette?: ColorPalette;
screens: ScreenDefinition[];
}