feat: use react-resizable-panels for resizable section dividers
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -1,4 +1,4 @@
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["immac_style_mixer/StyleMixerPanel-B4CBSKrc.js","immac_style_mixer/vendor-DPfS0z4g.js"])))=>i.map(i=>d[i]);
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["immac_style_mixer/StyleMixerPanel-YndEdIPa.js","immac_style_mixer/vendor-DPfS0z4g.js"])))=>i.map(i=>d[i]);
|
||||
import{r as h,a as R,R as v}from"./vendor-DPfS0z4g.js";const g="modulepreload",I=function(r){return"/"+r},f={},P=function(e,o,n){let c=Promise.resolve();if(o&&o.length>0){document.getElementsByTagName("link");const t=document.querySelector("meta[property=csp-nonce]"),i=(t==null?void 0:t.nonce)||(t==null?void 0:t.getAttribute("nonce"));c=Promise.allSettled(o.map(a=>{if(a=I(a),a in f)return;f[a]=!0;const d=a.endsWith(".css"),w=d?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${a}"]${w}`))return;const s=document.createElement("link");if(s.rel=d?"stylesheet":g,d||(s.as="script"),s.crossOrigin="",s.href=a,i&&s.setAttribute("nonce",i),document.head.appendChild(s),d)return new Promise((E,S)=>{s.addEventListener("load",E),s.addEventListener("error",()=>S(new Error(`Unable to preload CSS for ${a}`)))})}))}function l(t){const i=new Event("vite:preloadError",{cancelable:!0});if(i.payload=t,window.dispatchEvent(i),!i.defaultPrevented)throw t}return c.then(t=>{for(const i of t||[])i.status==="rejected"&&l(i.reason);return e().catch(l)})};var x={exports:{}},u={};/**
|
||||
* @license React
|
||||
* react-jsx-runtime.production.min.js
|
||||
@@ -7,4 +7,4 @@ import{r as h,a as R,R as v}from"./vendor-DPfS0z4g.js";const g="modulepreload",I
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var b=h,j=Symbol.for("react.element"),k=Symbol.for("react.fragment"),O=Object.prototype.hasOwnProperty,L=b.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,C={key:!0,ref:!0,__self:!0,__source:!0};function _(r,e,o){var n,c={},l=null,t=null;o!==void 0&&(l=""+o),e.key!==void 0&&(l=""+e.key),e.ref!==void 0&&(t=e.ref);for(n in e)O.call(e,n)&&!C.hasOwnProperty(n)&&(c[n]=e[n]);if(r&&r.defaultProps)for(n in e=r.defaultProps,e)c[n]===void 0&&(c[n]=e[n]);return{$$typeof:j,type:r,key:l,ref:t,props:c,_owner:L.current}}u.Fragment=k;u.jsx=_;u.jsxs=_;x.exports=u;var m=x.exports,p={},y=R;p.createRoot=y.createRoot,p.hydrateRoot=y.hydrateRoot;const T=v.lazy(()=>P(()=>import("./StyleMixerPanel-B4CBSKrc.js"),__vite__mapDeps([0,1])));function U(){return new Promise(r=>{function e(){if(window.app){r();return}const o=setInterval(()=>{window.app&&(clearInterval(o),r())},50);setTimeout(()=>{clearInterval(o),console.error("[ImmacStyleMixer] Timeout waiting for ComfyUI app"),r()},5e3)}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",e):e()})}async function M(){if(await U(),!window.app){console.error("[ImmacStyleMixer] ComfyUI app not available");return}window.app.extensionManager.registerSidebarTab({id:"immac-style-mixer",icon:"pi pi-palette",title:"Style Mixer",tooltip:"Immac Style Mixer",type:"custom",render:r=>{const e=document.createElement("div");e.id="immac-style-mixer-root",e.style.height="100%",r.appendChild(e),p.createRoot(e).render(m.jsx(v.StrictMode,{children:m.jsx(h.Suspense,{fallback:m.jsx("div",{style:{padding:"1rem"},children:"Loading…"}),children:m.jsx(T,{})})}))}}),window.app.registerExtension({name:"ImmacStyleMixer"})}M().catch(console.error);export{m as j};
|
||||
*/var b=h,j=Symbol.for("react.element"),k=Symbol.for("react.fragment"),O=Object.prototype.hasOwnProperty,L=b.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,C={key:!0,ref:!0,__self:!0,__source:!0};function _(r,e,o){var n,c={},l=null,t=null;o!==void 0&&(l=""+o),e.key!==void 0&&(l=""+e.key),e.ref!==void 0&&(t=e.ref);for(n in e)O.call(e,n)&&!C.hasOwnProperty(n)&&(c[n]=e[n]);if(r&&r.defaultProps)for(n in e=r.defaultProps,e)c[n]===void 0&&(c[n]=e[n]);return{$$typeof:j,type:r,key:l,ref:t,props:c,_owner:L.current}}u.Fragment=k;u.jsx=_;u.jsxs=_;x.exports=u;var m=x.exports,p={},y=R;p.createRoot=y.createRoot,p.hydrateRoot=y.hydrateRoot;const T=v.lazy(()=>P(()=>import("./StyleMixerPanel-YndEdIPa.js"),__vite__mapDeps([0,1])));function U(){return new Promise(r=>{function e(){if(window.app){r();return}const o=setInterval(()=>{window.app&&(clearInterval(o),r())},50);setTimeout(()=>{clearInterval(o),console.error("[ImmacStyleMixer] Timeout waiting for ComfyUI app"),r()},5e3)}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",e):e()})}async function M(){if(await U(),!window.app){console.error("[ImmacStyleMixer] ComfyUI app not available");return}window.app.extensionManager.registerSidebarTab({id:"immac-style-mixer",icon:"pi pi-palette",title:"Style Mixer",tooltip:"Immac Style Mixer",type:"custom",render:r=>{const e=document.createElement("div");e.id="immac-style-mixer-root",e.style.height="100%",r.appendChild(e),p.createRoot(e).render(m.jsx(v.StrictMode,{children:m.jsx(h.Suspense,{fallback:m.jsx("div",{style:{padding:"1rem"},children:"Loading…"}),children:m.jsx(T,{})})}))}}),window.app.registerExtension({name:"ImmacStyleMixer"})}M().catch(console.error);export{m as j};
|
||||
|
||||
Generated
+12
-1
@@ -9,7 +9,8 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"react-resizable-panels": "^4.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@comfyorg/comfyui-frontend-types": "^1.20.2",
|
||||
@@ -1713,6 +1714,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-resizable-panels": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-4.7.0.tgz",
|
||||
"integrity": "sha512-QJTeh8KJR6lKzDwVez+WGPPUPuarIjR3ptg23thJ0G5dAMYJH4iSN1AnNY0DuSP+PgH8s9eYfwrR7AlmX6TvYA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
||||
|
||||
+2
-1
@@ -12,7 +12,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"react-resizable-panels": "^4.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@comfyorg/comfyui-frontend-types": "^1.20.2",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { Group as PanelGroup, Panel, Separator as PanelResizeHandle } from 'react-resizable-panels'
|
||||
import { useStyleMixerData } from '../hooks/useStyleMixerData'
|
||||
import { Mix, MixEntry, Style, StyleMixerData } from '../types'
|
||||
import MixCard from './MixCard'
|
||||
@@ -28,15 +29,10 @@ function SectionHeader({ title }: { title: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
const DIVIDER_PX = 6
|
||||
const MIN_FLEX = 0.08 // minimum section size as a fraction of total flex
|
||||
|
||||
export default function StyleMixerPanel() {
|
||||
const { data, loading, error, update } = useStyleMixerData()
|
||||
const [mixFilter, setMixFilter] = useState('')
|
||||
// flex-grow proportions for the three panes; sum can be anything
|
||||
const [sizes, setSizes] = useState([1, 2, 2])
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const currentMix = data.mixes.find((m) => m.id === data.current_mix_id) ?? null
|
||||
|
||||
@@ -91,39 +87,6 @@ export default function StyleMixerPanel() {
|
||||
update((prev) => ({ ...prev, current_mix_id: id }))
|
||||
}, [update])
|
||||
|
||||
// ── Drag-to-resize dividers ──────────────────────────────────────────────────
|
||||
|
||||
function startDrag(dividerIndex: number, startY: number) {
|
||||
const container = containerRef.current
|
||||
if (!container) return
|
||||
const totalH = container.clientHeight - DIVIDER_PX * 2
|
||||
const snapshot = [...sizes]
|
||||
const snapshotTotal = snapshot.reduce((a, b) => a + b, 0)
|
||||
|
||||
function onMove(e: MouseEvent) {
|
||||
const dy = e.clientY - startY
|
||||
const delta = (dy / totalH) * snapshotTotal
|
||||
setSizes((prev) => {
|
||||
const next = [...prev]
|
||||
next[dividerIndex] = Math.max(MIN_FLEX * snapshotTotal, snapshot[dividerIndex] + delta)
|
||||
next[dividerIndex + 1] = Math.max(MIN_FLEX * snapshotTotal, snapshot[dividerIndex + 1] - delta)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
function onUp() {
|
||||
document.removeEventListener('mousemove', onMove)
|
||||
document.removeEventListener('mouseup', onUp)
|
||||
document.body.style.cursor = ''
|
||||
document.body.style.userSelect = ''
|
||||
}
|
||||
|
||||
document.body.style.cursor = 'ns-resize'
|
||||
document.body.style.userSelect = 'none'
|
||||
document.addEventListener('mousemove', onMove)
|
||||
document.addEventListener('mouseup', onUp)
|
||||
}
|
||||
|
||||
if (loading) return <div style={panelShell}>Loading…</div>
|
||||
if (error) return <div style={{ ...panelShell, color: '#e55' }}>Error: {error}</div>
|
||||
|
||||
@@ -135,159 +98,139 @@ export default function StyleMixerPanel() {
|
||||
: sortedMixes
|
||||
|
||||
return (
|
||||
<div ref={containerRef} style={panelShell}>
|
||||
<PanelGroup orientation="vertical" style={panelShell}>
|
||||
|
||||
{/* ── Pane 0: Current Mix ──────────────────────────────────────────── */}
|
||||
<div style={{ flex: sizes[0], minHeight: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={paneInner}>
|
||||
<SectionHeader title="Current Mix" />
|
||||
{!currentMix ? (
|
||||
<div style={{ fontSize: 12, color: '#666' }}>No mix active — select one from Mixes below.</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 14 }}>{currentMix.name}</div>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
{currentMix.styles.length === 0 && (
|
||||
<div style={{ fontSize: 12, color: '#666' }}>No styles in this mix yet.</div>
|
||||
)}
|
||||
{currentMix.styles.map((entry) => {
|
||||
const style = data.styles.find((s) => s.id === entry.style_id)
|
||||
return (
|
||||
<div
|
||||
key={entry.style_id}
|
||||
style={{
|
||||
border: `1px solid ${entry.enabled ? 'var(--p-primary-color, #6c6)' : 'var(--p-surface-border, #444)'}`,
|
||||
borderRadius: 6,
|
||||
padding: '6px 10px',
|
||||
background: 'var(--p-surface-section, #1e1e1e)',
|
||||
opacity: entry.enabled ? 1 : 0.45,
|
||||
fontSize: 12,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
minWidth: 80,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontWeight: 600 }}>{style?.name ?? '?'}</span>
|
||||
<span style={{ color: '#aaa' }}>×{entry.weight.toFixed(2)}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{/* ── Pane 0: Current Mix ─────────────────────────────────────────── */}
|
||||
<Panel defaultSize={20} minSize={8} style={panelInner}>
|
||||
<SectionHeader title="Current Mix" />
|
||||
{!currentMix ? (
|
||||
<div style={{ fontSize: 12, color: '#666' }}>No mix active — select one from Mixes below.</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 14 }}>{currentMix.name}</div>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
{currentMix.styles.length === 0 && (
|
||||
<div style={{ fontSize: 12, color: '#666' }}>No styles in this mix yet.</div>
|
||||
)}
|
||||
{currentMix.styles.map((entry) => {
|
||||
const style = data.styles.find((s) => s.id === entry.style_id)
|
||||
return (
|
||||
<div
|
||||
key={entry.style_id}
|
||||
style={{
|
||||
border: `1px solid ${entry.enabled ? 'var(--p-primary-color, #6c6)' : 'var(--p-surface-border, #444)'}`,
|
||||
borderRadius: 6,
|
||||
padding: '6px 10px',
|
||||
background: 'var(--p-surface-section, #1e1e1e)',
|
||||
opacity: entry.enabled ? 1 : 0.45,
|
||||
fontSize: 12,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
minWidth: 80,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontWeight: 600 }}>{style?.name ?? '?'}</span>
|
||||
<span style={{ color: '#aaa' }}>×{entry.weight.toFixed(2)}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Divider 0 ────────────────────────────────────────────────────── */}
|
||||
<div
|
||||
onMouseDown={(e) => startDrag(0, e.clientY)}
|
||||
style={dividerStyle}
|
||||
title="Drag to resize"
|
||||
>
|
||||
<div style={dividerHandle} />
|
||||
</div>
|
||||
|
||||
{/* ── Pane 1: Mixes ────────────────────────────────────────────────── */}
|
||||
<div style={{ flex: sizes[1], minHeight: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={paneInner}>
|
||||
<SectionHeader title="Mixes" />
|
||||
<input
|
||||
placeholder="Filter mixes…"
|
||||
value={mixFilter}
|
||||
onChange={(e) => setMixFilter(e.target.value)}
|
||||
style={{ ...filterInput, marginBottom: 8 }}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: 10, overflowX: 'auto', flexWrap: 'nowrap', paddingBottom: 6, flex: 1 }}>
|
||||
{filteredMixes.map((mix) => (
|
||||
<MixCard
|
||||
key={mix.id}
|
||||
mix={mix}
|
||||
styles={data.styles}
|
||||
isActive={mix.id === data.current_mix_id}
|
||||
onActivate={() => setCurrentMix(mix.id)}
|
||||
onUpdate={updateMix}
|
||||
onDelete={() => deleteMix(mix.id)}
|
||||
/>
|
||||
))}
|
||||
<button
|
||||
title="Add a new mix"
|
||||
onClick={addMix}
|
||||
style={{
|
||||
minWidth: 80,
|
||||
border: '1px dashed var(--p-surface-border, #555)',
|
||||
borderRadius: 8,
|
||||
background: 'transparent',
|
||||
color: '#666',
|
||||
cursor: 'pointer',
|
||||
fontSize: 22,
|
||||
alignSelf: 'flex-start',
|
||||
flexShrink: 0,
|
||||
aspectRatio: '1',
|
||||
}}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
|
||||
<PanelResizeHandle style={resizeHandle}>
|
||||
<div style={resizeHandlePill} />
|
||||
</PanelResizeHandle>
|
||||
|
||||
{/* ── Pane 1: Mixes ───────────────────────────────────────────────── */}
|
||||
<Panel defaultSize={40} minSize={8} style={panelInner}>
|
||||
<SectionHeader title="Mixes" />
|
||||
<input
|
||||
placeholder="Filter mixes…"
|
||||
value={mixFilter}
|
||||
onChange={(e) => setMixFilter(e.target.value)}
|
||||
style={{ ...filterInput, marginBottom: 8 }}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: 10, overflowX: 'auto', flexWrap: 'nowrap', paddingBottom: 4, flex: 1 }}>
|
||||
{filteredMixes.map((mix) => (
|
||||
<MixCard
|
||||
key={mix.id}
|
||||
mix={mix}
|
||||
styles={data.styles}
|
||||
isActive={mix.id === data.current_mix_id}
|
||||
onActivate={() => setCurrentMix(mix.id)}
|
||||
onUpdate={updateMix}
|
||||
onDelete={() => deleteMix(mix.id)}
|
||||
/>
|
||||
))}
|
||||
<button
|
||||
title="Add a new mix"
|
||||
onClick={addMix}
|
||||
style={{
|
||||
minWidth: 80,
|
||||
border: '1px dashed var(--p-surface-border, #555)',
|
||||
borderRadius: 8,
|
||||
background: 'transparent',
|
||||
color: '#666',
|
||||
cursor: 'pointer',
|
||||
fontSize: 22,
|
||||
alignSelf: 'flex-start',
|
||||
flexShrink: 0,
|
||||
aspectRatio: '1',
|
||||
}}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
{/* ── Divider 1 ────────────────────────────────────────────────────── */}
|
||||
<div
|
||||
onMouseDown={(e) => startDrag(1, e.clientY)}
|
||||
style={dividerStyle}
|
||||
title="Drag to resize"
|
||||
>
|
||||
<div style={dividerHandle} />
|
||||
</div>
|
||||
<PanelResizeHandle style={resizeHandle}>
|
||||
<div style={resizeHandlePill} />
|
||||
</PanelResizeHandle>
|
||||
|
||||
{/* ── Pane 2: Styles ───────────────────────────────────────────────── */}
|
||||
<div style={{ flex: sizes[2], minHeight: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={paneInner}>
|
||||
<SectionHeader title="Styles" />
|
||||
<StyleGallery
|
||||
styles={data.styles}
|
||||
onUpdate={updateStyle}
|
||||
onDelete={deleteStyle}
|
||||
onAdd={addStyle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* ── Pane 2: Styles ──────────────────────────────────────────────── */}
|
||||
<Panel defaultSize={40} minSize={8} style={panelInner}>
|
||||
<SectionHeader title="Styles" />
|
||||
<StyleGallery
|
||||
styles={data.styles}
|
||||
onUpdate={updateStyle}
|
||||
onDelete={deleteStyle}
|
||||
onAdd={addStyle}
|
||||
/>
|
||||
</Panel>
|
||||
|
||||
</div>
|
||||
</PanelGroup>
|
||||
)
|
||||
}
|
||||
|
||||
const panelShell: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
color: 'var(--p-text-color, #eee)',
|
||||
fontSize: 13,
|
||||
}
|
||||
|
||||
const paneInner: React.CSSProperties = {
|
||||
const panelInner: React.CSSProperties = {
|
||||
padding: '10px 12px',
|
||||
height: '100%',
|
||||
overflowY: 'auto',
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}
|
||||
|
||||
const dividerStyle: React.CSSProperties = {
|
||||
flexShrink: 0,
|
||||
height: DIVIDER_PX,
|
||||
cursor: 'ns-resize',
|
||||
const resizeHandle: React.CSSProperties = {
|
||||
height: 6,
|
||||
background: 'var(--p-surface-border, #333)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'ns-resize',
|
||||
flexShrink: 0,
|
||||
}
|
||||
|
||||
const dividerHandle: React.CSSProperties = {
|
||||
const resizeHandlePill: React.CSSProperties = {
|
||||
width: 32,
|
||||
height: 2,
|
||||
borderRadius: 2,
|
||||
|
||||
Reference in New Issue
Block a user