v0.9.82 ui changes

This commit is contained in:
2026-03-18 19:49:36 -04:00
parent ca2d472837
commit 600abc1800
9 changed files with 37 additions and 19 deletions

View File

@@ -10,7 +10,7 @@
PROJECT_NAME=jama PROJECT_NAME=jama
# Image version to run (set by build.sh, or use 'latest') # Image version to run (set by build.sh, or use 'latest')
JAMA_VERSION=0.9.81 JAMA_VERSION=0.9.82
# App port — the host port Docker maps to the container # App port — the host port Docker maps to the container
PORT=3000 PORT=3000

View File

@@ -1,6 +1,6 @@
{ {
"name": "jama-backend", "name": "jama-backend",
"version": "0.9.81", "version": "0.9.82",
"description": "TeamChat backend server", "description": "TeamChat backend server",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {

View File

@@ -13,7 +13,7 @@
# ───────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────
set -euo pipefail set -euo pipefail
VERSION="${1:-0.9.81}" VERSION="${1:-0.9.82}"
ACTION="${2:-}" ACTION="${2:-}"
REGISTRY="${REGISTRY:-}" REGISTRY="${REGISTRY:-}"
IMAGE_NAME="jama" IMAGE_NAME="jama"

View File

@@ -1,6 +1,6 @@
{ {
"name": "jama-frontend", "name": "jama-frontend",
"version": "0.9.81", "version": "0.9.82",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -134,7 +134,7 @@ export default function GroupInfoModal({ group, onClose, onUpdated, onBack }) {
<div style={{ marginBottom: 16 }}> <div style={{ marginBottom: 16 }}>
{editing ? ( {editing ? (
<div className="flex gap-2"> <div className="flex gap-2">
<input className="input flex-1" value={newName} onChange={e => setNewName(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleRename()} /> <input className="input flex-1" value={newName} onChange={e => setNewName(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleRename()} autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false} />
<button className="btn btn-primary btn-sm" onClick={handleRename}>Save</button> <button className="btn btn-primary btn-sm" onClick={handleRename}>Save</button>
<button className="btn btn-secondary btn-sm" onClick={() => setEditing(false)}>Cancel</button> <button className="btn btn-secondary btn-sm" onClick={() => setEditing(false)}>Cancel</button>
</div> </div>
@@ -219,7 +219,7 @@ export default function GroupInfoModal({ group, onClose, onUpdated, onBack }) {
</div> </div>
{canManage && ( {canManage && (
<div style={{ marginTop: 12 }}> <div style={{ marginTop: 12 }}>
<input className="input" placeholder="Search to add member..." value={addSearch} onChange={e => setAddSearch(e.target.value)} /> <input className="input" placeholder="Search to add member..." autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false} value={addSearch} onChange={e => setAddSearch(e.target.value)} />
{addResults.length > 0 && addSearch && ( {addResults.length > 0 && addSearch && (
<div style={{ border: '1px solid var(--border)', borderRadius: 'var(--radius)', marginTop: 4, maxHeight: 150, overflowY: 'auto', background: 'var(--surface)' }}> <div style={{ border: '1px solid var(--border)', borderRadius: 'var(--radius)', marginTop: 4, maxHeight: 150, overflowY: 'auto', background: 'var(--surface)' }}>
{addResults.filter(u => !members.find(m => m.id === u.id)).map(u => ( {addResults.filter(u => !members.find(m => m.id === u.id)).map(u => (

View File

@@ -33,7 +33,11 @@ function MembersScreen({ group, allUsers, onBack }) {
}; };
useEffect(() => { loadMembers(); }, [group.id]); useEffect(() => { loadMembers(); }, [group.id]);
const [search, setSearch] = useState('');
const memberIds = new Set(members.map(m => m.id)); const memberIds = new Set(members.map(m => m.id));
const filteredUsers = search.trim()
? allUsers.filter(u => (u.display_name||u.name).toLowerCase().includes(search.toLowerCase()))
: allUsers;
const toggle = async (user) => { const toggle = async (user) => {
const nowMember = memberIds.has(user.id); const nowMember = memberIds.has(user.id);
@@ -62,8 +66,21 @@ function MembersScreen({ group, allUsers, onBack }) {
<div style={{ textAlign:'center',padding:40,color:'var(--text-tertiary)' }}>Loading</div> <div style={{ textAlign:'center',padding:40,color:'var(--text-tertiary)' }}>Loading</div>
) : ( ) : (
<div style={{ flex:1,overflowY:'auto' }}> <div style={{ flex:1,overflowY:'auto' }}>
<div style={{ padding:'10px 16px 4px',fontSize:11,fontWeight:700,color:'var(--text-tertiary)',textTransform:'uppercase',letterSpacing:'0.5px' }}>All Users</div> <div style={{ padding:'10px 16px 4px' }}>
{allUsers.map(u => { <div style={{ position:'relative' }}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="2" style={{position:'absolute',left:10,top:'50%',transform:'translateY(-50%)',pointerEvents:'none'}}><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input
value={search} onChange={e=>setSearch(e.target.value)}
placeholder="Search users…"
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false}
style={{width:'100%',padding:'8px 10px 8px 32px',border:'1px solid var(--border)',borderRadius:'var(--radius)',background:'var(--background)',color:'var(--text-primary)',fontSize:14,boxSizing:'border-box'}}
/>
</div>
</div>
<div style={{ padding:'4px 16px 4px',fontSize:11,fontWeight:700,color:'var(--text-tertiary)',textTransform:'uppercase',letterSpacing:'0.5px' }}>
{search ? `${filteredUsers.length} result${filteredUsers.length!==1?'s':''}` : 'All Users'}
</div>
{filteredUsers.map(u => {
const isMember = memberIds.has(u.id); const isMember = memberIds.has(u.id);
return ( return (
<div key={u.id} style={{ display:'flex',alignItems:'center',gap:12,padding:'11px 16px',borderBottom:'1px solid var(--border)' }}> <div key={u.id} style={{ display:'flex',alignItems:'center',gap:12,padding:'11px 16px',borderBottom:'1px solid var(--border)' }}>
@@ -129,7 +146,7 @@ function MultiGroupDmsScreen({ userGroups, onBack }) {
/> />
{creating && ( {creating && (
<div style={{ padding:16,background:'var(--surface)',borderBottom:'1px solid var(--border)' }}> <div style={{ padding:16,background:'var(--surface)',borderBottom:'1px solid var(--border)' }}>
<input autoFocus value={newName} onChange={e=>setNewName(e.target.value)} placeholder="DM name…" style={{ width:'100%',padding:'9px 12px',border:'1px solid var(--border)',borderRadius:'var(--radius)',background:'var(--background)',color:'var(--text-primary)',fontSize:15,marginBottom:10,boxSizing:'border-box' }}/> <input autoFocus value={newName} onChange={e=>setNewName(e.target.value)} placeholder="DM name…" autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false} style={{ width:'100%',padding:'9px 12px',border:'1px solid var(--border)',borderRadius:'var(--radius)',background:'var(--background)',color:'var(--text-primary)',fontSize:15,marginBottom:10,boxSizing:'border-box' }}/>
<div style={{ fontSize:12,color:'var(--text-tertiary)',marginBottom:6 }}>Select groups (min 2):</div> <div style={{ fontSize:12,color:'var(--text-tertiary)',marginBottom:6 }}>Select groups (min 2):</div>
{userGroups.map(g=>( {userGroups.map(g=>(
<label key={g.id} style={{ display:'flex',alignItems:'center',gap:10,padding:'8px 0',borderBottom:'1px solid var(--border)',cursor:'pointer' }}> <label key={g.id} style={{ display:'flex',alignItems:'center',gap:10,padding:'8px 0',borderBottom:'1px solid var(--border)',cursor:'pointer' }}>
@@ -224,7 +241,7 @@ export default function MobileGroupManager({ onClose }) {
<> <>
{creating && ( {creating && (
<div style={{ padding:'12px 16px',background:'var(--surface)',borderBottom:'1px solid var(--border)',display:'flex',gap:10 }}> <div style={{ padding:'12px 16px',background:'var(--surface)',borderBottom:'1px solid var(--border)',display:'flex',gap:10 }}>
<input autoFocus value={newName} onChange={e=>setNewName(e.target.value)} onKeyDown={e=>e.key==='Enter'&&createGroup()} placeholder="Group name…" style={{ flex:1,padding:'8px 12px',border:'1px solid var(--border)',borderRadius:'var(--radius)',background:'var(--background)',color:'var(--text-primary)',fontSize:15 }}/> <input autoFocus value={newName} onChange={e=>setNewName(e.target.value)} onKeyDown={e=>e.key==='Enter'&&createGroup()} placeholder="Group name…" autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false} style={{ flex:1,padding:'8px 12px',border:'1px solid var(--border)',borderRadius:'var(--radius)',background:'var(--background)',color:'var(--text-primary)',fontSize:15 }}/>
<button onClick={createGroup} disabled={saving||!newName.trim()} style={{ padding:'8px 16px',background:'var(--primary)',color:'white',border:'none',borderRadius:'var(--radius)',fontSize:14,fontWeight:600,cursor:'pointer' }}>{saving?'…':'Create'}</button> <button onClick={createGroup} disabled={saving||!newName.trim()} style={{ padding:'8px 16px',background:'var(--primary)',color:'white',border:'none',borderRadius:'var(--radius)',fontSize:14,fontWeight:600,cursor:'pointer' }}>{saving?'…':'Create'}</button>
<button onClick={()=>{setCreating(false);setNewName('');}} style={{ padding:'8px',background:'none',border:'none',cursor:'pointer',color:'var(--text-secondary)',fontSize:18 }}></button> <button onClick={()=>{setCreating(false);setNewName('');}} style={{ padding:'8px',background:'none',border:'none',cursor:'pointer',color:'var(--text-secondary)',fontSize:18 }}></button>
</div> </div>

View File

@@ -119,7 +119,8 @@ export default function ProfileModal({ onClose }) {
} }
}} }}
placeholder={user?.name} placeholder={user?.name}
style={{ borderColor: displayNameWarning ? '#e53935' : undefined }} autoComplete="off" autoCorrect="off" autoCapitalize="words" spellCheck={false}
style={ borderColor: displayNameWarning ? '#e53935' : undefined }
/> />
{displayName !== savedDisplayName ? null : savedDisplayName ? ( {displayName !== savedDisplayName ? null : savedDisplayName ? (
<button <button
@@ -137,7 +138,7 @@ export default function ProfileModal({ onClose }) {
</div> </div>
<div className="flex-col gap-1"> <div className="flex-col gap-1">
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>About Me</label> <label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>About Me</label>
<textarea className="input" value={aboutMe} onChange={e => setAboutMe(e.target.value)} placeholder="Tell your team about yourself..." rows={3} style={{ resize: 'vertical' }} /> <textarea className="input" value={aboutMe} onChange={e => setAboutMe(e.target.value)} placeholder="Tell your team about yourself..." rows={3} autoComplete="off" autoCorrect="off" spellCheck={false} style={{ resize: 'vertical' }} />
</div> </div>
{user?.role === 'admin' && ( {user?.role === 'admin' && (
<label className="flex items-center gap-2 text-sm pointer" style={{ color: 'var(--text-secondary)', userSelect: 'none' }}> <label className="flex items-center gap-2 text-sm pointer" style={{ color: 'var(--text-secondary)', userSelect: 'none' }}>

View File

@@ -1160,7 +1160,7 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
{/* Calendar or panel content */} {/* Calendar or panel content */}
<div style={{ flex:1, overflowY:'auto', overflowX: panel==='eventForm'?'auto':'hidden' }}> <div style={{ flex:1, overflowY:'auto', overflowX: panel==='eventForm'?'auto':'hidden' }}>
{panel === 'calendar' && view === 'schedule' && <ScheduleView events={events} selectedDate={selDate} onSelect={openDetail} filterKeyword={filterKeyword} filterTypeId={filterTypeId} isMobile={isMobile}/>} {panel === 'calendar' && view === 'schedule' && <div style={{paddingBottom: isMobile ? 80 : 0}}><ScheduleView events={events} selectedDate={selDate} onSelect={openDetail} filterKeyword={filterKeyword} filterTypeId={filterTypeId} isMobile={isMobile}/></div>}
{panel === 'calendar' && view === 'day' && <DayView events={events} selectedDate={selDate} onSelect={openDetail} onSwipe={isMobile ? dir => { const d=new Date(selDate); d.setDate(d.getDate()+dir); setSelDate(d); } : undefined}/>} {panel === 'calendar' && view === 'day' && <DayView events={events} selectedDate={selDate} onSelect={openDetail} onSwipe={isMobile ? dir => { const d=new Date(selDate); d.setDate(d.getDate()+dir); setSelDate(d); } : undefined}/>}
{panel === 'calendar' && view === 'week' && <WeekView events={events} selectedDate={selDate} onSelect={openDetail}/>} {panel === 'calendar' && view === 'week' && <WeekView events={events} selectedDate={selDate} onSelect={openDetail}/>}
{panel === 'calendar' && view === 'month' && <MonthView events={events} selectedDate={selDate} onSelect={openDetail} onSelectDay={d=>{setSelDate(d);setView('schedule');}}/>} {panel === 'calendar' && view === 'month' && <MonthView events={events} selectedDate={selDate} onSelect={openDetail} onSelectDay={d=>{setSelDate(d);setView('schedule');}}/>}
@@ -1193,9 +1193,9 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
)} )}
</div> </div>
{/* Mobile bottom bar — inside column wrapper so it sits at the bottom */} {/* Mobile bottom bar — position:fixed so keyboard doesn't push it up */}
{isMobile && ( {isMobile && (
<div style={{ background:'var(--surface)', borderTop:'1px solid var(--border)', flexShrink:0 }}> <div style={{ position:'fixed', bottom:0, left:0, right:0, zIndex:20, background:'var(--surface)', borderTop:'1px solid var(--border)' }}>
<UserFooter onProfile={onProfile} onHelp={onHelp} onAbout={onAbout} /> <UserFooter onProfile={onProfile} onHelp={onHelp} onAbout={onAbout} />
</div> </div>
)} )}

View File

@@ -302,7 +302,7 @@ export default function UserManagerModal({ onClose }) {
{/* Users list — accordion */} {/* Users list — accordion */}
{tab === 'users' && ( {tab === 'users' && (
<> <>
<input className="input" style={{ marginBottom: 12 }} placeholder="Search users…" value={search} onChange={e => setSearch(e.target.value)} /> <input className="input" style={{ marginBottom: 12 }} placeholder="Search users…" autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false} value={search} onChange={e => setSearch(e.target.value)} />
{loading ? ( {loading ? (
<div className="flex justify-center" style={{ padding: 40 }}><div className="spinner" /></div> <div className="flex justify-center" style={{ padding: 40 }}><div className="spinner" /></div>
) : ( ) : (
@@ -321,17 +321,17 @@ export default function UserManagerModal({ onClose }) {
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<div className="flex-col gap-1"> <div className="flex-col gap-1">
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Full Name <span style={{ fontWeight: 400, color: 'var(--text-tertiary)' }}>(First Last)</span></label> <label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Full Name <span style={{ fontWeight: 400, color: 'var(--text-tertiary)' }}>(First Last)</span></label>
<input className="input" placeholder="Jane Smith" value={form.name} onChange={e => setForm(p => ({ ...p, name: e.target.value }))} /> <input className="input" placeholder="Jane Smith" autoComplete="off" autoCorrect="off" autoCapitalize="words" spellCheck={false} value={form.name} onChange={e => setForm(p => ({ ...p, name: e.target.value }))} />
</div> </div>
<div className="flex-col gap-1"> <div className="flex-col gap-1">
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Email</label> <label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Email</label>
<input className="input" type="email" placeholder="jane@example.com" value={form.email} onChange={e => setForm(p => ({ ...p, email: e.target.value }))} /> <input className="input" type="email" placeholder="jane@example.com" autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false} value={form.email} onChange={e => setForm(p => ({ ...p, email: e.target.value }))} />
</div> </div>
</div> </div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 12 }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 12 }}>
<div className="flex-col gap-1"> <div className="flex-col gap-1">
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Temp Password <span style={{ fontWeight: 400, color: 'var(--text-tertiary)' }}>(blank = {userPass || 'USER_PASS'})</span></label> <label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Temp Password <span style={{ fontWeight: 400, color: 'var(--text-tertiary)' }}>(blank = {userPass || 'USER_PASS'})</span></label>
<input className="input" type="text" value={form.password} onChange={e => setForm(p => ({ ...p, password: e.target.value }))} /> <input className="input" type="text" autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false} value={form.password} onChange={e => setForm(p => ({ ...p, password: e.target.value }))} />
</div> </div>
<div className="flex-col gap-1"> <div className="flex-col gap-1">
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Role</label> <label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Role</label>