diff --git a/craft/src/panels/right/styles/FormStylePanel.tsx b/craft/src/panels/right/styles/FormStylePanel.tsx index 6905b16..17380eb 100644 --- a/craft/src/panels/right/styles/FormStylePanel.tsx +++ b/craft/src/panels/right/styles/FormStylePanel.tsx @@ -28,6 +28,8 @@ import { useNodeProp, continuousSetProp, } from './shared'; +import { useResolvedStyle, MARGIN_KEYS, PADDING_KEYS } from './responsive-panel'; +import { OverrideDot } from './OverrideDot'; /* The full sanitizeInputType (utils/escape.ts) allowlist, plus the two fake "types" (textarea/select) that take their own ContactForm render @@ -175,7 +177,10 @@ export const FormStylePanel: React.FC = ({ selectedId, nodeProp const { actions } = useEditor(); const { setProp, setPropStyle, setProps, setPropContinuous, setPropStyleContinuous } = useNodeProp(selectedId); - const style = nodeProps.style || {}; + // Effective value for the current device (#45): the setters write + // tablet/mobile overrides, so the fields must read them back. + const style = useResolvedStyle(nodeProps) as Record; + const dot = (key: string | readonly string[]) => ; // ContactForm only: FormContainer/SubscribeForm have no destinationType prop, // so their relay controls stay exactly as they were. const isWebhook = nodeProps.destinationType === 'webhook'; @@ -484,7 +489,7 @@ export const FormStylePanel: React.FC = ({ selectedId, nodeProp setPropStyle('backgroundColor', v)} />
- Padding + Padding{dot(PADDING_KEYS)} setPropStyle('padding', v)} />
@@ -498,12 +503,12 @@ export const FormStylePanel: React.FC = ({ selectedId, nodeProp they all spread `style` onto their root element. */} Margin{dot(MARGIN_KEYS)}} value={{ top: style.marginTop, right: style.marginRight, bottom: style.marginBottom, left: style.marginLeft }} onChange={(side, v) => setPropStyleContinuous(`margin${SPACING_SIDE_KEYS.find((s) => s.side === side)!.suffix}`, v)} /> Padding (per side){dot(PADDING_KEYS)}} value={{ top: style.paddingTop, right: style.paddingRight, bottom: style.paddingBottom, left: style.paddingLeft }} onChange={(side, v) => setPropStyleContinuous(`padding${SPACING_SIDE_KEYS.find((s) => s.side === side)!.suffix}`, v)} /> diff --git a/craft/src/panels/right/styles/GenericPropsEditor.tsx b/craft/src/panels/right/styles/GenericPropsEditor.tsx index 8cd48b4..f056f3b 100644 --- a/craft/src/panels/right/styles/GenericPropsEditor.tsx +++ b/craft/src/panels/right/styles/GenericPropsEditor.tsx @@ -16,6 +16,8 @@ import { sectionGap, useNodeProp, } from './shared'; +import { useResolvedStyle, PADDING_KEYS } from './responsive-panel'; +import { OverrideDot } from './OverrideDot'; import { ArrayItemFieldsEditor } from './ArrayItemFields'; import { HtmlCodeField } from './HtmlCodeField'; @@ -35,7 +37,10 @@ export const GenericPropsEditor: React.FC<{ selectedId: string; nodeProps: Recor const stringProps = allProps.filter(([key, val]) => typeof val === 'string' && !/color/i.test(key)); const arrayProps = allProps.filter(([_, val]) => Array.isArray(val)); - const style = nodeProps.style || {}; + // Effective value for the current device (#45): the setters write + // tablet/mobile overrides, so the fields must read them back. + const style = useResolvedStyle(nodeProps) as Record; + const dot = (key: string | readonly string[]) => ; const hasCodeProp = typeof nodeProps.code === 'string'; return ( @@ -117,11 +122,11 @@ export const GenericPropsEditor: React.FC<{ selectedId: string; nodeProps: Recor setStyleValue('color', v)} />
- Padding + Padding{dot(PADDING_KEYS)} setStyleValue('padding', v)} />
- Text Alignment + Text Alignment{dot('textAlign')}
{(['left', 'center', 'right'] as const).map((a) => (
- Padding + Padding{dot(PADDING_KEYS)} setPropStyle('padding', v)} />
- Text Alignment + Text Alignment{dot('textAlign')}
{(['left', 'center', 'right'] as const).map((a) => (
- Padding + Padding{dot(PADDING_KEYS)} setPropStyle('padding', v)} />
@@ -261,7 +266,7 @@ export const SocialStylePanel: React.FC = ({ selectedId, nodePr {/* Box model: margin, padding (per-side), border, shadow, opacity */} Margin{dot(MARGIN_KEYS)}} value={{ top: style.marginTop as string, right: style.marginRight as string, bottom: style.marginBottom as string, left: style.marginLeft as string, @@ -269,7 +274,7 @@ export const SocialStylePanel: React.FC = ({ selectedId, nodePr onChange={(side, v) => setPropStyleContinuous(`margin${capSide(side)}`, v)} /> Padding{dot(PADDING_KEYS)}} value={{ top: style.paddingTop as string, right: style.paddingRight as string, bottom: style.paddingBottom as string, left: style.paddingLeft as string, diff --git a/craft/src/panels/right/styles/responsive-panel.test.tsx b/craft/src/panels/right/styles/responsive-panel.test.tsx index 659d9c4..ad6de5f 100644 --- a/craft/src/panels/right/styles/responsive-panel.test.tsx +++ b/craft/src/panels/right/styles/responsive-panel.test.tsx @@ -9,6 +9,11 @@ import type { DeviceMode } from '../../../types'; import { GuidedStyles } from '../GuidedStyles'; import { TextStylePanel } from './TextStylePanel'; import { ContainerStylePanel } from './ContainerStylePanel'; +import { SocialStylePanel } from './SocialStylePanel'; +import { SectionTypePanel } from './SectionTypePanel'; +import { PricingStylePanel } from './PricingStylePanel'; +import { FormStylePanel } from './FormStylePanel'; +import { GenericPropsEditor } from './GenericPropsEditor'; import { useNodeProp, closeContinuousBurst, @@ -315,6 +320,61 @@ describe('DeviceBanner in GuidedStyles', () => { }); }); +describe('every useNodeProp panel reads back what it writes per device (fix round 1)', () => { + // Panels that edit responsive keys through the device-aware setters must + // show the RESOLVED value and a dot, or a mobile edit looks like a no-op. + const blockState = () => + JSON.stringify({ + ROOT: node('Container', { tag: 'div', style: {} }, ['blk']), + blk: node('Container', { tag: 'div', style: { padding: '8px', marginTop: '0', marginRight: '0', marginBottom: '0', marginLeft: '0', textAlign: 'left' } }, [], { parent: 'ROOT' }), + }); + const withType = (P: React.FC, typeName: string): React.FC => (p) =>

; + const panels: [string, React.FC, { preset: boolean; spacing: boolean; align?: string }][] = [ + ['SocialStylePanel', SocialStylePanel, { preset: true, spacing: true }], + ['SectionTypePanel', withType(SectionTypePanel, 'Accordion'), { preset: true, spacing: true, align: 'Text Alignment' }], + ['PricingStylePanel', PricingStylePanel, { preset: false, spacing: true }], + ['FormStylePanel', FormStylePanel, { preset: true, spacing: true }], + ['GenericPropsEditor', withType(GenericPropsEditor, 'Thing'), { preset: true, spacing: false, align: 'Text Alignment' }], + ]; + + const marginControl = () => + Array.from(h!.container.querySelectorAll('[data-testid="spacing-control"]')) + .find((el) => (el.querySelector('.guided-section-label')?.textContent || '').startsWith('Margin')) as HTMLElement; + + test.each(panels)('%s: mobile edits land in styleMobile and the panel shows them with a dot', (_n, Panel, has) => { + mountPanel('mobile', Panel, 'blk', blockState()); + if (has.preset) { + expect(activePreset('Padding')).toBe('S'); + expect(hollowDot('Padding')).not.toBeNull(); + click(presetBtn('Padding', 'L')); + expect(props('blk').styleMobile?.padding).toBe('24px'); + expect(props('blk').style.padding).toBe('8px'); + expect(activePreset('Padding')).toBe('L'); + expect(filledDot('Padding')).not.toBeNull(); + } + if (has.spacing) { + const m = marginControl(); + expect(m.querySelector('button.preset-btn.active')?.textContent).toBe('None'); + click(Array.from(m.querySelectorAll('button.preset-btn')).find((b) => b.textContent === 'M')!); + expect(props('blk').styleMobile?.marginTop).toBe('16px'); + expect(props('blk').style.marginTop).toBe('0'); + expect(marginControl().querySelector('button.preset-btn.active')?.textContent).toBe('M'); + const dot = marginControl().querySelector('[data-testid="override-dot"]') as HTMLElement; + expect(dot).not.toBeNull(); + click(dot); + expect(props('blk').styleMobile?.marginTop).toBeUndefined(); + expect(marginControl().querySelector('button.preset-btn.active')?.textContent).toBe('None'); + } + if (has.align) { + click(section(has.align).querySelector('button[title="center"]')!); + expect(props('blk').styleMobile?.textAlign).toBe('center'); + expect(props('blk').style.textAlign).toBe('left'); + expect(section(has.align).querySelector('button.preset-btn.active')?.getAttribute('title')).toBe('center'); + expect(filledDot(has.align)).not.toBeNull(); + } + }); +}); + describe('end to end: export', () => { test('a mobile font-size edit exports as a scoped max-width:767px rule', () => { mountPanel('mobile');