Files
site-builder/craft/src/panels/right/styles/FeaturesEditor.tsx
T

79 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useRef } from 'react';
import { useEditor } from '@craftjs/core';
import { labelStyle, inputStyle, sectionGap, continuousSetProp, discreteSetProp } from './shared';
import { AssetPicker } from '../../../ui/AssetPicker';
import { useLayerFocus } from '../../left/LayerFocusContext';
interface Feature {
title?: string; description?: string; icon?: string;
image?: string; imageAlt?: string; buttonText?: string; buttonUrl?: string;
}
/**
* Dedicated per-feature editor for the FeaturesGrid guided panel. Unlike the
* generic array editor (which only shows fields already present on the item),
* this always exposes icon / image (upload or URL) / button controls, so it
* works on template-created features that lack those keys. An image renders in
* place of the icon whenever `image` is set.
*/
export const FeaturesEditor: React.FC<{ selectedId: string; features: unknown }> = ({ selectedId, features }) => {
const { actions } = useEditor();
const list: Feature[] = Array.isArray(features) ? (features as Feature[]) : [];
const mutate = (fn: (arr: Feature[]) => Feature[]) =>
discreteSetProp(actions, selectedId, (p: any) => { p.features = fn([...(p.features || [])]); });
// Field edits are typing bursts: one undo step per field per burst.
const update = (i: number, field: keyof Feature, value: string) =>
continuousSetProp(actions, selectedId, `${i}:${field}`, (p: any) => {
const arr = [...(p.features || [])]; arr[i] = { ...arr[i], [field]: value }; p.features = arr;
});
const add = () =>
mutate((arr) => [...arr, { title: 'New Feature', description: 'Describe this feature.', icon: '🔧', image: '', imageAlt: '', buttonText: '', buttonUrl: '' }]);
const remove = (i: number) => mutate((arr) => { arr.splice(i, 1); return arr; });
// Layers panel -> array editor "scroll to this item" hookup. See
// ArrayItemFields.tsx for the generic-editor counterpart; this component
// keeps its own per-feature cards, so it tags/scrolls them directly.
const { focus } = useLayerFocus();
const rootRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!focus || focus.prop !== 'features') return;
const card = rootRef.current?.querySelector(`[data-array-item="features:${focus.index}"]`);
card?.scrollIntoView?.({ block: 'nearest', behavior: 'smooth' });
// `focus.nonce` is in the dep list so clicking the SAME row twice
// re-scrolls (the request object is otherwise identical).
}, [focus?.nonce, focus?.prop, focus?.index]);
return (
<div ref={rootRef} style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{list.map((feat, i) => (
<div key={i} data-array-item={`features:${i}`} style={{ background: '#1e1e22', borderRadius: 6, padding: 8, display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
<input type="text" value={feat.title || ''} onChange={(e) => update(i, 'title', e.target.value)} placeholder="Title" style={{ ...inputStyle, flex: 1 }} />
<button onClick={() => remove(i)} title="Remove" style={{ padding: '2px 8px', fontSize: 11, background: '#ef4444', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer', flex: 'none' }}>×</button>
</div>
<textarea value={feat.description || ''} onChange={(e) => update(i, 'description', e.target.value)} placeholder="Description" rows={2} style={{ ...inputStyle, resize: 'vertical' }} />
{/* Media: image (upload/URL) takes precedence over the icon when set */}
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
<input type="text" value={feat.icon || ''} onChange={(e) => update(i, 'icon', e.target.value)} placeholder="Icon (emoji)" style={{ ...inputStyle, width: 60, flex: 'none', textAlign: 'center' }} />
<div style={{ flex: 1 }}>
<AssetPicker variant="compact" value={feat.image || ''} onChange={(url) => update(i, 'image', url)} placeholder="or image URL" />
</div>
</div>
{/* Optional button */}
<div style={{ display: 'flex', gap: 4 }}>
<input type="text" value={feat.buttonText || ''} onChange={(e) => update(i, 'buttonText', e.target.value)} placeholder="Button text (optional)" style={{ ...inputStyle, flex: 1 }} />
<input type="text" value={feat.buttonUrl || ''} onChange={(e) => update(i, 'buttonUrl', e.target.value)} placeholder="Button URL" style={{ ...inputStyle, flex: 1 }} />
</div>
</div>
))}
<button onClick={add} style={{ ...labelStyle, padding: '6px', fontSize: 11, background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer', textAlign: 'center' }}>
+ Add Feature
</button>
</div>
);
};