feat(studio): add right-click context menu (Solo mode, Collapse all, Expand all) for DevelopPanels, pin Color Chrome sub-chips, and fix library single view sorting & time range select

This commit is contained in:
2026-10-07 17:35:20 +07:00
parent eb47afc63e
commit 4704e936ba
9 changed files with 587 additions and 190 deletions
+141 -3
View File
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import type { ReactNode } from 'react';
import type { ColorAdjustments } from '../../shared/types';
import { PARAM_DEFS } from '../../shared/utils/paramDefs';
@@ -98,6 +98,7 @@ function DevRow({
// slugs, comma-joined; a slug no panel answers to is dropped on the way in, so a
// panel that is renamed or taken out does not leave a hole in the column.
const OPEN_KEY = 'recipescam.develop.open';
const SOLO_KEY = 'recipescam.develop.soloMode';
// The studio's right column: every panel's knobs behind a disclosure that folds
// its own rows away, and the column opens with all of them folded — a dozen rows
@@ -117,15 +118,67 @@ export function DevelopPanels({
onPro?: () => void;
slots?: { wb?: ReactNode; wbFoot?: ReactNode; toneFoot?: ReactNode; effects?: ReactNode };
}) {
const [soloMode, setSoloMode] = useState<boolean>(() => recall(SOLO_KEY) === 'true');
const [open, setOpen] = useState<Record<string, boolean>>(() => {
const saved = new Set((recall(OPEN_KEY) ?? '').split(','));
return Object.fromEntries(PANELS.filter((panel) => saved.has(panel.slug)).map((panel) => [panel.slug, true]));
});
const fold = (slug: string, isOpen: boolean) => {
const next = { ...open, [slug]: !isOpen };
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
useEffect(() => {
if (!contextMenu) return;
const close = () => setContextMenu(null);
window.addEventListener('click', close);
window.addEventListener('keydown', (e) => e.key === 'Escape' && close());
return () => {
window.removeEventListener('click', close);
};
}, [contextMenu]);
const saveOpen = (next: Record<string, boolean>) => {
setOpen(next);
keep(OPEN_KEY, PANELS.filter((panel) => next[panel.slug]).map((panel) => panel.slug).join(','));
};
const fold = (slug: string, isOpen: boolean) => {
if (soloMode) {
if (isOpen) {
saveOpen({ ...open, [slug]: false });
} else {
const next = Object.fromEntries(PANELS.map((p) => [p.slug, p.slug === slug]));
saveOpen(next);
}
} else {
saveOpen({ ...open, [slug]: !isOpen });
}
};
const toggleSolo = () => {
const nextSolo = !soloMode;
setSoloMode(nextSolo);
keep(SOLO_KEY, String(nextSolo));
if (nextSolo) {
const activeSlug = PANELS.find((p) => open[p.slug])?.slug ?? 'wb';
const next = Object.fromEntries(PANELS.map((p) => [p.slug, p.slug === activeSlug]));
saveOpen(next);
}
setContextMenu(null);
};
const collapseAll = () => {
saveOpen(Object.fromEntries(PANELS.map((p) => [p.slug, false])));
setContextMenu(null);
};
const expandAll = () => {
if (soloMode) {
setSoloMode(false);
keep(SOLO_KEY, 'false');
}
saveOpen(Object.fromEntries(PANELS.map((p) => [p.slug, true])));
setContextMenu(null);
};
return (
<div className="dev-panels">
{PANELS.map((panel) => {
@@ -139,6 +192,10 @@ export function DevelopPanels({
className="dev-panel-head"
aria-expanded={isOpen}
onClick={() => fold(panel.slug, isOpen)}
onContextMenu={(e) => {
e.preventDefault();
setContextMenu({ x: Math.min(e.clientX, window.innerWidth - 180), y: Math.min(e.clientY, window.innerHeight - 150) });
}}
>
<span className="dev-panel-label">{panel.title}</span>
<span className="dev-caret" aria-hidden="true">{isOpen ? '▾' : '▸'}</span>
@@ -156,6 +213,87 @@ export function DevelopPanels({
</section>
);
})}
{contextMenu ? (
<div
className="adm-context-menu"
style={{
position: 'fixed',
top: contextMenu.y,
left: contextMenu.x,
zIndex: 99999,
background: 'var(--color-bg-elevated, #232328)',
border: '1px solid var(--color-border, #3a3a42)',
borderRadius: '6px',
boxShadow: '0 8px 24px rgba(0,0,0,0.6)',
padding: '4px 0',
minWidth: '150px',
fontSize: '12px',
color: '#eee',
}}
onClick={(e) => e.stopPropagation()}
>
<button
type="button"
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
width: '100%',
padding: '6px 12px',
border: 'none',
background: 'transparent',
color: 'currentColor',
cursor: 'pointer',
textAlign: 'left',
fontSize: '12px',
}}
onClick={toggleSolo}
>
<input type="checkbox" checked={soloMode} readOnly style={{ pointerEvents: 'none', cursor: 'pointer' }} />
<span>Solo Mode</span>
</button>
<div style={{ height: '1px', background: 'var(--color-border, #3a3a42)', margin: '4px 0' }} />
<button
type="button"
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
width: '100%',
padding: '6px 12px',
border: 'none',
background: 'transparent',
color: 'currentColor',
cursor: 'pointer',
textAlign: 'left',
fontSize: '12px',
}}
onClick={collapseAll}
>
<span>Collapse All</span>
</button>
<button
type="button"
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
width: '100%',
padding: '6px 12px',
border: 'none',
background: 'transparent',
color: 'currentColor',
cursor: 'pointer',
textAlign: 'left',
fontSize: '12px',
}}
onClick={expandAll}
>
<span>Expand All</span>
</button>
</div>
) : null}
</div>
);
}