340 lines
12 KiB
TypeScript
340 lines
12 KiB
TypeScript
|
|
import React, { CSSProperties, useState } from 'react';
|
||
|
|
import { useNode, UserComponent } from '@craftjs/core';
|
||
|
|
import { cssPropsToString } from '../../utils/style-helpers';
|
||
|
|
|
||
|
|
interface TabItem {
|
||
|
|
label: string;
|
||
|
|
content: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface TabsProps {
|
||
|
|
tabs?: TabItem[];
|
||
|
|
style?: CSSProperties;
|
||
|
|
activeTabBg?: string;
|
||
|
|
activeTabColor?: string;
|
||
|
|
inactiveTabBg?: string;
|
||
|
|
inactiveTabColor?: string;
|
||
|
|
contentBg?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
const defaultTabs: TabItem[] = [
|
||
|
|
{ label: 'Overview', content: 'Welcome to our platform. We provide the tools you need to build, launch, and grow your online presence. Our intuitive interface makes it simple to get started in minutes.' },
|
||
|
|
{ label: 'Features', content: 'Drag-and-drop editor, responsive templates, custom domains, analytics dashboard, SEO tools, and integrations with your favorite services. Everything you need in one place.' },
|
||
|
|
{ label: 'Support', content: 'Our dedicated support team is available 24/7 to help you with any questions. Access our knowledge base, community forums, or reach out directly via live chat or email.' },
|
||
|
|
];
|
||
|
|
|
||
|
|
export const Tabs: UserComponent<TabsProps> = ({
|
||
|
|
tabs = defaultTabs,
|
||
|
|
style = {},
|
||
|
|
activeTabBg = '#3b82f6',
|
||
|
|
activeTabColor = '#ffffff',
|
||
|
|
inactiveTabBg = '#f1f5f9',
|
||
|
|
inactiveTabColor = '#64748b',
|
||
|
|
contentBg = '#ffffff',
|
||
|
|
}) => {
|
||
|
|
const {
|
||
|
|
connectors: { connect, drag },
|
||
|
|
selected,
|
||
|
|
} = useNode((node) => ({
|
||
|
|
selected: node.events.selected,
|
||
|
|
}));
|
||
|
|
|
||
|
|
const [activeIndex, setActiveIndex] = useState(0);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<section
|
||
|
|
ref={(ref: HTMLElement | null): void => { if (ref) connect(drag(ref)); }}
|
||
|
|
style={{
|
||
|
|
padding: '60px 20px',
|
||
|
|
backgroundColor: '#ffffff',
|
||
|
|
outline: selected ? '2px solid #3b82f6' : 'none',
|
||
|
|
...style,
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<div style={{ maxWidth: '800px', margin: '0 auto' }}>
|
||
|
|
{/* Tab buttons */}
|
||
|
|
<div style={{ display: 'flex', gap: '2px', borderBottom: '2px solid #e2e8f0' }}>
|
||
|
|
{tabs.map((tab, i) => (
|
||
|
|
<button
|
||
|
|
key={i}
|
||
|
|
onClick={() => setActiveIndex(i)}
|
||
|
|
style={{
|
||
|
|
padding: '12px 24px',
|
||
|
|
fontSize: '14px',
|
||
|
|
fontWeight: '600',
|
||
|
|
border: 'none',
|
||
|
|
borderTopLeftRadius: '8px',
|
||
|
|
borderTopRightRadius: '8px',
|
||
|
|
cursor: 'pointer',
|
||
|
|
backgroundColor: i === activeIndex ? activeTabBg : inactiveTabBg,
|
||
|
|
color: i === activeIndex ? activeTabColor : inactiveTabColor,
|
||
|
|
transition: 'background-color 0.2s, color 0.2s',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{tab.label}
|
||
|
|
</button>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
{/* Content panel */}
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
padding: '24px',
|
||
|
|
backgroundColor: contentBg,
|
||
|
|
border: '1px solid #e2e8f0',
|
||
|
|
borderTop: 'none',
|
||
|
|
borderBottomLeftRadius: '8px',
|
||
|
|
borderBottomRightRadius: '8px',
|
||
|
|
fontSize: '14px',
|
||
|
|
lineHeight: '1.7',
|
||
|
|
color: '#4b5563',
|
||
|
|
minHeight: '100px',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{tabs[activeIndex]?.content || ''}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
/* ---------- Settings panel ---------- */
|
||
|
|
|
||
|
|
const TabsSettings: React.FC = () => {
|
||
|
|
const { actions: { setProp }, props } = useNode((node) => ({
|
||
|
|
props: node.data.props as TabsProps,
|
||
|
|
}));
|
||
|
|
|
||
|
|
const tabs = props.tabs || defaultTabs;
|
||
|
|
|
||
|
|
const inputStyle: CSSProperties = {
|
||
|
|
width: '100%', padding: '3px 6px', background: '#27272a', color: '#e4e4e7',
|
||
|
|
border: '1px solid #3f3f46', borderRadius: 4, fontSize: 11,
|
||
|
|
};
|
||
|
|
|
||
|
|
const labelStyle: CSSProperties = { fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 };
|
||
|
|
|
||
|
|
const updateTab = (index: number, field: keyof TabItem, value: string) => {
|
||
|
|
setProp((p: TabsProps) => {
|
||
|
|
const updated = [...(p.tabs || defaultTabs)];
|
||
|
|
updated[index] = { ...updated[index], [field]: value };
|
||
|
|
p.tabs = updated;
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
const addTab = () => {
|
||
|
|
setProp((p: TabsProps) => {
|
||
|
|
p.tabs = [...(p.tabs || defaultTabs), { label: 'New Tab', content: 'Tab content goes here.' }];
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
const removeTab = (index: number) => {
|
||
|
|
setProp((p: TabsProps) => {
|
||
|
|
const updated = [...(p.tabs || defaultTabs)];
|
||
|
|
updated.splice(index, 1);
|
||
|
|
p.tabs = updated;
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
const colorSwatches = ['#3b82f6', '#8b5cf6', '#10b981', '#f59e0b', '#ef4444', '#18181b', '#ffffff', '#f1f5f9'];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div style={{ padding: '12px', display: 'flex', flexDirection: 'column', gap: '14px' }}>
|
||
|
|
<div>
|
||
|
|
<label style={labelStyle}>Active Tab Background</label>
|
||
|
|
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||
|
|
{colorSwatches.map((c) => (
|
||
|
|
<button
|
||
|
|
key={c}
|
||
|
|
onClick={() => setProp((p: TabsProps) => { p.activeTabBg = c; })}
|
||
|
|
style={{
|
||
|
|
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||
|
|
backgroundColor: c, cursor: 'pointer',
|
||
|
|
outline: props.activeTabBg === c ? '2px solid #3b82f6' : 'none',
|
||
|
|
outlineOffset: 1,
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label style={labelStyle}>Active Tab Text Color</label>
|
||
|
|
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||
|
|
{['#ffffff', '#18181b', '#1f2937', '#e2e8f0'].map((c) => (
|
||
|
|
<button
|
||
|
|
key={c}
|
||
|
|
onClick={() => setProp((p: TabsProps) => { p.activeTabColor = c; })}
|
||
|
|
style={{
|
||
|
|
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||
|
|
backgroundColor: c, cursor: 'pointer',
|
||
|
|
outline: props.activeTabColor === c ? '2px solid #3b82f6' : 'none',
|
||
|
|
outlineOffset: 1,
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label style={labelStyle}>Inactive Tab Background</label>
|
||
|
|
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||
|
|
{['#f1f5f9', '#e2e8f0', '#f8fafc', '#ffffff', '#27272a', '#18181b'].map((c) => (
|
||
|
|
<button
|
||
|
|
key={c}
|
||
|
|
onClick={() => setProp((p: TabsProps) => { p.inactiveTabBg = c; })}
|
||
|
|
style={{
|
||
|
|
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||
|
|
backgroundColor: c, cursor: 'pointer',
|
||
|
|
outline: props.inactiveTabBg === c ? '2px solid #3b82f6' : 'none',
|
||
|
|
outlineOffset: 1,
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label style={labelStyle}>Inactive Tab Text Color</label>
|
||
|
|
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||
|
|
{['#64748b', '#94a3b8', '#18181b', '#ffffff'].map((c) => (
|
||
|
|
<button
|
||
|
|
key={c}
|
||
|
|
onClick={() => setProp((p: TabsProps) => { p.inactiveTabColor = c; })}
|
||
|
|
style={{
|
||
|
|
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||
|
|
backgroundColor: c, cursor: 'pointer',
|
||
|
|
outline: props.inactiveTabColor === c ? '2px solid #3b82f6' : 'none',
|
||
|
|
outlineOffset: 1,
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label style={labelStyle}>Content Background</label>
|
||
|
|
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||
|
|
{['#ffffff', '#f8fafc', '#f1f5f9', '#18181b', '#1e293b'].map((c) => (
|
||
|
|
<button
|
||
|
|
key={c}
|
||
|
|
onClick={() => setProp((p: TabsProps) => { p.contentBg = c; })}
|
||
|
|
style={{
|
||
|
|
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||
|
|
backgroundColor: c, cursor: 'pointer',
|
||
|
|
outline: props.contentBg === c ? '2px solid #3b82f6' : 'none',
|
||
|
|
outlineOffset: 1,
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label style={labelStyle}>Tabs</label>
|
||
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||
|
|
{tabs.map((tab, i) => (
|
||
|
|
<div key={i} style={{ background: '#1e1e22', borderRadius: 6, padding: 8, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||
|
|
<div style={{ display: 'flex', gap: 4 }}>
|
||
|
|
<input type="text" value={tab.label} onChange={(e) => updateTab(i, 'label', e.target.value)} placeholder="Label" style={{ ...inputStyle, flex: 1 }} />
|
||
|
|
<button
|
||
|
|
onClick={() => removeTab(i)}
|
||
|
|
style={{ padding: '2px 6px', fontSize: 11, background: '#ef4444', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer' }}
|
||
|
|
>
|
||
|
|
X
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<textarea
|
||
|
|
value={tab.content}
|
||
|
|
onChange={(e) => updateTab(i, 'content', e.target.value)}
|
||
|
|
placeholder="Content"
|
||
|
|
rows={2}
|
||
|
|
style={{ ...inputStyle, resize: 'vertical' }}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
<button
|
||
|
|
onClick={addTab}
|
||
|
|
style={{ marginTop: 6, width: '100%', padding: '6px', fontSize: 11, background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer' }}
|
||
|
|
>
|
||
|
|
+ Add Tab
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
/* ---------- Craft config ---------- */
|
||
|
|
|
||
|
|
Tabs.craft = {
|
||
|
|
displayName: 'Tabs',
|
||
|
|
props: {
|
||
|
|
tabs: defaultTabs,
|
||
|
|
style: { backgroundColor: '#ffffff' },
|
||
|
|
activeTabBg: '#3b82f6',
|
||
|
|
activeTabColor: '#ffffff',
|
||
|
|
inactiveTabBg: '#f1f5f9',
|
||
|
|
inactiveTabColor: '#64748b',
|
||
|
|
contentBg: '#ffffff',
|
||
|
|
},
|
||
|
|
rules: {
|
||
|
|
canDrag: () => true,
|
||
|
|
canMoveIn: () => false,
|
||
|
|
canMoveOut: () => true,
|
||
|
|
},
|
||
|
|
related: {
|
||
|
|
settings: TabsSettings,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
/* ---------- HTML export ---------- */
|
||
|
|
|
||
|
|
(Tabs as any).toHtml = (props: TabsProps, _childrenHtml: string) => {
|
||
|
|
const esc = (s: string) => s.replace(/</g, '<').replace(/>/g, '>');
|
||
|
|
const sectionStyle = cssPropsToString({
|
||
|
|
padding: '60px 20px',
|
||
|
|
...props.style,
|
||
|
|
});
|
||
|
|
const tabs = props.tabs || defaultTabs;
|
||
|
|
const activeTabBg = props.activeTabBg || '#3b82f6';
|
||
|
|
const activeTabColor = props.activeTabColor || '#ffffff';
|
||
|
|
const inactiveTabBg = props.inactiveTabBg || '#f1f5f9';
|
||
|
|
const inactiveTabColor = props.inactiveTabColor || '#64748b';
|
||
|
|
const contentBg = props.contentBg || '#ffffff';
|
||
|
|
|
||
|
|
const tabId = 'tabs_' + Math.random().toString(36).slice(2, 8);
|
||
|
|
|
||
|
|
const tabButtons = tabs.map((tab, i) => {
|
||
|
|
const isActive = i === 0;
|
||
|
|
return `<button onclick="${tabId}_switch(${i})" id="${tabId}_btn_${i}" style="padding:12px 24px;font-size:14px;font-weight:600;border:none;border-top-left-radius:8px;border-top-right-radius:8px;cursor:pointer;background-color:${isActive ? activeTabBg : inactiveTabBg};color:${isActive ? activeTabColor : inactiveTabColor}">${esc(tab.label)}</button>`;
|
||
|
|
}).join('\n ');
|
||
|
|
|
||
|
|
const tabPanels = tabs.map((tab, i) => {
|
||
|
|
return `<div id="${tabId}_panel_${i}" style="padding:24px;background-color:${contentBg};border:1px solid #e2e8f0;border-top:none;border-bottom-left-radius:8px;border-bottom-right-radius:8px;font-size:14px;line-height:1.7;color:#4b5563;min-height:100px;${i !== 0 ? 'display:none' : ''}">${esc(tab.content)}</div>`;
|
||
|
|
}).join('\n ');
|
||
|
|
|
||
|
|
const switchScript = `<script>
|
||
|
|
function ${tabId}_switch(idx){
|
||
|
|
var total=${tabs.length};
|
||
|
|
for(var i=0;i<total;i++){
|
||
|
|
document.getElementById('${tabId}_panel_'+i).style.display=i===idx?'':'none';
|
||
|
|
var btn=document.getElementById('${tabId}_btn_'+i);
|
||
|
|
btn.style.backgroundColor=i===idx?'${activeTabBg}':'${inactiveTabBg}';
|
||
|
|
btn.style.color=i===idx?'${activeTabColor}':'${inactiveTabColor}';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</script>`;
|
||
|
|
|
||
|
|
return {
|
||
|
|
html: `<section${sectionStyle ? ` style="${sectionStyle}"` : ''}>
|
||
|
|
<div style="max-width:800px;margin:0 auto">
|
||
|
|
<div style="display:flex;gap:2px;border-bottom:2px solid #e2e8f0">
|
||
|
|
${tabButtons}
|
||
|
|
</div>
|
||
|
|
${tabPanels}
|
||
|
|
</div>
|
||
|
|
${switchScript}
|
||
|
|
</section>`,
|
||
|
|
};
|
||
|
|
};
|