diff --git a/.env.example b/.env.example
index d9c2521..69cef98 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.15
+JAMA_VERSION=0.9.16
# App port — the host port Docker maps to the container
PORT=3000
diff --git a/backend/package.json b/backend/package.json
index 6995f75..b897f61 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "jama-backend",
- "version": "0.9.15",
+ "version": "0.9.16",
"description": "TeamChat backend server",
"main": "src/index.js",
"scripts": {
diff --git a/build.sh b/build.sh
index f7fe532..3a0ba63 100644
--- a/build.sh
+++ b/build.sh
@@ -13,7 +13,7 @@
# ─────────────────────────────────────────────────────────────
set -euo pipefail
-VERSION="${1:-0.9.15}"
+VERSION="${1:-0.9.16}"
ACTION="${2:-}"
REGISTRY="${REGISTRY:-}"
IMAGE_NAME="jama"
diff --git a/frontend/package.json b/frontend/package.json
index 5d9313f..e322eb5 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "jama-frontend",
- "version": "0.9.15",
+ "version": "0.9.16",
"private": true,
"scripts": {
"dev": "vite",
diff --git a/frontend/src/components/BrandingModal.jsx b/frontend/src/components/BrandingModal.jsx
index caee937..97dbee5 100644
--- a/frontend/src/components/BrandingModal.jsx
+++ b/frontend/src/components/BrandingModal.jsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useRef } from 'react';
import { api } from '../utils/api.js';
import { useToast } from '../contexts/ToastContext.jsx';
@@ -6,46 +6,124 @@ const DEFAULT_TITLE_COLOR = '#1a73e8';
const DEFAULT_PUBLIC_COLOR = '#1a73e8';
const DEFAULT_DM_COLOR = '#a142f4';
-function ColorSwatch({ color, title }) {
+const COLOUR_SUGGESTIONS = [
+ '#1a73e8','#a142f4','#e53935','#34a853','#fa7b17',
+ '#00897b','#e91e8c','#0097a7','#5c6bc0','#f4511e',
+ '#616161','#795548','#00acc1','#43a047','#fdd835',
+];
+
+// A single colour picker card: shows suggestions by default, Custom button opens native picker
+function ColourPicker({ label, description, value, onChange, preview }) {
+ const [mode, setMode] = useState('suggestions'); // 'suggestions' | 'custom'
+ const inputRef = useRef(null);
+
+ const handleCustomClick = () => {
+ setMode('custom');
+ // Open native colour picker immediately after switching
+ setTimeout(() => inputRef.current?.click(), 50);
+ };
+
return (
-
+
+
{label}
+ {description && (
+
{description}
+ )}
+
+ {/* Current colour preview */}
+
+ {preview
+ ? preview(value)
+ :
+ }
+
{value}
+
+
+ {mode === 'suggestions' && (
+ <>
+ {/* Suggestion swatches */}
+
+ {COLOUR_SUGGESTIONS.map(hex => (
+
+ {/* Custom button — styled to match suggestion swatches row */}
+
+ >
+ )}
+
+ {mode === 'custom' && (
+
+
+ onChange(e.target.value)}
+ style={{
+ width: 56, height: 44, padding: 2,
+ borderRadius: 8, border: '1px solid var(--border)',
+ cursor: 'pointer', background: 'none',
+ }}
+ />
+ {value}
+
+ {/* Back button — same style as Custom button */}
+
+
+
+
+ )}
+
);
}
export default function BrandingModal({ onClose }) {
const toast = useToast();
- const [tab, setTab] = useState('general'); // 'general' | 'colors'
+ const [tab, setTab] = useState('general'); // 'general' | 'colours'
const [settings, setSettings] = useState({});
const [appName, setAppName] = useState('');
const [loading, setLoading] = useState(false);
const [resetting, setResetting] = useState(false);
const [showResetConfirm, setShowResetConfirm] = useState(false);
- // Color state
- const [colorTitle, setColorTitle] = useState(DEFAULT_TITLE_COLOR);
- const [colorPublic, setColorPublic] = useState(DEFAULT_PUBLIC_COLOR);
- const [colorDm, setColorDm] = useState(DEFAULT_DM_COLOR);
- const [savingColors, setSavingColors] = useState(false);
+ const [colourTitle, setColourTitle] = useState(DEFAULT_TITLE_COLOR);
+ const [colourPublic, setColourPublic] = useState(DEFAULT_PUBLIC_COLOR);
+ const [colourDm, setColourDm] = useState(DEFAULT_DM_COLOR);
+ const [savingColours, setSavingColours] = useState(false);
useEffect(() => {
api.getSettings().then(({ settings }) => {
setSettings(settings);
setAppName(settings.app_name || 'jama');
- setColorTitle(settings.color_title || DEFAULT_TITLE_COLOR);
- setColorPublic(settings.color_avatar_public || DEFAULT_PUBLIC_COLOR);
- setColorDm(settings.color_avatar_dm || DEFAULT_DM_COLOR);
+ setColourTitle(settings.color_title || DEFAULT_TITLE_COLOR);
+ setColourPublic(settings.color_avatar_public || DEFAULT_PUBLIC_COLOR);
+ setColourDm(settings.color_avatar_dm || DEFAULT_DM_COLOR);
}).catch(() => {});
}, []);
- const notifySidebarRefresh = () => {
- window.dispatchEvent(new Event('jama:settings-changed'));
- };
+ const notifySidebarRefresh = () => window.dispatchEvent(new Event('jama:settings-changed'));
const handleSaveName = async () => {
if (!appName.trim()) return;
@@ -76,26 +154,26 @@ export default function BrandingModal({ onClose }) {
}
};
- const handleSaveColors = async () => {
- setSavingColors(true);
+ const handleSaveColours = async () => {
+ setSavingColours(true);
try {
await api.updateColors({
- colorTitle,
- colorAvatarPublic: colorPublic,
- colorAvatarDm: colorDm,
+ colorTitle: colourTitle,
+ colorAvatarPublic: colourPublic,
+ colorAvatarDm: colourDm,
});
setSettings(prev => ({
...prev,
- color_title: colorTitle,
- color_avatar_public: colorPublic,
- color_avatar_dm: colorDm,
+ color_title: colourTitle,
+ color_avatar_public: colourPublic,
+ color_avatar_dm: colourDm,
}));
- toast('Colors updated', 'success');
+ toast('Colours updated', 'success');
notifySidebarRefresh();
} catch (e) {
toast(e.message, 'error');
} finally {
- setSavingColors(false);
+ setSavingColours(false);
}
};
@@ -106,9 +184,9 @@ export default function BrandingModal({ onClose }) {
const { settings: fresh } = await api.getSettings();
setSettings(fresh);
setAppName(fresh.app_name || 'jama');
- setColorTitle(DEFAULT_TITLE_COLOR);
- setColorPublic(DEFAULT_PUBLIC_COLOR);
- setColorDm(DEFAULT_DM_COLOR);
+ setColourTitle(DEFAULT_TITLE_COLOR);
+ setColourPublic(DEFAULT_PUBLIC_COLOR);
+ setColourDm(DEFAULT_DM_COLOR);
toast('Settings reset to defaults', 'success');
notifySidebarRefresh();
setShowResetConfirm(false);
@@ -132,7 +210,7 @@ export default function BrandingModal({ onClose }) {
{/* Tabs */}
-
+
{tab === 'general' && (
@@ -146,11 +224,7 @@ export default function BrandingModal({ onClose }) {
border: '1px solid var(--border)', overflow: 'hidden', display: 'flex',
alignItems: 'center', justifyContent: 'center', flexShrink: 0
}}>
-
+
- {/* Reset + Version */}
+ {/* Reset */}
Reset
{!showResetConfirm ? (
-
+
) : (
-
+
This will reset the app name, logo and all colours to their install defaults. This cannot be undone.
@@ -200,9 +267,7 @@ export default function BrandingModal({ onClose }) {
)}
{settings.app_version && (
-
- v{settings.app_version}
-
+
v{settings.app_version}
)}
@@ -216,74 +281,50 @@ export default function BrandingModal({ onClose }) {
>
)}
- {tab === 'colors' && (
+ {tab === 'colours' && (
- {/* App Title Color */}
-
-
App Title Color
-
- The color of the app name shown in the top bar.
-
-
- setColorTitle(e.target.value)}
- style={{ width: 48, height: 40, padding: 2, borderRadius: 8, border: '1px solid var(--border)', cursor: 'pointer', background: 'none' }}
- />
-
- {colorTitle}
-
-
+
+
+
-
Public Message Avatar Color
-
- Background color for public channel avatars (users without a custom avatar).
-
-
-
setColorPublic(e.target.value)}
- style={{ width: 48, height: 40, padding: 2, borderRadius: 8, border: '1px solid var(--border)', cursor: 'pointer', background: 'none' }}
- />
-
A
-
{colorPublic}
-
-
+
(
+ B
+ )}
+ />
-
Direct Message Avatar Color
-
- Background color for private group and direct message avatars (users without a custom avatar).
-
-
-
setColorDm(e.target.value)}
- style={{ width: 48, height: 40, padding: 2, borderRadius: 8, border: '1px solid var(--border)', cursor: 'pointer', background: 'none' }}
- />
-
B
-
{colorDm}
-
-
-
-
-
-