From f07708f29644a49eb65d3d8e3f384c91a131c369 Mon Sep 17 00:00:00 2001 From: Nathan Schneider Date: Tue, 30 Jun 2026 16:02:16 -0600 Subject: [PATCH] Redesign gradient control as ASCII diverging-fill scale Replace the click-anywhere ASCII bar (whose hit-mapping was offset from the displayed marker, so taps registered the wrong value) with a track of nine discrete, exact tap targets that fill from the center outward to the selection: [----==#--]. Direction and distance from center read at a glance. - Exact per-position selection on touch and desktop; no more position math - Keyboard support (arrows/Home/End), scoped so it doesn't trigger the app-level screen navigation - Center the bar and slim mobile padding so the full track fits narrow viewports - Apply the same control to the manual analysis gradient; render the automated ones as a matching read-only, dimmed display - Add a subtle gray "Value: #" indicator below the buttons on the diagnostic gradients, matching the analysis screens - Move the "auto-calculated" note under the ANALYSIS header instead of after the value Co-Authored-By: Claude Opus 4.8 (1M context) --- bicorder-app/src/App.svelte | 13 + .../src/components/AnalysisDisplay.svelte | 318 ++++++++++-------- .../src/components/GradientSlider.svelte | 249 ++++++++------ 3 files changed, 342 insertions(+), 238 deletions(-) diff --git a/bicorder-app/src/App.svelte b/bicorder-app/src/App.svelte index ab42762..fefc16f 100644 --- a/bicorder-app/src/App.svelte +++ b/bicorder-app/src/App.svelte @@ -569,6 +569,10 @@
ANALYSIS
+ {#if screen.gradient.automated} +
auto-calculated
+ {/if} + {#if isFirstAnalysisScreen} (); + const scaleValues = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + const CENTER = 5; + let showNotes = false; let notesText = gradient.notes || ''; + let scaleEl: HTMLDivElement; + + // Shared with GradientSlider: the track fills from the center (=) out to + // the chosen position (#), so distance from the middle reads at a glance. + type CellKind = 'empty' | 'center' | 'fill' | 'marker'; + + function cellKind(cell: number, value: number | null): CellKind { + if (value === null) return cell === CENTER ? 'center' : 'empty'; + if (cell === value) return 'marker'; + if (value > CENTER && cell >= CENTER && cell < value) return 'fill'; + if (value < CENTER && cell > value && cell <= CENTER) return 'fill'; + return 'empty'; + } + + function cellGlyph(kind: CellKind): string { + switch (kind) { + case 'marker': return '#'; + case 'fill': return '='; + case 'center': return '+'; + default: return '-'; + } + } + + function selectValue(value: number) { + if (gradient.automated) return; + dispatch('change', value); + scaleEl?.focus(); + } function handleNotApplicable() { dispatch('change', null); @@ -23,82 +54,35 @@ showNotes = false; } - function handleScaleClick(event: MouseEvent) { + function handleKeydown(event: KeyboardEvent) { if (gradient.automated) return; - const target = event.currentTarget as HTMLElement; - const rect = target.getBoundingClientRect(); - const x = event.clientX - rect.left; - const width = rect.width; + const current = gradient.value; + let next: number | null = null; - // Account for button padding to get actual text area - const styles = window.getComputedStyle(target); - const paddingLeft = parseFloat(styles.paddingLeft); - const paddingRight = parseFloat(styles.paddingRight); - - const contentWidth = width - paddingLeft - paddingRight; - const xInContent = x - paddingLeft; - - // Clamp to content area and get ratio - const ratio = Math.max(0, Math.min(1, xInContent / contentWidth)); - - // Map to 1-9 with proper centering on each character position - const value = Math.min(9, Math.max(1, Math.floor(ratio * 9) + 1)); - - dispatch('change', value); - } - - function handleScaleTouch(event: TouchEvent) { - if (gradient.automated) return; + if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') { + next = current === null ? CENTER : Math.max(1, current - 1); + } else if (event.key === 'ArrowRight' || event.key === 'ArrowUp') { + next = current === null ? CENTER : Math.min(9, current + 1); + } else if (event.key === 'Home') { + next = 1; + } else if (event.key === 'End') { + next = 9; + } else { + return; + } event.preventDefault(); - const target = event.currentTarget as HTMLElement; - const rect = target.getBoundingClientRect(); - const touch = event.touches[0]; - const x = touch.clientX - rect.left; - const width = rect.width; - - // Account for button padding to get actual text area - const styles = window.getComputedStyle(target); - const paddingLeft = parseFloat(styles.paddingLeft); - const paddingRight = parseFloat(styles.paddingRight); - - const contentWidth = width - paddingLeft - paddingRight; - const xInContent = x - paddingLeft; - - // Clamp to content area and get ratio - const ratio = Math.max(0, Math.min(1, xInContent / contentWidth)); - - // Map to 1-9 with proper centering on each character position - const value = Math.min(9, Math.max(1, Math.floor(ratio * 9) + 1)); - - dispatch('change', value); + event.stopPropagation(); + dispatch('change', next); } - - function renderBar(value: number | null): string { - // Fixed scale with 9 positions using ||||#|||| - if (value === null) { - return '||||+||||'; - } - // Value is 1-9, position the # marker at the right spot - const positions = [ - '#||||||||', - '|#|||||||', - '||#||||||', - '|||#|||||', - '||||#||||', - '|||||#|||', - '||||||#||', - '|||||||#|', - '||||||||#' - ]; - return positions[value - 1]; - } - - $: barDisplay = renderBar(gradient.value);
+ {#if gradient.automated && !focusedMode} +
auto-calculated
+ {/if} +
{#if !focusedMode}
@@ -109,22 +93,53 @@ {/if}
- + {#if gradient.automated} + + + {:else} + +
+ + {#each scaleValues as n} + {@const kind = cellKind(n, gradient.value)} + + {/each} + +
+ {/if}
{#if !focusedMode} @@ -136,13 +151,6 @@ {/if}
-
- Value: {gradient.value !== null ? gradient.value : ''} - {#if gradient.automated} - (auto-calculated) - {/if} -
-
{#if !gradient.automated}
@@ -176,6 +184,10 @@ {/if}
+
+ Value: {gradient.value !== null ? gradient.value : ''} +
+ {#if showNotes}