fix: commands sidebar

This commit is contained in:
Alexander Medvedev 2026-08-31 21:23:46 +02:00
parent 21f54d9274
commit 35e34b36f9
4 changed files with 174 additions and 166 deletions

View File

@ -26,7 +26,6 @@
"i18next": "^26.4.0", "i18next": "^26.4.0",
"i18next-browser-languagedetector": "^8.2.1", "i18next-browser-languagedetector": "^8.2.1",
"lucide-react": "^1.37.0", "lucide-react": "^1.37.0",
"or": "^0.2.0",
"qrcode.react": "^4.2.0", "qrcode.react": "^4.2.0",
"react": "^19.2.8", "react": "^19.2.8",
"react-dom": "^19.2.8", "react-dom": "^19.2.8",

View File

@ -202,173 +202,176 @@ const PluginCommandsSidebarWidget = ({ commands }: { commands?: PluginCommand[]
const visibleCommands = expanded ? commands : commands.slice(0, 4); const visibleCommands = expanded ? commands : commands.slice(0, 4);
return ( return (
<div className="sidebar-widget commands-widget" style={{ marginTop: '1.25rem', position: 'static' }}> <>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '0.85rem' }}> <div className="divider-sm" />
<h3 style={{ margin: 0, display: 'flex', alignItems: 'center', gap: '0.45rem', fontSize: '0.82rem' }}> <div className="commands-widget-section">
<Terminal size={14} color="var(--primary)" /> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '0.85rem' }}>
{t('plugin.commands.title', 'Commands & Permissions')} <h3 style={{ margin: 0, display: 'flex', alignItems: 'center', gap: '0.45rem', fontSize: '0.82rem' }}>
</h3> <Terminal size={14} color="var(--primary)" />
<span style={{ {t('plugin.commands.title', 'Commands & Permissions')}
fontSize: '0.7rem', </h3>
fontWeight: 700, <span style={{
background: 'rgba(255, 255, 255, 0.07)', fontSize: '0.7rem',
padding: '2px 7px', fontWeight: 700,
borderRadius: '999px', background: 'rgba(255, 255, 255, 0.07)',
color: 'var(--text-muted)' padding: '2px 7px',
}}> borderRadius: '999px',
{commands.length} color: 'var(--text-muted)'
</span> }}>
</div> {commands.length}
</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{visibleCommands.map((cmd: any, index: number) => { {visibleCommands.map((cmd: any, index: number) => {
const desc = getLocalizedCommandDescription(cmd.description, activeLang); const desc = getLocalizedCommandDescription(cmd.description, activeLang);
const cmdCopyKey = `cmd-${cmd.id || index}`; const cmdCopyKey = `cmd-${cmd.id || index}`;
const permCopyKey = `perm-${cmd.id || index}`; const permCopyKey = `perm-${cmd.id || index}`;
const formattedName = cmd.name?.startsWith('/') ? cmd.name : `/${cmd.name}`; const formattedName = cmd.name?.startsWith('/') ? cmd.name : `/${cmd.name}`;
return ( return (
<div <div
key={cmd.id || index} key={cmd.id || index}
style={{ style={{
background: 'rgba(0, 0, 0, 0.25)', background: 'rgba(0, 0, 0, 0.25)',
border: '1px solid var(--border)', border: '1px solid var(--border)',
borderRadius: '8px', borderRadius: '8px',
padding: '0.75rem', padding: '0.75rem',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: '0.4rem', gap: '0.4rem',
}} }}
> >
{/* Command row */} {/* Command row */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '0.5rem' }}>
<code style={{
fontFamily: 'var(--font-mono)',
fontSize: '0.82rem',
fontWeight: 700,
color: 'var(--primary, #4ade80)',
wordBreak: 'break-all',
}}>
{formattedName}
</code>
<button
type="button"
onClick={() => handleCopy(formattedName, cmdCopyKey)}
title={t('plugin.commands.copy_command', 'Copy command')}
style={{
background: copiedId === cmdCopyKey ? 'rgba(74, 222, 128, 0.15)' : 'rgba(255, 255, 255, 0.06)',
border: `1px solid ${copiedId === cmdCopyKey ? 'rgba(74, 222, 128, 0.4)' : 'rgba(255, 255, 255, 0.1)'}`,
borderRadius: '4px',
color: copiedId === cmdCopyKey ? 'var(--success, #4ade80)' : 'var(--text-muted)',
cursor: 'pointer',
padding: '3px 6px',
display: 'flex',
alignItems: 'center',
gap: '3px',
fontSize: '0.68rem',
fontWeight: 600,
transition: 'all 0.15s ease',
}}
>
{copiedId === cmdCopyKey ? (
<>
<Check size={11} />
<span>{t('plugin.commands.copied', 'Copied')}</span>
</>
) : (
<Copy size={11} />
)}
</button>
</div>
{/* Permission row */}
{cmd.permission ? (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '0.5rem' }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '0.5rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.3rem', minWidth: 0 }}> <code style={{
<Key size={10} color="var(--text-muted)" style={{ flexShrink: 0 }} /> fontFamily: 'var(--font-mono)',
<span style={{ fontSize: '0.82rem',
fontSize: '0.7rem', fontWeight: 700,
fontFamily: 'var(--font-mono)', color: 'var(--primary, #4ade80)',
color: 'var(--text-muted)', wordBreak: 'break-all',
overflow: 'hidden', }}>
textOverflow: 'ellipsis', {formattedName}
whiteSpace: 'nowrap', </code>
}}>
{cmd.permission}
</span>
</div>
<button <button
type="button" type="button"
onClick={() => handleCopy(cmd.permission, permCopyKey)} onClick={() => handleCopy(formattedName, cmdCopyKey)}
title={t('plugin.commands.copy_permission', 'Copy permission')} title={t('plugin.commands.copy_command', 'Copy command')}
style={{ style={{
background: 'none', background: copiedId === cmdCopyKey ? 'rgba(74, 222, 128, 0.15)' : 'rgba(255, 255, 255, 0.06)',
border: 'none', border: `1px solid ${copiedId === cmdCopyKey ? 'rgba(74, 222, 128, 0.4)' : 'rgba(255, 255, 255, 0.1)'}`,
color: copiedId === permCopyKey ? 'var(--success, #4ade80)' : 'var(--text-muted)', borderRadius: '4px',
color: copiedId === cmdCopyKey ? 'var(--success, #4ade80)' : 'var(--text-muted)',
cursor: 'pointer', cursor: 'pointer',
padding: '2px', padding: '3px 6px',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
flexShrink: 0, gap: '3px',
fontSize: '0.68rem',
fontWeight: 600,
transition: 'all 0.15s ease',
}} }}
> >
{copiedId === permCopyKey ? <Check size={11} /> : <Copy size={10} />} {copiedId === cmdCopyKey ? (
<>
<Check size={11} />
<span>{t('plugin.commands.copied', 'Copied')}</span>
</>
) : (
<Copy size={11} />
)}
</button> </button>
</div> </div>
) : null}
{/* Description */} {/* Permission row */}
{desc ? ( {cmd.permission ? (
<p style={{ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '0.5rem' }}>
margin: '0.15rem 0 0 0', <div style={{ display: 'flex', alignItems: 'center', gap: '0.3rem', minWidth: 0 }}>
fontSize: '0.78rem', <Key size={10} color="var(--text-muted)" style={{ flexShrink: 0 }} />
color: 'var(--text-2, #94a3b8)', <span style={{
lineHeight: 1.4, fontSize: '0.7rem',
}}> fontFamily: 'var(--font-mono)',
{desc} color: 'var(--text-muted)',
</p> overflow: 'hidden',
) : null} textOverflow: 'ellipsis',
</div> whiteSpace: 'nowrap',
); }}>
})} {cmd.permission}
</span>
</div>
<button
type="button"
onClick={() => handleCopy(cmd.permission, permCopyKey)}
title={t('plugin.commands.copy_permission', 'Copy permission')}
style={{
background: 'none',
border: 'none',
color: copiedId === permCopyKey ? 'var(--success, #4ade80)' : 'var(--text-muted)',
cursor: 'pointer',
padding: '2px',
display: 'flex',
alignItems: 'center',
flexShrink: 0,
}}
>
{copiedId === permCopyKey ? <Check size={11} /> : <Copy size={10} />}
</button>
</div>
) : null}
{/* Description */}
{desc ? (
<p style={{
margin: '0.15rem 0 0 0',
fontSize: '0.78rem',
color: 'var(--text-2, #94a3b8)',
lineHeight: 1.4,
}}>
{desc}
</p>
) : null}
</div>
);
})}
</div>
{commands.length > 4 && (
<button
type="button"
onClick={() => setExpanded(!expanded)}
style={{
width: '100%',
marginTop: '0.75rem',
background: 'rgba(255, 255, 255, 0.04)',
border: '1px solid var(--border)',
borderRadius: '6px',
color: 'var(--text)',
padding: '6px 10px',
fontSize: '0.75rem',
fontWeight: 600,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '4px',
transition: 'background 0.15s ease',
}}
>
{expanded ? (
<>
<ChevronUp size={13} />
{t('plugin.commands.show_less', 'Show less')}
</>
) : (
<>
<ChevronDown size={13} />
{t('plugin.commands.show_all', 'Show all {{count}} commands', { count: commands.length })}
</>
)}
</button>
)}
</div> </div>
</>
{commands.length > 4 && (
<button
type="button"
onClick={() => setExpanded(!expanded)}
style={{
width: '100%',
marginTop: '0.75rem',
background: 'rgba(255, 255, 255, 0.04)',
border: '1px solid var(--border)',
borderRadius: '6px',
color: 'var(--text)',
padding: '6px 10px',
fontSize: '0.75rem',
fontWeight: 600,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '4px',
transition: 'background 0.15s ease',
}}
>
{expanded ? (
<>
<ChevronUp size={13} />
{t('plugin.commands.show_less', 'Show less')}
</>
) : (
<>
<ChevronDown size={13} />
{t('plugin.commands.show_all', 'Show all {{count}} commands', { count: commands.length })}
</>
)}
</button>
)}
</div>
); );
}; };
@ -1503,8 +1506,11 @@ const PluginDetail = () => {
)} )}
</div> </div>
)} )}
</div>
<div className="divider-sm" /> <PluginCommandsSidebarWidget commands={plugin.commands} />
{(user || reportDone) && <div className="divider-sm" />}
{user && !reportDone && ( {user && !reportDone && (
<> <>
{!reportOpen ? ( {!reportOpen ? (
@ -1562,9 +1568,6 @@ const PluginDetail = () => {
</p> </p>
)} )}
</div> </div>
</div>
<PluginCommandsSidebarWidget commands={plugin.commands} />
</aside> </aside>
</div> </div>

View File

@ -252,9 +252,10 @@ const AddPlugin = () => {
}; };
const updateCommandField = (index: number, field: 'name' | 'permission', value: string) => { const updateCommandField = (index: number, field: 'name' | 'permission', value: string) => {
const cleanValue = field === 'name' ? value.replace(/^\/+/, '') : value;
setCommands(prev => { setCommands(prev => {
const next = [...prev]; const next = [...prev];
next[index] = { ...next[index], [field]: value }; next[index] = { ...next[index], [field]: cleanValue };
return next; return next;
}); });
}; };
@ -460,9 +461,9 @@ const AddPlugin = () => {
setSubmitting(true); setSubmitting(true);
try { try {
const validCommands = commands const validCommands = commands
.filter(c => c.name.trim().length > 0) .filter(c => c.name.trim().replace(/^\/+/, '').length > 0)
.map((c, idx) => ({ .map((c, idx) => ({
name: c.name.trim(), name: c.name.trim().replace(/^\/+/, ''),
permission: c.permission.trim() || undefined, permission: c.permission.trim() || undefined,
description: c.descriptions, description: c.descriptions,
display_order: idx, display_order: idx,
@ -864,8 +865,9 @@ const AddPlugin = () => {
<input <input
className="mp-input" className="mp-input"
style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', padding: '0.45rem 0.65rem' }} style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', padding: '0.45rem 0.65rem' }}
placeholder="/spawn [player]" placeholder="spawn [player]"
value={cmd.name} value={cmd.name}
maxLength={64}
onChange={e => updateCommandField(idx, 'name', e.target.value)} onChange={e => updateCommandField(idx, 'name', e.target.value)}
required required
/> />
@ -879,6 +881,7 @@ const AddPlugin = () => {
style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', padding: '0.45rem 0.65rem' }} style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', padding: '0.45rem 0.65rem' }}
placeholder="pumpkin.command.spawn" placeholder="pumpkin.command.spawn"
value={cmd.permission} value={cmd.permission}
maxLength={128}
onChange={e => updateCommandField(idx, 'permission', e.target.value)} onChange={e => updateCommandField(idx, 'permission', e.target.value)}
/> />
</div> </div>

View File

@ -234,7 +234,7 @@ const StoreListing = ({ plugin, onSaved }: Props) => {
} }
return { return {
id: cmd.id, id: cmd.id,
name: cmd.name || '', name: (cmd.name || '').replace(/^\/+/, ''),
permission: cmd.permission || '', permission: cmd.permission || '',
descriptions: descMap, descriptions: descMap,
}; };
@ -253,9 +253,10 @@ const StoreListing = ({ plugin, onSaved }: Props) => {
}; };
const updateCommandField = (index: number, field: 'name' | 'permission', value: string) => { const updateCommandField = (index: number, field: 'name' | 'permission', value: string) => {
const cleanValue = field === 'name' ? value.replace(/^\/+/, '') : value;
setCommands(prev => { setCommands(prev => {
const next = [...prev]; const next = [...prev];
next[index] = { ...next[index], [field]: value }; next[index] = { ...next[index], [field]: cleanValue };
return next; return next;
}); });
}; };
@ -311,10 +312,10 @@ const StoreListing = ({ plugin, onSaved }: Props) => {
setSaveError(null); setSaveError(null);
setSaveSuccess(false); setSaveSuccess(false);
const validCommands = commands const validCommands = commands
.filter(c => c.name.trim().length > 0) .filter(c => c.name.trim().replace(/^\/+/, '').length > 0)
.map((c, idx) => ({ .map((c, idx) => ({
id: c.id, id: c.id,
name: c.name.trim(), name: c.name.trim().replace(/^\/+/, ''),
permission: c.permission.trim() || undefined, permission: c.permission.trim() || undefined,
description: c.descriptions, description: c.descriptions,
display_order: idx, display_order: idx,
@ -685,8 +686,9 @@ const StoreListing = ({ plugin, onSaved }: Props) => {
<input <input
className="mp-input" className="mp-input"
style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', padding: '0.45rem 0.65rem' }} style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', padding: '0.45rem 0.65rem' }}
placeholder="/spawn [player]" placeholder="spawn [player]"
value={cmd.name} value={cmd.name}
maxLength={64}
onChange={e => updateCommandField(idx, 'name', e.target.value)} onChange={e => updateCommandField(idx, 'name', e.target.value)}
required required
/> />
@ -700,6 +702,7 @@ const StoreListing = ({ plugin, onSaved }: Props) => {
style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', padding: '0.45rem 0.65rem' }} style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', padding: '0.45rem 0.65rem' }}
placeholder="pumpkin.command.spawn" placeholder="pumpkin.command.spawn"
value={cmd.permission} value={cmd.permission}
maxLength={128}
onChange={e => updateCommandField(idx, 'permission', e.target.value)} onChange={e => updateCommandField(idx, 'permission', e.target.value)}
/> />
</div> </div>