fix(styles): NavStylePanel reads the per-device resolved style
Switches NavStylePanel from nodeProps.style to useResolvedStyle(nodeProps) and adds OverrideDots on Margin and Padding, the conversion #45 applied to the other panels. NavStylePanel has no style-based text-alignment control (Menu alignment is a top-level prop), so no dot there. Refs #45 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9697dc95a3
commit
d5bfb02911
2 files changed
+78
-3
No files matched your search
@@ -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<string, unknown>, nodes: string[] = [], extra: Record<string, unknown> = {}) => ({
|
||||
type: { resolvedName: type }, isCanvas: type === 'Container', props, displayName: type,
|
||||
custom: {}, hidden: false, nodes, linkedNodes: {}, ...extra,
|
||||
});
|
||||
const state = (extra: Record<string, unknown> = {}) => 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 <NavStylePanel selectedId="logo" nodeProps={nodeProps} />;
|
||||
};
|
||||
function mount(device: DeviceMode, initialState = state()) {
|
||||
h = renderEditorHarness({ initialState, wrap: (t) => <DeviceProvider initialDevice={device}>{t}</DeviceProvider> });
|
||||
h.mountChild(<Host />);
|
||||
}
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -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<StylePanelProps> = ({ 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<string, any>;
|
||||
const dot = (key: string | readonly string[]) => <OverrideDot nodeProps={nodeProps} selectedId={selectedId} styleKey={key} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -544,12 +549,12 @@ export const NavStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps
|
||||
previous behavior exactly). */}
|
||||
<CollapsibleSection title="Spacing">
|
||||
<SpacingControl
|
||||
label="Margin"
|
||||
label={<>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)}
|
||||
/>
|
||||
<SpacingControl
|
||||
label="Padding"
|
||||
label={<>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}
|
||||
|
||||
Reference in new issue
Block a user