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 ( +