From 878299d6613ba6007e7c98fd81900c2cbd461c1a Mon Sep 17 00:00:00 2001 From: Ricky Stretch Date: Sat, 14 Mar 2026 17:24:05 -0400 Subject: [PATCH] v0.9.20 created new colour picker on mobile --- .env.example | 2 +- backend/package.json | 2 +- build.sh | 2 +- frontend/package.json | 2 +- frontend/src/components/BrandingModal.jsx | 255 ++++++++++++++++++---- 5 files changed, 219 insertions(+), 44 deletions(-) diff --git a/.env.example b/.env.example index e2b15b5..0a2e628 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,7 @@ PROJECT_NAME=jama # Image version to run (set by build.sh, or use 'latest') -JAMA_VERSION=0.9.18 +JAMA_VERSION=0.9.20 # App port — the host port Docker maps to the container PORT=3000 diff --git a/backend/package.json b/backend/package.json index f06824f..7c85339 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "jama-backend", - "version": "0.9.18", + "version": "0.9.20", "description": "TeamChat backend server", "main": "src/index.js", "scripts": { diff --git a/build.sh b/build.sh index b16b319..b7bbb41 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.9.18}" +VERSION="${1:-0.9.20}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="jama" diff --git a/frontend/package.json b/frontend/package.json index 0419707..77e1078 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "jama-frontend", - "version": "0.9.18", + "version": "0.9.20", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/src/components/BrandingModal.jsx b/frontend/src/components/BrandingModal.jsx index 1fb3006..d9dd05e 100644 --- a/frontend/src/components/BrandingModal.jsx +++ b/frontend/src/components/BrandingModal.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; import { api } from '../utils/api.js'; import { useToast } from '../contexts/ToastContext.jsx'; @@ -10,18 +10,216 @@ const COLOUR_SUGGESTIONS = [ '#1a73e8', '#a142f4', '#e53935', '#fa7b17', '#fdd835', '#34a853', ]; -// A single colour picker card: shows suggestions by default, Custom button opens native picker +// ── Colour math helpers ────────────────────────────────────────────────────── + +function hexToHsv(hex) { + const r = parseInt(hex.slice(1,3),16)/255; + const g = parseInt(hex.slice(3,5),16)/255; + const b = parseInt(hex.slice(5,7),16)/255; + const max = Math.max(r,g,b), min = Math.min(r,g,b), d = max - min; + let h = 0; + if (d !== 0) { + if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6; + else if (max === g) h = ((b - r) / d + 2) / 6; + else h = ((r - g) / d + 4) / 6; + } + return { h: h * 360, s: max === 0 ? 0 : d / max, v: max }; +} + +function hsvToHex(h, s, v) { + h = h / 360; + const i = Math.floor(h * 6); + const f = h * 6 - i; + const p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + let r, g, b; + switch (i % 6) { + case 0: r=v; g=t; b=p; break; case 1: r=q; g=v; b=p; break; + case 2: r=p; g=v; b=t; break; case 3: r=p; g=q; b=v; break; + case 4: r=t; g=p; b=v; break; default: r=v; g=p; b=q; + } + return '#' + [r,g,b].map(x => Math.round(x*255).toString(16).padStart(2,'0')).join(''); +} + +function isValidHex(h) { return /^#[0-9a-fA-F]{6}$/.test(h); } + +// ── SV (saturation/value) square ───────────────────────────────────────────── + +function SvSquare({ hue, s, v, onChange }) { + const canvasRef = useRef(null); + const dragging = useRef(false); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + const W = canvas.width, H = canvas.height; + // White → hue gradient (left→right) + const hGrad = ctx.createLinearGradient(0, 0, W, 0); + hGrad.addColorStop(0, '#fff'); + hGrad.addColorStop(1, `hsl(${hue},100%,50%)`); + ctx.fillStyle = hGrad; ctx.fillRect(0, 0, W, H); + // Transparent → black gradient (top→bottom) + const vGrad = ctx.createLinearGradient(0, 0, 0, H); + vGrad.addColorStop(0, 'transparent'); + vGrad.addColorStop(1, '#000'); + ctx.fillStyle = vGrad; ctx.fillRect(0, 0, W, H); + }, [hue]); + + const getPos = (e, canvas) => { + const r = canvas.getBoundingClientRect(); + const cx = (e.touches ? e.touches[0].clientX : e.clientX) - r.left; + const cy = (e.touches ? e.touches[0].clientY : e.clientY) - r.top; + return { + s: Math.max(0, Math.min(1, cx / r.width)), + v: Math.max(0, Math.min(1, 1 - cy / r.height)), + }; + }; + + const handle = (e) => { + e.preventDefault(); + const p = getPos(e, canvasRef.current); + onChange(p.s, p.v); + }; + + return ( +
+ { dragging.current = true; handle(e); }} + onMouseMove={e => { if (dragging.current) handle(e); }} + onMouseUp={() => { dragging.current = false; }} + onMouseLeave={() => { dragging.current = false; }} + onTouchStart={handle} onTouchMove={handle} + /> + {/* Cursor circle */} +
+
+ ); +} + +// ── Hue bar ─────────────────────────────────────────────────────────────────── + +function HueBar({ hue, onChange }) { + const barRef = useRef(null); + const dragging = useRef(false); + + const handle = (e) => { + e.preventDefault(); + const r = barRef.current.getBoundingClientRect(); + const cx = (e.touches ? e.touches[0].clientX : e.clientX) - r.left; + onChange(Math.max(0, Math.min(360, (cx / r.width) * 360))); + }; + + return ( +
+
{ dragging.current = true; handle(e); }} + onMouseMove={e => { if (dragging.current) handle(e); }} + onMouseUp={() => { dragging.current = false; }} + onMouseLeave={() => { dragging.current = false; }} + onTouchStart={handle} onTouchMove={handle} + /> +
+
+ ); +} + +// ── Custom HSV picker ───────────────────────────────────────────────────────── + +function CustomPicker({ initial, onSet, onBack }) { + const { h: ih, s: is, v: iv } = hexToHsv(initial); + const [hue, setHue] = useState(ih); + const [sat, setSat] = useState(is); + const [val, setVal] = useState(iv); + const [hexInput, setHexInput] = useState(initial); + const [hexError, setHexError] = useState(false); + + const current = hsvToHex(hue, sat, val); + + // Sync hex input when sliders change + useEffect(() => { setHexInput(current); setHexError(false); }, [current]); + + const handleHexInput = (e) => { + const v = e.target.value; + setHexInput(v); + if (isValidHex(v)) { + const { h, s, v: bv } = hexToHsv(v); + setHue(h); setSat(s); setVal(bv); + setHexError(false); + } else { + setHexError(true); + } + }; + + return ( +
+ { setSat(s); setVal(v); }} /> + + + {/* Preview + hex input */} +
+
+ + Chosen colour +
+ + {/* Actions */} +
+ + +
+
+ ); +} + +// ── ColourPicker card ───────────────────────────────────────────────────────── + function ColourPicker({ label, value, onChange, preview }) { const [mode, setMode] = useState('suggestions'); // 'suggestions' | 'custom' - const [draft, setDraft] = useState(value); - - // Keep draft in sync if parent value changes (e.g. reset) - useEffect(() => { setDraft(value); }, [value]); - - const handleSet = () => { - onChange(draft); - setMode('suggestions'); - }; return (
@@ -54,41 +252,18 @@ function ColourPicker({ label, value, onChange, preview }) { /> ))}
- )} {mode === 'custom' && ( -
- {/* Large label-wrapped swatch triggers the colour input — reliable on mobile and desktop */} - -
- - -
-
+ { onChange(hex); setMode('suggestions'); }} + onBack={() => setMode('suggestions')} + /> )}
);