diff --git a/craft/src/panels/right/styles/NavStylePanel.responsive.test.tsx b/craft/src/panels/right/styles/NavStylePanel.responsive.test.tsx new file mode 100644 index 0000000..26e9ae6 --- /dev/null +++ b/craft/src/panels/right/styles/NavStylePanel.responsive.test.tsx @@ -0,0 +1,70 @@ +import { describe, test, expect, afterEach } from 'vitest'; +import React from 'react'; +import { useEditor } from '@craftjs/core'; +import { renderEditorHarness, EditorHarness } from '../../../test-utils/editorHarness'; +import { DeviceProvider } from '../../../state/DeviceContext'; +import type { DeviceMode } from '../../../types'; +import { NavStylePanel } from './NavStylePanel'; +import { closeContinuousBurst } from './shared'; + +/** + * Integration follow-up (#45 review): NavStylePanel reads the per-device + * resolved style and shows override dots on Margin / Padding, the same + * conversion #45 applied to the other panels. + */ +let h: EditorHarness | null = null; +afterEach(() => { closeContinuousBurst(); h?.unmount(); h = null; }); + +const node = (type: string, props: Record, nodes: string[] = [], extra: Record = {}) => ({ + type: { resolvedName: type }, isCanvas: type === 'Container', props, displayName: type, + custom: {}, hidden: false, nodes, linkedNodes: {}, ...extra, +}); +const state = (extra: Record = {}) => JSON.stringify({ + ROOT: node('Container', { tag: 'div', style: {} }, ['logo']), + logo: node('Logo', { + type: 'text', text: 'Acme', + style: { marginTop: '0', marginRight: '0', marginBottom: '0', marginLeft: '0', paddingTop: '8px', paddingRight: '8px', paddingBottom: '8px', paddingLeft: '8px' }, + ...extra, + }, [], { parent: 'ROOT' }), +}); + +const Host: React.FC = () => { + const { nodeProps } = useEditor((s) => ({ nodeProps: s.nodes.logo?.data.props || {} })); + return ; +}; +function mount(device: DeviceMode, initialState = state()) { + h = renderEditorHarness({ initialState, wrap: (t) => {t} }); + h.mountChild(); +} +const props = () => h!.query.node('logo').get().data.props; +const control = (label: string) => + Array.from(h!.container.querySelectorAll('[data-testid="spacing-control"]')) + .find((el) => (el.querySelector('.guided-section-label')?.textContent || '').startsWith(label)) as HTMLElement; +const active = (label: string) => control(label).querySelector('button.preset-btn.active')?.textContent ?? null; +const click = (el: Element) => h!.act(() => { (el as HTMLElement).click(); }); + +describe('NavStylePanel per-device spacing (#45 follow-up)', () => { + test('mobile reads the mobile override, not the desktop style', () => { + mount('mobile', state({ styleMobile: { marginTop: '16px', marginRight: '16px', marginBottom: '16px', marginLeft: '16px' } })); + expect(active('Margin')).toBe('M'); + expect(control('Margin').querySelector('[data-testid="override-dot"]')).not.toBeNull(); + expect(control('Padding').querySelector('[data-testid="override-dot-inherited"]')).not.toBeNull(); + }); + + test('a mobile Margin edit lands in styleMobile, reads back, and its dot resets it', () => { + mount('mobile'); + expect(active('Margin')).toBe('None'); + click(Array.from(control('Margin').querySelectorAll('button.preset-btn')).find((b) => b.textContent === 'M')!); + expect(props().styleMobile?.marginTop).toBe('16px'); + expect(props().style.marginTop).toBe('0'); + expect(active('Margin')).toBe('M'); + click(control('Margin').querySelector('[data-testid="override-dot"]')!); + expect(props().styleMobile?.marginTop).toBeUndefined(); + expect(active('Margin')).toBe('None'); + }); + + test('desktop shows no dots', () => { + mount('desktop', state({ styleMobile: { marginTop: '16px' } })); + expect(h!.container.querySelector('.wsb-override-dot')).toBeNull(); + }); +}); diff --git a/craft/src/panels/right/styles/NavStylePanel.tsx b/craft/src/panels/right/styles/NavStylePanel.tsx index 5209d75..da9fd9b 100644 --- a/craft/src/panels/right/styles/NavStylePanel.tsx +++ b/craft/src/panels/right/styles/NavStylePanel.tsx @@ -26,6 +26,8 @@ import { continuousSetProp, discreteSetProp, } from './shared'; +import { useResolvedStyle, MARGIN_KEYS, PADDING_KEYS } from './responsive-panel'; +import { OverrideDot } from './OverrideDot'; import { AssetPicker } from '../../../ui/AssetPicker'; import { usePages } from '../../../state/PageContext'; import { PageData } from '../../../types'; @@ -329,7 +331,10 @@ export const NavStylePanel: React.FC = ({ selectedId, nodeProps component's .craft.props), so -- unlike the Links/Colors sections above, which are shared across a genuinely disparate prop schema -- these sections render unconditionally rather than gating on presence. */ - const style = nodeProps.style || {}; + // #45: the per-device resolved style (desktop ⊕ tablet ⊕ mobile), so a + // tablet/mobile edit reads back what it wrote, plus override dots. + const style = useResolvedStyle(nodeProps) as Record; + const dot = (key: string | readonly string[]) => ; return ( <> @@ -544,12 +549,12 @@ export const NavStylePanel: React.FC = ({ selectedId, nodeProps previous behavior exactly). */} Margin{dot(MARGIN_KEYS)}} value={{ top: style.marginTop, right: style.marginRight, bottom: style.marginBottom, left: style.marginLeft }} onChange={(side, v) => setPropStyleContinuous(`margin${capitalize(side)}`, v)} /> Padding{dot(PADDING_KEYS)}} value={{ top: style.paddingTop, right: style.paddingRight, bottom: style.paddingBottom, left: style.paddingLeft }} onChange={(side, v) => setPropStyleContinuous(`padding${capitalize(side)}`, v)} presets={SPACING_PRESETS}