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 (
- {
- setSelectedIndex(logicalIndex);
- setOpenIndex(openIndex === logicalIndex ? null : logicalIndex);
- previewPhysical(physicalIndex);
- }}
- className="relative transition-all"
- style={{
- minHeight: 40,
- aspectRatio: '1',
- borderRadius: 8,
- border: isSelected ? '2px solid #4a7cff' : isHoveredPhysical ? '2px solid #fff' : '1px solid #1f2330',
- background: isSelected ? 'rgba(74,124,255,0.18)' : '#101119',
- color: '#e8e8f0',
- display: 'grid',
- placeItems: 'center',
- padding: 4
- }}
- >
- {labelFor(logicalIndex, gridColumns)}
- P{physicalIndex + 1}
-
- );
- })}
-
-
-
- {selectedIndex === null ? (
-
-
- No spot selected
-
-
- ) : (
- <>
-
setOpenIndex(openIndex === selectedIndex ? null : selectedIndex)}
- className="w-full flex items-center justify-between"
- style={{
- minHeight: 44,
- padding: '0 12px',
- color: '#e8e8f0',
- background: '#12131d',
- border: 0,
- borderBottom: openIndex === selectedIndex ? '1px solid #1f2330' : 0
- }}
- >
-
- Grid {labelFor(selectedIndex, gridColumns)}
-
-
- Physical {selectedPhysical === null ? '-' : selectedPhysical + 1}
-
-
-
- {openIndex === selectedIndex && (
-
previewPhysical(null)}
- >
- {physicalOptions.map(physicalIndex => (
- previewPhysical(physicalIndex)}
- onFocus={() => previewPhysical(physicalIndex)}
- onClick={() => assignPhysical(selectedIndex, physicalIndex)}
- style={{
- minHeight: 36,
- borderRadius: 7,
- border: physicalIndex === selectedPhysical ? '1px solid #4a7cff' : '1px solid #202432',
- background: physicalIndex === selectedPhysical ? 'rgba(74,124,255,0.2)' : '#151722',
- color: physicalIndex === selectedPhysical ? '#ffffff' : '#b8bccb',
- fontSize: 12,
- fontWeight: 700
- }}
- >
- Light {physicalIndex + 1}
-
- ))}
-
- )}
- >
- )}
-
-
-
-
-
- {status === 'saving' ? 'Saving' : 'Save'}
-
-
- Reset
-
-
- {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 {