improve ui/ux further

This commit is contained in:
Boof2015
2026-03-21 17:41:01 -04:00
parent 72dac16983
commit dc3604dbb5
5 changed files with 296 additions and 127 deletions
+1 -3
View File
@@ -12,7 +12,6 @@ import { Waveform } from '../visualizers/Waveform'
interface ScopeModuleProps {
scopeKind: ScopeKind
lineColor?: string
widthWeight?: number
}
interface Visualizer {
@@ -98,7 +97,7 @@ function createVisualizer(scopeKind: ScopeKind, canvas: HTMLCanvasElement, mySet
}
}
export default function ScopeModule({ scopeKind, lineColor = '#38bdf8', widthWeight = 1 }: ScopeModuleProps): JSX.Element {
export default function ScopeModule({ scopeKind, lineColor = '#38bdf8' }: ScopeModuleProps): JSX.Element {
const containerRef = useRef<HTMLDivElement>(null)
const canvasRef = useRef<HTMLCanvasElement>(null)
const visualizerRef = useRef<Visualizer | null>(null)
@@ -167,7 +166,6 @@ export default function ScopeModule({ scopeKind, lineColor = '#38bdf8', widthWei
<div
ref={containerRef}
style={{
flex: widthWeight,
minWidth: 0,
height: '100%',
position: 'relative',
+27 -21
View File
@@ -1,8 +1,9 @@
import { useEffect, useRef, type CSSProperties, type JSX, type ReactNode } from 'react'
import { useEffect, useMemo, useRef, type CSSProperties, type JSX, type ReactNode } from 'react'
import { useAudioStore } from '../stores/audioStore'
import { useSettingsStore, type ScopeSettings } from '../stores/settingsStore'
import { useThemeStore, PRESETS, PRESET_IDS } from '../stores/themeStore'
import type { ScopeKind } from '../../types/scope'
import { buildAnalyzerGridTemplateColumns } from '../analyzerLayout'
const SCOPE_LABELS: Record<ScopeKind, string> = {
spectrum: 'Spectrum',
@@ -151,7 +152,7 @@ function RangeControl({
)
}
function ScopeSettingsCard({
function ScopeSettingsSection({
kind,
settings,
onUpdate,
@@ -163,13 +164,13 @@ function ScopeSettingsCard({
const scopeSettings = settings[kind]
return (
<section className="settings-card">
<div className="settings-card__header">
<div className="settings-card__title">{SCOPE_LABELS[kind]}</div>
<div className="settings-card__summary">{scopeSummary(kind, scopeSettings)}</div>
<section className="settings-scope-section">
<div className="settings-scope-section__header">
<div className="settings-scope-section__title">{SCOPE_LABELS[kind]}</div>
<div className="settings-scope-section__summary">{scopeSummary(kind, scopeSettings)}</div>
</div>
<div className="settings-card__controls">
<div className="settings-scope-section__controls">
{kind === 'spectrum' && (() => {
const current = scopeSettings as ScopeSettings['spectrum']
return (
@@ -487,11 +488,16 @@ export default function SettingsPanel({ onClose, onHeightChange }: SettingsPanel
setCaptureMode,
startCapture,
} = useAudioStore()
const { scopeSettings, updateScopeSettings, hiddenScopes, scopeOrder } = useSettingsStore()
const { scopeSettings, updateScopeSettings, hiddenScopes, scopeOrder, widthWeights } = useSettingsStore()
const { presetId, accent, setPreset, setCustomAccent, customAccent } = useThemeStore()
const panelRef = useRef<HTMLDivElement | null>(null)
const visibleScopes = scopeOrder.filter((kind) => !hiddenScopes.has(kind))
const scopeTrackStyle = useMemo(() => {
const gridTemplateColumns = buildAnalyzerGridTemplateColumns(visibleScopes, widthWeights)
if (!gridTemplateColumns) return undefined
return { gridTemplateColumns } as CSSProperties
}, [visibleScopes, widthWeights])
useEffect(() => {
void refreshDevices()
@@ -542,8 +548,8 @@ export default function SettingsPanel({ onClose, onHeightChange }: SettingsPanel
return (
<div className="settings-panel" ref={panelRef}>
<div className="settings-panel__utility">
<section className="settings-utility-card">
<div className="settings-panel__utility-row">
<section className="settings-utility-section settings-utility-section--source">
<div className="settings-section-title">Audio Source</div>
<label className="settings-control settings-control--stack">
@@ -576,7 +582,7 @@ export default function SettingsPanel({ onClose, onHeightChange }: SettingsPanel
) : null}
</section>
<section className="settings-utility-card">
<section className="settings-utility-section settings-utility-section--theme">
<div className="settings-section-title">Theme</div>
<div className="settings-theme-swatches">
@@ -616,11 +622,19 @@ export default function SettingsPanel({ onClose, onHeightChange }: SettingsPanel
</div>
</label>
</section>
<button
type="button"
className="settings-panel__close"
onClick={onClose}
>
Close
</button>
</div>
<div className="settings-panel__scopes">
<div className="settings-panel__scope-track" style={scopeTrackStyle}>
{visibleScopes.map((kind) => (
<ScopeSettingsCard
<ScopeSettingsSection
key={kind}
kind={kind}
settings={scopeSettings}
@@ -628,14 +642,6 @@ export default function SettingsPanel({ onClose, onHeightChange }: SettingsPanel
/>
))}
</div>
<button
type="button"
className="settings-panel__close"
onClick={onClose}
>
Close
</button>
</div>
)
}
+146 -42
View File
@@ -1,30 +1,132 @@
import { Fragment, useCallback, useRef, type JSX } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type JSX } from 'react'
import { useSettingsStore } from '../stores/settingsStore'
import { useThemeStore } from '../stores/themeStore'
import type { ScopeKind } from '../../types/scope'
import ScopeModule from './ScopeModule'
import { buildAnalyzerGridTemplateColumns } from '../analyzerLayout'
function ResizeHandle({ leftKind, rightKind }: { leftKind: ScopeKind; rightKind: ScopeKind }): JSX.Element {
export default function Strip(): JSX.Element {
const scopeOrder = useSettingsStore((s) => s.scopeOrder)
const hiddenScopes = useSettingsStore((s) => s.hiddenScopes)
const widthWeights = useSettingsStore((s) => s.widthWeights)
const setScopeWidthWeight = useSettingsStore((s) => s.setScopeWidthWeight)
const handleRef = useRef<HTMLDivElement>(null)
const accent = useThemeStore((s) => s.accent)
const stripRef = useRef<HTMLDivElement>(null)
const gridRef = useRef<HTMLDivElement>(null)
const scopeRefs = useRef<Partial<Record<ScopeKind, HTMLDivElement | null>>>({})
const [handleOffsets, setHandleOffsets] = useState<number[]>([])
const visibleScopes = scopeOrder.filter((k) => !hiddenScopes.has(k))
const gridTemplateColumns = useMemo(() => {
return buildAnalyzerGridTemplateColumns(visibleScopes, widthWeights)
}, [visibleScopes, widthWeights])
const gridStyle = useMemo(() => {
if (!gridTemplateColumns) return undefined
return { gridTemplateColumns } as CSSProperties
}, [gridTemplateColumns])
const onMouseDown = useCallback((e: React.MouseEvent) => {
e.preventDefault()
const startX = e.clientX
const container = handleRef.current?.parentElement
if (!container) return
const updateHandleOffsets = useCallback((): void => {
if (visibleScopes.length < 2) {
setHandleOffsets([])
return
}
const totalWidth = container.getBoundingClientRect().width
const { widthWeights } = useSettingsStore.getState()
const startLeftWeight = widthWeights[leftKind] ?? 1
const startRightWeight = widthWeights[rightKind] ?? 1
const nextOffsets: number[] = []
for (let index = 0; index < visibleScopes.length - 1; index += 1) {
const leftElement = scopeRefs.current[visibleScopes[index]]
if (!leftElement) continue
nextOffsets.push(leftElement.offsetLeft + leftElement.offsetWidth)
}
setHandleOffsets(nextOffsets)
}, [visibleScopes])
useEffect(() => {
const strip = stripRef.current
if (!strip) return
const setAnalyzerHeight = (): void => {
const nextHeight = Math.max(0, Math.ceil(strip.getBoundingClientRect().height))
document.documentElement.style.setProperty('--analyzer-height', `${nextHeight}px`)
}
setAnalyzerHeight()
const observer = typeof ResizeObserver === 'undefined'
? null
: new ResizeObserver(() => setAnalyzerHeight())
observer?.observe(strip)
window.addEventListener('resize', setAnalyzerHeight)
return () => {
observer?.disconnect()
window.removeEventListener('resize', setAnalyzerHeight)
}
}, [])
useEffect(() => {
updateHandleOffsets()
const observer = typeof ResizeObserver === 'undefined'
? null
: new ResizeObserver(() => updateHandleOffsets())
if (gridRef.current) {
observer?.observe(gridRef.current)
}
for (const scope of visibleScopes) {
const element = scopeRefs.current[scope]
if (element) {
observer?.observe(element)
}
}
window.addEventListener('resize', updateHandleOffsets)
return () => {
observer?.disconnect()
window.removeEventListener('resize', updateHandleOffsets)
}
}, [gridTemplateColumns, updateHandleOffsets, visibleScopes])
const startResizeDrag = useCallback((handleIndex: number, event: React.MouseEvent<HTMLButtonElement>) => {
const leftKind = visibleScopes[handleIndex]
const rightKind = visibleScopes[handleIndex + 1]
if (!leftKind || !rightKind) return
const leftElement = scopeRefs.current[leftKind]
const rightElement = scopeRefs.current[rightKind]
if (!leftElement || !rightElement) return
event.preventDefault()
const startX = event.clientX
const leftWidth = leftElement.getBoundingClientRect().width
const rightWidth = rightElement.getBoundingClientRect().width
const pairWidth = Math.max(1, leftWidth + rightWidth)
const { widthWeights: currentWeights } = useSettingsStore.getState()
const startLeftWeight = currentWeights[leftKind] ?? 1
const startRightWeight = currentWeights[rightKind] ?? 1
const totalWeight = startLeftWeight + startRightWeight
const minWeight = 0.15
const onMouseMove = (ev: MouseEvent): void => {
const delta = ev.clientX - startX
const ratio = delta / totalWidth * totalWeight * 2
const newLeft = Math.max(0.15, startLeftWeight + ratio)
const newRight = Math.max(0.15, startRightWeight - ratio)
const weightDelta = (delta / pairWidth) * totalWeight
let newLeft = startLeftWeight + weightDelta
let newRight = startRightWeight - weightDelta
if (newLeft < minWeight) {
newRight -= minWeight - newLeft
newLeft = minWeight
}
if (newRight < minWeight) {
newLeft -= minWeight - newRight
newRight = minWeight
}
setScopeWidthWeight(leftKind, newLeft)
setScopeWidthWeight(rightKind, newRight)
}
@@ -40,36 +142,38 @@ function ResizeHandle({ leftKind, rightKind }: { leftKind: ScopeKind; rightKind:
document.body.style.userSelect = 'none'
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
}, [leftKind, rightKind, setScopeWidthWeight])
}, [setScopeWidthWeight, visibleScopes])
return (
<div ref={handleRef} onMouseDown={onMouseDown} className="scope-strip__handle">
<div className="scope-strip__handle-line" />
</div>
)
}
<div ref={stripRef} className="scope-strip">
<div ref={gridRef} className="scope-strip__grid" style={gridStyle}>
{visibleScopes.map((kind) => (
<div
key={kind}
ref={(element) => {
scopeRefs.current[kind] = element
}}
className="scope-strip__cell"
>
<ScopeModule
scopeKind={kind}
lineColor={accent}
/>
</div>
))}
</div>
export default function Strip(): JSX.Element {
const scopeOrder = useSettingsStore((s) => s.scopeOrder)
const hiddenScopes = useSettingsStore((s) => s.hiddenScopes)
const widthWeights = useSettingsStore((s) => s.widthWeights)
const accent = useThemeStore((s) => s.accent)
const visibleScopes = scopeOrder.filter((k) => !hiddenScopes.has(k))
return (
<div className="scope-strip">
{visibleScopes.map((kind, i) => (
<Fragment key={kind}>
{i > 0 && (
<ResizeHandle leftKind={visibleScopes[i - 1]} rightKind={kind} />
)}
<ScopeModule
scopeKind={kind}
lineColor={accent}
widthWeight={widthWeights[kind] ?? 1}
/>
</Fragment>
{visibleScopes.length > 1 && handleOffsets.map((offset, index) => (
<button
key={`${visibleScopes[index]}:${visibleScopes[index + 1]}`}
type="button"
className="scope-strip__resize-handle"
style={{ left: `${offset}px` }}
onMouseDown={(event) => startResizeDrag(index, event)}
aria-label={`Resize between ${visibleScopes[index]} and ${visibleScopes[index + 1]}`}
>
<span className="scope-strip__resize-handle-grip" aria-hidden="true" />
</button>
))}
</div>
)