diff --git a/packages/ui/src/app.tsx b/packages/ui/src/app.tsx index 42298af..72127ec 100644 --- a/packages/ui/src/app.tsx +++ b/packages/ui/src/app.tsx @@ -21,7 +21,6 @@ import { PatternsTab } from '@/components/patterns-tab'; import { PlaylistTab } from '@/components/playlist-tab'; import { PrideTab } from '@/components/pride-tab'; import { SequencesTab } from '@/components/sequences-tab'; -import { SettingsTab } from '@/components/settings-tab'; import { ShiftDial } from '@/components/shift-dial'; import { StatusDot } from '@/components/status-dot'; import { UsaTab } from '@/components/usa-tab'; @@ -59,8 +58,7 @@ const tabs: { key: GridMode; label: string }[] = [ { key: 'flags', label: 'Flags' }, { key: 'drops', label: 'Drops' }, { key: 'audio', label: 'Audio' }, - { key: 'video', label: 'Video' }, - { key: 'debug', label: 'Debug' } + { key: 'video', label: 'Video' } ]; /* ---------- Tool content (no tabs, just the active tool) ---------- */ @@ -293,13 +291,6 @@ function ToolContent({ )} - {tab === 'debug' && ( - - )} ); } @@ -322,7 +313,7 @@ function TabStrip({
@@ -332,8 +323,8 @@ function TabStrip({ onClick={() => setTab(t.key)} className="whitespace-nowrap transition-all" style={{ - padding: isPhone ? '10px 16px' : isRight ? '8px 12px' : '10px 18px', - borderRadius: 22, + padding: isPhone ? '9px 14px' : isRight ? '7px 11px' : '8px 15px', + borderRadius: 20, fontSize: isPhone ? 14 : isRight ? 12 : 14, fontWeight: 600, letterSpacing: '0.02em', @@ -593,7 +584,6 @@ export default function Home() { }, [settings]); useEffect(() => { - if (tab === 'debug') return; send({ type: 'physical_preview_clear' }); send({ type: 'calibration_mode', enabled: false }); }, [send, tab]); @@ -826,10 +816,6 @@ export default function Home() { ); const handleTabChange = useCallback((t: GridMode) => { - if (t !== 'debug' && tab === 'debug') { - send({ type: 'physical_preview_clear' }); - send({ type: 'calibration_mode', enabled: false }); - } setTab(t); localStorage.setItem('wavegrid-tab', t); if (isPhone && sheetSnap === 'peek') { diff --git a/packages/ui/src/components/grid-display.tsx b/packages/ui/src/components/grid-display.tsx index 33b38bf..f278da3 100644 --- a/packages/ui/src/components/grid-display.tsx +++ b/packages/ui/src/components/grid-display.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef } from 'react'; import type { CannonColor, Orientation } from '@/lib/use-socket'; -export type GridMode = 'paint' | 'gradient' | 'drops' | 'scenes' | 'animations' | 'audio' | 'video' | 'flags' | 'pride' | 'usa' | 'nova' | 'grace' | 'patterns' | 'playlist' | 'sequences' | 'debug'; +export type GridMode = 'paint' | 'gradient' | 'drops' | 'scenes' | 'animations' | 'audio' | 'video' | 'flags' | 'pride' | 'usa' | 'nova' | 'grace' | 'patterns' | 'playlist' | 'sequences'; export interface FixturePos { u: number; diff --git a/packages/ui/src/components/settings-tab.tsx b/packages/ui/src/components/settings-tab.tsx deleted file mode 100644 index 580bb5a..0000000 --- a/packages/ui/src/components/settings-tab.tsx +++ /dev/null @@ -1,333 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; - -interface LightMapConfig { - version: 1; - numCannons: number; - gridColumns: number; - physicalLights: number[]; - updatedAt?: string; -} - -interface SettingsTabProps { - numCannons: number; - gridColumns: number; - send: (msg: Record) => void; -} - -function identityMap(numCannons: number): number[] { - return Array.from({ length: numCannons }, (_, index) => index); -} - -function labelFor(index: number, columns: number): string { - const row = Math.floor(index / columns) + 1; - const col = (index % columns) + 1; - return `${row}.${col}`; -} - -function normalizeMap(values: number[], numCannons: number): number[] { - const used = new Set(); - const result = values.slice(0, numCannons).map(value => { - if (!Number.isInteger(value) || value < 0 || value >= numCannons || used.has(value)) return -1; - used.add(value); - return value; - }); - - for (let index = 0; index < numCannons; index++) { - if (result[index] !== undefined && result[index] >= 0) continue; - const next = identityMap(numCannons).find(value => !used.has(value)); - result[index] = next ?? index; - used.add(result[index]); - } - - return result; -} - -export function SettingsTab({ numCannons, gridColumns, send }: SettingsTabProps) { - const [physicalLights, setPhysicalLights] = useState(() => identityMap(numCannons)); - const [selectedIndex, setSelectedIndex] = useState(null); - const [openIndex, setOpenIndex] = useState(null); - const [hoveredPhysical, setHoveredPhysical] = useState(null); - const [status, setStatus] = useState<'idle' | 'loading' | 'saving' | 'saved' | 'error'>('loading'); - const [updatedAt, setUpdatedAt] = useState(null); - - const physicalOptions = useMemo(() => identityMap(numCannons), [numCannons]); - - const previewPhysical = useCallback((physicalIndex: number | null) => { - setHoveredPhysical(physicalIndex); - if (physicalIndex === null) { - send({ type: 'physical_preview_clear' }); - return; - } - send({ type: 'physical_preview', physicalIndex }); - }, [send]); - - useEffect(() => { - let cancelled = false; - setStatus('loading'); - fetch('/api/light-map') - .then(res => res.json()) - .then((config: LightMapConfig) => { - if (cancelled) return; - setPhysicalLights(normalizeMap(config.physicalLights, numCannons)); - setUpdatedAt(config.updatedAt ?? null); - setStatus('idle'); - }) - .catch(() => { - if (cancelled) return; - setPhysicalLights(identityMap(numCannons)); - setStatus('error'); - }); - - return () => { - cancelled = true; - }; - }, [numCannons]); - - useEffect(() => { - send({ type: 'calibration_mode', enabled: true }); - send({ type: 'physical_preview_clear' }); - return () => { - send({ type: 'physical_preview_clear' }); - send({ type: 'calibration_mode', enabled: false }); - }; - }, [send]); - - const assignPhysical = useCallback((logicalIndex: number, physicalIndex: number) => { - setPhysicalLights(current => { - const next = [...current]; - const previousPhysical = next[logicalIndex]; - const previousOwner = next.findIndex(value => value === physicalIndex); - next[logicalIndex] = physicalIndex; - if (previousOwner >= 0 && previousOwner !== logicalIndex) { - next[previousOwner] = previousPhysical; - } - return next; - }); - setOpenIndex(null); - previewPhysical(physicalIndex); - }, [previewPhysical]); - - const save = useCallback(() => { - setStatus('saving'); - fetch('/api/light-map', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - version: 1, - numCannons, - gridColumns, - physicalLights - }) - }) - .then(res => res.json()) - .then((config: LightMapConfig) => { - setPhysicalLights(normalizeMap(config.physicalLights, numCannons)); - setUpdatedAt(config.updatedAt ?? null); - setStatus('saved'); - }) - .catch(() => setStatus('error')); - }, [gridColumns, numCannons, physicalLights]); - - const resetIdentity = useCallback(() => { - const next = identityMap(numCannons); - setPhysicalLights(next); - setSelectedIndex(null); - setOpenIndex(null); - previewPhysical(null); - }, [numCannons, previewPhysical]); - - const rows = Math.ceil(numCannons / gridColumns); - const selectedPhysical = selectedIndex === null ? null : physicalLights[selectedIndex]; - - return ( -
-
-
previewPhysical(null)} - > - {physicalLights.map((physicalIndex, logicalIndex) => { - const isSelected = selectedIndex === logicalIndex; - const isHoveredPhysical = hoveredPhysical === physicalIndex; - - return ( - - ); - })} -
- -
- {selectedIndex === null ? ( -
- - No spot selected - -
- ) : ( - <> - - - {openIndex === selectedIndex && ( -
previewPhysical(null)} - > - {physicalOptions.map(physicalIndex => ( - - ))} -
- )} - - )} -
-
- -
- - - - {status === 'loading' - ? 'Loading' - : status === 'saved' - ? 'Saved' - : status === 'error' - ? 'Config unavailable' - : updatedAt - ? `Saved ${new Date(updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` - : ''} - -
-
- ); -} diff --git a/packages/ui/src/globals.css b/packages/ui/src/globals.css index 90f6b13..70957d2 100644 --- a/packages/ui/src/globals.css +++ b/packages/ui/src/globals.css @@ -29,17 +29,29 @@ html, body { -webkit-user-select: none; } +/* The element itself is only the hit area — its height is a touch target that + components are free to override. The visible gutter lives on the track + pseudo-element, so it can't be squeezed out of existence by that height. */ input[type="range"] { -webkit-appearance: none; appearance: none; + height: 28px; + background: transparent; + outline: none; +} + +/* Dark blue rather than black: the panel stays seamless, but the channel the + thumb travels in is visible. */ +input[type="range"]::-webkit-slider-runnable-track { height: 10px; border-radius: 5px; - /* Only the gutter lifts off the black page — a very dark blue, enough to see - how far the thumb can travel without putting a grey bar in the panel. */ - background: linear-gradient(to right, #16203a, var(--color-accent)); - outline: none; - padding: 8px 0; - background-clip: content-box; + background: linear-gradient(to right, #1c2b4d, var(--color-accent)); +} + +input[type="range"]::-moz-range-track { + height: 10px; + border-radius: 5px; + background: linear-gradient(to right, #1c2b4d, var(--color-accent)); } input[type="range"]::-webkit-slider-thumb { @@ -50,6 +62,8 @@ input[type="range"]::-webkit-slider-thumb { background: var(--color-accent); box-shadow: 0 0 12px var(--color-accent-glow); cursor: pointer; + /* Centre the thumb on the thinner track. */ + margin-top: -11px; } input[type="range"]::-moz-range-thumb { @@ -65,15 +79,23 @@ input[type="range"]::-moz-range-thumb { /* Larger hit area for sliders on touch devices */ @media (pointer: coarse) { input[type="range"] { + height: 44px; + } + + input[type="range"]::-webkit-slider-runnable-track { + height: 12px; + border-radius: 6px; + } + + input[type="range"]::-moz-range-track { height: 12px; border-radius: 6px; - padding: 14px 0; - min-height: 44px; } input[type="range"]::-webkit-slider-thumb { width: 36px; height: 36px; + margin-top: -12px; } input[type="range"]::-moz-range-thumb {