Site builder: security & data-loss hardening + unified asset picker + audit backlog #3

Merged
jknapp merged 61 commits from builder-hardening-2026-07 into main 2026-07-13 01:13:28 +00:00
13 changed files with 46 additions and 132 deletions
Showing only changes of commit 4733fe84b5 - Show all commits
@@ -1,5 +1,4 @@
import React, { useCallback } from 'react'; import React from 'react';
import { useEditor } from '@craftjs/core';
import { import {
BG_COLORS, BG_COLORS,
SPACING_PRESETS, SPACING_PRESETS,
@@ -15,22 +14,13 @@ import {
labelStyle, labelStyle,
inputStyle, inputStyle,
sectionGap, sectionGap,
useNodeProp,
} from './shared'; } from './shared';
import { AssetPicker } from '../../../ui/AssetPicker'; import { AssetPicker } from '../../../ui/AssetPicker';
/* ---------- BACKGROUND SECTION ---------- */ /* ---------- BACKGROUND SECTION ---------- */
export const BackgroundSectionStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => { export const BackgroundSectionStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => {
const { actions } = useEditor(); const { setProp, setPropStyle } = useNodeProp(selectedId);
const setProp = useCallback((key: string, value: any) => {
actions.setProp(selectedId, (props: any) => { props[key] = value; });
}, [actions, selectedId]);
const setPropStyle = useCallback((key: string, value: string) => {
actions.setProp(selectedId, (props: any) => {
props.style = { ...props.style, [key]: value };
});
}, [actions, selectedId]);
const style = nodeProps.style || {}; const style = nodeProps.style || {};
@@ -12,6 +12,7 @@ import {
PresetButtonGrid, PresetButtonGrid,
TextInputField, TextInputField,
autoTextColor, autoTextColor,
useNodeProp,
} from './shared'; } from './shared';
/* ---------- BUTTON ---------- */ /* ---------- BUTTON ---------- */
@@ -19,14 +20,7 @@ export const ButtonStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodePr
const { actions } = useEditor(); const { actions } = useEditor();
const style: CSSProperties = nodeProps.style || {}; const style: CSSProperties = nodeProps.style || {};
const setPropStyle = useCallback( const { setPropStyle } = useNodeProp(selectedId);
(property: string, value: string) => {
actions.setProp(selectedId, (props: any) => {
props.style = { ...props.style, [property]: value };
});
},
[actions, selectedId],
);
const setButtonColor = useCallback( const setButtonColor = useCallback(
(bgColor: string) => { (bgColor: string) => {
@@ -1,5 +1,4 @@
import React, { useCallback, CSSProperties } from 'react'; import React, { CSSProperties } from 'react';
import { useEditor } from '@craftjs/core';
import { import {
BG_COLORS, BG_COLORS,
SPACING_PRESETS, SPACING_PRESETS,
@@ -14,28 +13,14 @@ import {
labelStyle, labelStyle,
inputStyle, inputStyle,
sectionGap, sectionGap,
useNodeProp,
} from './shared'; } from './shared';
/* ---------- CONTAINER / SECTION ---------- */ /* ---------- CONTAINER / SECTION ---------- */
export const ContainerStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => { export const ContainerStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => {
const { actions } = useEditor();
const style: CSSProperties = nodeProps.style || {}; const style: CSSProperties = nodeProps.style || {};
const setPropStyle = useCallback( const { setProp, setPropStyle } = useNodeProp(selectedId);
(property: string, value: string) => {
actions.setProp(selectedId, (props: any) => {
props.style = { ...props.style, [property]: value };
});
},
[actions, selectedId],
);
const setProp = useCallback(
(key: string, value: string) => {
actions.setProp(selectedId, (props: any) => { props[key] = value; });
},
[actions, selectedId],
);
return ( return (
<> <>
@@ -1,5 +1,4 @@
import React, { useCallback } from 'react'; import React from 'react';
import { useEditor } from '@craftjs/core';
import { import {
BG_COLORS, BG_COLORS,
SPACING_PRESETS, SPACING_PRESETS,
@@ -15,21 +14,12 @@ import {
inputStyle, inputStyle,
btnActiveStyle, btnActiveStyle,
sectionGap, sectionGap,
useNodeProp,
} from './shared'; } from './shared';
/* ---------- FORM ---------- */ /* ---------- FORM ---------- */
export const FormStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => { export const FormStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => {
const { actions } = useEditor(); const { setProp, setPropStyle } = useNodeProp(selectedId);
const setProp = useCallback((key: string, value: any) => {
actions.setProp(selectedId, (props: any) => { props[key] = value; });
}, [actions, selectedId]);
const setPropStyle = useCallback((key: string, value: string) => {
actions.setProp(selectedId, (props: any) => {
props.style = { ...props.style, [key]: value };
});
}, [actions, selectedId]);
const style = nodeProps.style || {}; const style = nodeProps.style || {};
@@ -1,4 +1,4 @@
import React, { useCallback } from 'react'; import React from 'react';
import { useEditor } from '@craftjs/core'; import { useEditor } from '@craftjs/core';
import { import {
TEXT_COLORS, TEXT_COLORS,
@@ -17,6 +17,7 @@ import {
inputStyle, inputStyle,
smallInputStyle, smallInputStyle,
sectionGap, sectionGap,
useNodeProp,
} from './shared'; } from './shared';
/* ---------- SMART GENERIC PROPS EDITOR (Fallback) ---------- */ /* ---------- SMART GENERIC PROPS EDITOR (Fallback) ---------- */
@@ -27,15 +28,7 @@ export const GenericPropsEditor: React.FC<{ selectedId: string; nodeProps: Recor
const SKIP_PROPS = new Set(['style', 'children', 'cssId', 'cssClass']); const SKIP_PROPS = new Set(['style', 'children', 'cssId', 'cssClass']);
const setPropValue = useCallback((key: string, value: any) => { const { setProp: setPropValue, setPropStyle: setStyleValue } = useNodeProp(selectedId);
actions.setProp(selectedId, (props: any) => { props[key] = value; });
}, [actions, selectedId]);
const setStyleValue = useCallback((key: string, value: string) => {
actions.setProp(selectedId, (props: any) => {
props.style = { ...props.style, [key]: value };
});
}, [actions, selectedId]);
// Categorize all props // Categorize all props
const allProps = Object.entries(nodeProps).filter(([key]) => !SKIP_PROPS.has(key)); const allProps = Object.entries(nodeProps).filter(([key]) => !SKIP_PROPS.has(key));
@@ -1,5 +1,4 @@
import React, { useCallback } from 'react'; import React from 'react';
import { useEditor } from '@craftjs/core';
import { import {
StylePanelProps, StylePanelProps,
CollapsibleSection, CollapsibleSection,
@@ -8,16 +7,13 @@ import {
inputStyle, inputStyle,
btnActiveStyle, btnActiveStyle,
sectionGap, sectionGap,
useNodeProp,
} from './shared'; } from './shared';
import { AssetPicker } from '../../../ui/AssetPicker'; import { AssetPicker } from '../../../ui/AssetPicker';
/* ---------- HERO STYLE PANEL ---------- */ /* ---------- HERO STYLE PANEL ---------- */
export const HeroStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => { export const HeroStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => {
const { actions } = useEditor(); const { setProp } = useNodeProp(selectedId);
const setProp = useCallback((key: string, value: any) => {
actions.setProp(selectedId, (props: any) => { props[key] = value; });
}, [actions, selectedId]);
const bgType = nodeProps.bgType || 'color'; const bgType = nodeProps.bgType || 'color';
@@ -1,4 +1,4 @@
import React, { useCallback, CSSProperties } from 'react'; import React, { CSSProperties } from 'react';
import { useEditor } from '@craftjs/core'; import { useEditor } from '@craftjs/core';
import { import {
IMAGE_RADIUS_PRESETS, IMAGE_RADIUS_PRESETS,
@@ -8,6 +8,7 @@ import {
SectionLabel, SectionLabel,
PresetButtonGrid, PresetButtonGrid,
TextInputField, TextInputField,
useNodeProp,
} from './shared'; } from './shared';
import { AssetPicker } from '../../../ui/AssetPicker'; import { AssetPicker } from '../../../ui/AssetPicker';
@@ -16,14 +17,7 @@ export const ImageStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodePro
const { actions } = useEditor(); const { actions } = useEditor();
const style: CSSProperties = nodeProps.style || {}; const style: CSSProperties = nodeProps.style || {};
const setPropStyle = useCallback( const { setPropStyle } = useNodeProp(selectedId);
(property: string, value: string) => {
actions.setProp(selectedId, (props: any) => {
props.style = { ...props.style, [property]: value };
});
},
[actions, selectedId],
);
const maxWidthPresets = [ const maxWidthPresets = [
{ label: '25%', value: '25%' }, { label: '25%', value: '25%' },
@@ -1,4 +1,4 @@
import React, { useCallback } from 'react'; import React from 'react';
import { useEditor } from '@craftjs/core'; import { useEditor } from '@craftjs/core';
import { import {
BG_COLORS, BG_COLORS,
@@ -17,22 +17,14 @@ import {
inputStyle, inputStyle,
smallInputStyle, smallInputStyle,
sectionGap, sectionGap,
useNodeProp,
} from './shared'; } from './shared';
import { AssetPicker } from '../../../ui/AssetPicker'; import { AssetPicker } from '../../../ui/AssetPicker';
/* ---------- MEDIA (Video / Gallery / Map / Slider) ---------- */ /* ---------- MEDIA (Video / Gallery / Map / Slider) ---------- */
export const MediaStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => { export const MediaStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => {
const { actions } = useEditor(); const { actions } = useEditor();
const { setProp, setPropStyle } = useNodeProp(selectedId);
const setProp = useCallback((key: string, value: any) => {
actions.setProp(selectedId, (props: any) => { props[key] = value; });
}, [actions, selectedId]);
const setPropStyle = useCallback((key: string, value: string) => {
actions.setProp(selectedId, (props: any) => {
props.style = { ...props.style, [key]: value };
});
}, [actions, selectedId]);
const style = nodeProps.style || {}; const style = nodeProps.style || {};
@@ -15,22 +15,14 @@ import {
inputStyle, inputStyle,
smallInputStyle, smallInputStyle,
sectionGap, sectionGap,
useNodeProp,
} from './shared'; } from './shared';
import { AssetPicker } from '../../../ui/AssetPicker'; import { AssetPicker } from '../../../ui/AssetPicker';
/* ---------- NAV / MENU / LOGO ---------- */ /* ---------- NAV / MENU / LOGO ---------- */
export const NavStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => { export const NavStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => {
const { actions } = useEditor(); const { actions } = useEditor();
const { setProp, setPropStyle } = useNodeProp(selectedId);
const setProp = useCallback((key: string, value: any) => {
actions.setProp(selectedId, (props: any) => { props[key] = value; });
}, [actions, selectedId]);
const setPropStyle = useCallback((key: string, value: string) => {
actions.setProp(selectedId, (props: any) => {
props.style = { ...props.style, [key]: value };
});
}, [actions, selectedId]);
const links: any[] = nodeProps.links || []; const links: any[] = nodeProps.links || [];
@@ -1,4 +1,4 @@
import React, { useCallback } from 'react'; import React from 'react';
import { useEditor } from '@craftjs/core'; import { useEditor } from '@craftjs/core';
import { import {
BG_COLORS, BG_COLORS,
@@ -17,22 +17,14 @@ import {
inputStyle, inputStyle,
smallInputStyle, smallInputStyle,
sectionGap, sectionGap,
useNodeProp,
} from './shared'; } from './shared';
import { FeaturesEditor } from './FeaturesEditor'; import { FeaturesEditor } from './FeaturesEditor';
/* ---------- SECTION-TYPE (Accordion, Tabs, Pricing, Testimonials, etc.) ---------- */ /* ---------- SECTION-TYPE (Accordion, Tabs, Pricing, Testimonials, etc.) ---------- */
export const SectionTypePanel: React.FC<StylePanelProps & { typeName: string }> = ({ selectedId, nodeProps, typeName }) => { export const SectionTypePanel: React.FC<StylePanelProps & { typeName: string }> = ({ selectedId, nodeProps, typeName }) => {
const { actions } = useEditor(); const { actions } = useEditor();
const { setProp, setPropStyle } = useNodeProp(selectedId);
const setProp = useCallback((key: string, value: any) => {
actions.setProp(selectedId, (props: any) => { props[key] = value; });
}, [actions, selectedId]);
const setPropStyle = useCallback((key: string, value: string) => {
actions.setProp(selectedId, (props: any) => {
props.style = { ...props.style, [key]: value };
});
}, [actions, selectedId]);
const style = nodeProps.style || {}; const style = nodeProps.style || {};
@@ -1,4 +1,4 @@
import React, { useCallback } from 'react'; import React from 'react';
import { useEditor } from '@craftjs/core'; import { useEditor } from '@craftjs/core';
import { import {
BG_COLORS, BG_COLORS,
@@ -16,21 +16,13 @@ import {
smallInputStyle, smallInputStyle,
btnActiveStyle, btnActiveStyle,
sectionGap, sectionGap,
useNodeProp,
} from './shared'; } from './shared';
/* ---------- SOCIAL / ICON / STAR RATING ---------- */ /* ---------- SOCIAL / ICON / STAR RATING ---------- */
export const SocialStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => { export const SocialStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => {
const { actions } = useEditor(); const { actions } = useEditor();
const { setProp, setPropStyle } = useNodeProp(selectedId);
const setProp = useCallback((key: string, value: any) => {
actions.setProp(selectedId, (props: any) => { props[key] = value; });
}, [actions, selectedId]);
const setPropStyle = useCallback((key: string, value: string) => {
actions.setProp(selectedId, (props: any) => {
props.style = { ...props.style, [key]: value };
});
}, [actions, selectedId]);
const style = nodeProps.style || {}; const style = nodeProps.style || {};
@@ -1,5 +1,4 @@
import React, { useCallback, CSSProperties } from 'react'; import React, { CSSProperties } from 'react';
import { useEditor } from '@craftjs/core';
import { import {
TEXT_COLORS, TEXT_COLORS,
FONT_FAMILIES, FONT_FAMILIES,
@@ -11,21 +10,14 @@ import {
SectionLabel, SectionLabel,
ColorSwatchGrid, ColorSwatchGrid,
PresetButtonGrid, PresetButtonGrid,
useNodeProp,
} from './shared'; } from './shared';
/* ---------- TEXT ---------- */ /* ---------- TEXT ---------- */
export const TextStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => { export const TextStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => {
const { actions } = useEditor();
const style: CSSProperties = nodeProps.style || {}; const style: CSSProperties = nodeProps.style || {};
const setPropStyle = useCallback( const { setPropStyle } = useNodeProp(selectedId);
(property: string, value: string) => {
actions.setProp(selectedId, (props: any) => {
props.style = { ...props.style, [property]: value };
});
},
[actions, selectedId],
);
return ( return (
<> <>
+12
View File
@@ -288,6 +288,18 @@ export interface StylePanelProps {
nodeProps: Record<string, any>; nodeProps: Record<string, any>;
} }
/* ---------- useNodeProp ----------
Shared setProp/setPropStyle boilerplate repeated across many *StylePanel.tsx
files. Only adopted where a panel's inline definitions were byte-equivalent
to this (some panels use different param names or extra logic and keep
their own local versions). */
export function useNodeProp(selectedId: string) {
const { actions } = useEditor();
const setProp = useCallback((key: string, value: any) => actions.setProp(selectedId, (p: any) => { p[key] = value; }), [actions, selectedId]);
const setPropStyle = useCallback((prop: string, value: string) => actions.setProp(selectedId, (p: any) => { p.style = { ...p.style, [prop]: value }; }), [actions, selectedId]);
return { setProp, setPropStyle };
}
/* ---------- Array Prop Editor (reusable for features, items, plans, etc.) ---------- */ /* ---------- Array Prop Editor (reusable for features, items, plans, etc.) ---------- */
interface ArrayPropEditorProps { interface ArrayPropEditorProps {
selectedId: string; selectedId: string;