build.sh bug fixes

This commit is contained in:
2026-03-20 23:06:39 -04:00
parent 0a048271c6
commit 3ac72b7ac9
22 changed files with 87 additions and 156 deletions

View File

@@ -42,8 +42,7 @@ function TitleColourRow({ bgColor, bgLabel, textColor, onChange }) {
<CustomPicker <CustomPicker
initial={textColor} initial={textColor}
onSet={(hex) => { onChange(hex); setMode('idle'); }} onSet={(hex) => { onChange(hex); setMode('idle'); }}
onBack={() => setMode('idle')} onBack={() => setMode('idle')} />
/>
</div> </div>
)} )}
</div> </div>
@@ -130,8 +129,7 @@ function SvSquare({ hue, s, v, onChange }) {
onMouseMove={e => { if (dragging.current) handle(e); }} onMouseMove={e => { if (dragging.current) handle(e); }}
onMouseUp={() => { dragging.current = false; }} onMouseUp={() => { dragging.current = false; }}
onMouseLeave={() => { dragging.current = false; }} onMouseLeave={() => { dragging.current = false; }}
onTouchStart={handle} onTouchMove={handle} onTouchStart={handle} onTouchMove={handle} />
/>
{/* Cursor circle */} {/* Cursor circle */}
<div style={{ <div style={{
position: 'absolute', position: 'absolute',
@@ -173,8 +171,7 @@ function HueBar({ hue, onChange }) {
onMouseMove={e => { if (dragging.current) handle(e); }} onMouseMove={e => { if (dragging.current) handle(e); }}
onMouseUp={() => { dragging.current = false; }} onMouseUp={() => { dragging.current = false; }}
onMouseLeave={() => { dragging.current = false; }} onMouseLeave={() => { dragging.current = false; }}
onTouchStart={handle} onTouchMove={handle} onTouchStart={handle} onTouchMove={handle} />
/>
<div style={{ <div style={{
position: 'absolute', position: 'absolute',
left: `calc(${(hue / 360) * 100}% - 9px)`, left: `calc(${(hue / 360) * 100}% - 9px)`,
@@ -238,7 +235,7 @@ function CustomPicker({ initial, onSet, onBack }) {
width: 110, background: 'var(--surface)', width: 110, background: 'var(--surface)',
color: 'var(--text-primary)', color: 'var(--text-primary)',
}} }}
placeholder="#000000" autoComplete="new-password"> placeholder="#000000" autoComplete="new-password" >
<span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>Chosen colour</span> <span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>Chosen colour</span>
</div> </div>
@@ -287,8 +284,7 @@ function ColourPicker({ label, value, onChange, preview }) {
boxShadow: hex === value ? '0 0 0 2px var(--surface), 0 0 0 4px var(--text-primary)' : 'none', boxShadow: hex === value ? '0 0 0 2px var(--surface), 0 0 0 4px var(--text-primary)' : 'none',
transition: 'box-shadow 0.15s', transition: 'box-shadow 0.15s',
}} }}
title={hex} title={hex} />
/>
))} ))}
</div> </div>
<button className="btn btn-secondary btn-sm" onClick={() => setMode('custom')}> <button className="btn btn-secondary btn-sm" onClick={() => setMode('custom')}>
@@ -301,8 +297,7 @@ function ColourPicker({ label, value, onChange, preview }) {
<CustomPicker <CustomPicker
initial={value} initial={value}
onSet={(hex) => { onChange(hex); setMode('suggestions'); }} onSet={(hex) => { onChange(hex); setMode('suggestions'); }}
onBack={() => setMode('suggestions')} onBack={() => setMode('suggestions')} />
/>
)} )}
</div> </div>
); );
@@ -460,9 +455,7 @@ export default function BrandingModal({ onClose }) {
className="input flex-1" className="input flex-1"
value={appName} value={appName}
maxLength={16} maxLength={16}
onChange={e = autoComplete="new-password"> setAppName(e.target.value)} onChange={e => setAppName(e.target.value)} autoComplete="new-password" onKeyDown={e => e.key === 'Enter' && handleSaveName()} />
onKeyDown={e => e.key === 'Enter' && handleSaveName()}
/>
<button className="btn btn-primary btn-sm" onClick={handleSaveName} disabled={loading}>{loading ? '...' : 'Save'}</button> <button className="btn btn-primary btn-sm" onClick={handleSaveName} disabled={loading}>{loading ? '...' : 'Save'}</button>
</div> </div>
<p style={{ fontSize: 12, color: 'var(--text-tertiary)', marginTop: 6 }}> <p style={{ fontSize: 12, color: 'var(--text-tertiary)', marginTop: 6 }}>
@@ -513,14 +506,12 @@ export default function BrandingModal({ onClose }) {
bgColor="#f1f3f4" bgColor="#f1f3f4"
bgLabel="Light mode" bgLabel="Light mode"
textColor={colourTitle} textColor={colourTitle}
onChange={setColourTitle} onChange={setColourTitle} />
/>
<TitleColourRow <TitleColourRow
bgColor="#13131f" bgColor="#13131f"
bgLabel="Dark mode" bgLabel="Dark mode"
textColor={colourTitleDark} textColor={colourTitleDark}
onChange={setColourTitleDark} onChange={setColourTitleDark} />
/>
</div> </div>
</div> </div>
@@ -535,8 +526,7 @@ export default function BrandingModal({ onClose }) {
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'white', fontWeight: 700, fontSize: 15, flexShrink: 0, color: 'white', fontWeight: 700, fontSize: 15, flexShrink: 0,
}}>A</div> }}>A</div>
)} )} />
/>
</div> </div>
<div style={{ borderTop: '1px solid var(--border)', paddingTop: 20 }}> <div style={{ borderTop: '1px solid var(--border)', paddingTop: 20 }}>
@@ -550,8 +540,7 @@ export default function BrandingModal({ onClose }) {
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'white', fontWeight: 700, fontSize: 15, flexShrink: 0, color: 'white', fontWeight: 700, fontSize: 15, flexShrink: 0,
}}>B</div> }}>B</div>
)} )} />
/>
</div> </div>
<div style={{ borderTop: '1px solid var(--border)', paddingTop: 20 }}> <div style={{ borderTop: '1px solid var(--border)', paddingTop: 20 }}>

View File

@@ -283,8 +283,7 @@ export default function ChatWindow({ group, onBack, onGroupUpdated, onDirectMess
onReact={handleReact} onReact={handleReact}
onDirectMessage={handleDirectMessage} onDirectMessage={handleDirectMessage}
isDirect={isDirect} isDirect={isDirect}
onlineUserIds={onlineUserIds} onlineUserIds={onlineUserIds} />
/>
))} ))}
{typing.length > 0 && ( {typing.length > 0 && (
@@ -315,8 +314,7 @@ export default function ChatWindow({ group, onBack, onGroupUpdated, onDirectMess
group={group} group={group}
onClose={() => setShowInfo(false)} onClose={() => setShowInfo(false)}
onUpdated={(updatedGroup) => { setShowInfo(false); onGroupUpdated && onGroupUpdated(updatedGroup); }} onUpdated={(updatedGroup) => { setShowInfo(false); onGroupUpdated && onGroupUpdated(updatedGroup); }}
onBack={() => setShowInfo(false)} onBack={() => setShowInfo(false)} />
/>
)} )}
</> </>
); );

View File

@@ -131,7 +131,7 @@ export function ColourPicker({ value, onChange }) {
<input value={hexInput} onChange={handleHexInput} maxLength={7} placeholder="#000000" <input value={hexInput} onChange={handleHexInput} maxLength={7} placeholder="#000000"
style={{fontFamily:'monospace',fontSize:14,padding:'6px 10px',borderRadius:8, style={{fontFamily:'monospace',fontSize:14,padding:'6px 10px',borderRadius:8,
border:`1px solid ${hexError?'#e53935':'var(--border)'}`,width:110, border:`1px solid ${hexError?'#e53935':'var(--border)'}`,width:110,
background:'var(--surface)',color:'var(--text-primary)'}} autoComplete="new-password"> background:'var(--surface)',color:'var(--text-primary)'}} autoComplete="new-password" >
</div> </div>
<div style={{display:'flex',gap:8,marginTop:12}}> <div style={{display:'flex',gap:8,marginTop:12}}>
<button className="btn btn-primary btn-sm" onClick={()=>{onChange(current);setMode('suggestions');}} disabled={hexError}>Set</button> <button className="btn btn-primary btn-sm" onClick={()=>{onChange(current);setMode('suggestions');}} disabled={hexError}>Set</button>

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 = autoComplete="new-password"> setNewName(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleRename()} autoComplete="new-password" autoCorrect="off" autoCapitalize="off" spellCheck={false} /> <input className="input flex-1" value={newName} onChange={e => setNewName(e.target.value)} autoComplete="new-password" onKeyDown={e => e.key === 'Enter' && handleRename()} autoComplete="new-password" 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>
@@ -165,10 +165,8 @@ export default function GroupInfoModal({ group, onClose, onUpdated, onBack }) {
<input <input
className="input flex-1" className="input flex-1"
value={customName} value={customName}
onChange={e = autoComplete="new-password"> setCustomName(e.target.value)} onChange={e => setCustomName(e.target.value)} autoComplete="new-password" placeholder={group.owner_name_original || group.name}
placeholder={group.owner_name_original || group.name} onKeyDown={e => e.key === 'Enter' && handleCustomName()} />
onKeyDown={e => e.key === 'Enter' && handleCustomName()}
/>
{customName.trim() !== savedCustomName ? ( {customName.trim() !== savedCustomName ? (
<button className="btn btn-primary btn-sm" onClick={handleCustomName} disabled={savingCustom}> <button className="btn btn-primary btn-sm" onClick={handleCustomName} disabled={savingCustom}>
Save Save

View File

@@ -46,8 +46,7 @@ export default function HelpModal({ onClose, dismissed: initialDismissed }) {
) : ( ) : (
<div <div
className="help-markdown" className="help-markdown"
dangerouslySetInnerHTML={{ __html: marked.parse(content) }} dangerouslySetInnerHTML={{ __html: marked.parse(content) }} />
/>
)} )}
</div> </div>
@@ -57,8 +56,7 @@ export default function HelpModal({ onClose, dismissed: initialDismissed }) {
<input <input
type="checkbox" type="checkbox"
checked={dismissed} checked={dismissed}
onChange={handleDismissToggle} onChange={handleDismissToggle} />
/>
Do not show again at login Do not show again at login
</label> </label>
<button className="btn btn-primary btn-sm" onClick={onClose}>Close</button> <button className="btn btn-primary btn-sm" onClick={onClose}>Close</button>

View File

@@ -55,8 +55,7 @@ function FieldGroup({ label, children }) {
function Field({ label, value, onChange, placeholder, type = 'text', hint, required }) { function Field({ label, value, onChange, placeholder, type = 'text', hint, required }) {
return ( return (
<FieldGroup label={label}> <FieldGroup label={label}>
<input type={type} value={value} onChange={e = autoComplete="new-password"> onChange(e.target.value)} <input type={type} value={value} onChange={e => onChange(e.target.value)} autoComplete="new-password" placeholder={placeholder} required={required}
placeholder={placeholder} required={required}
autoComplete="new-password" autoCorrect="off" spellCheck={false} autoComplete="new-password" autoCorrect="off" spellCheck={false}
className="input" style={{ fontSize: 13 }} /> className="input" style={{ fontSize: 13 }} />
{hint && <span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>{hint}</span>} {hint && <span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>{hint}</span>}
@@ -230,7 +229,7 @@ function DeleteModal({ api, tenant, onClose, onDone }) {
Type <code style={{ background:'var(--background)', padding:'2px 6px', borderRadius:4 }}>{expected}</code> to confirm: Type <code style={{ background:'var(--background)', padding:'2px 6px', borderRadius:4 }}>{expected}</code> to confirm:
</p> </p>
{error && <div style={{ color:'var(--error)', fontSize:13, marginBottom:10 }}>{error}</div>} {error && <div style={{ color:'var(--error)', fontSize:13, marginBottom:10 }}>{error}</div>}
<input className="input" value={confirm} onChange={e = autoComplete="new-password"> setConfirm(e.target.value)} placeholder={expected} style={{ marginBottom:16 }} autoComplete="new-password" /> <input className="input" value={confirm} onChange={e => setConfirm(e.target.value)} autoComplete="new-password" placeholder={expected} style={{ marginBottom:16 }} autoComplete="new-password" />
<div style={{ display:'flex', justifyContent:'flex-end', gap:8 }}> <div style={{ display:'flex', justifyContent:'flex-end', gap:8 }}>
<button className="btn btn-secondary" onClick={onClose}>Cancel</button> <button className="btn btn-secondary" onClick={onClose}>Cancel</button>
<button className="btn btn-danger" onClick={handle} disabled={confirm !== expected || deleting}> <button className="btn btn-danger" onClick={handle} disabled={confirm !== expected || deleting}>
@@ -323,8 +322,7 @@ function KeyEntry({ onSubmit }) {
<h2 style={{ fontSize:18, fontWeight:700, margin:'0 0 4px' }}>Control Panel</h2> <h2 style={{ fontSize:18, fontWeight:700, margin:'0 0 4px' }}>Control Panel</h2>
<p style={{ color:'var(--text-secondary)', fontSize:13, margin:'0 0 20px' }}>Enter your host admin key to continue.</p> <p style={{ color:'var(--text-secondary)', fontSize:13, margin:'0 0 20px' }}>Enter your host admin key to continue.</p>
{error && <div style={{ padding:'8px 12px', background:'#fce8e6', color:'var(--error)', borderRadius:6, fontSize:13, marginBottom:14 }}>{error}</div>} {error && <div style={{ padding:'8px 12px', background:'#fce8e6', color:'var(--error)', borderRadius:6, fontSize:13, marginBottom:14 }}>{error}</div>}
<input type="password" className="input" value={key} onChange={e = autoComplete="new-password"> setKey(e.target.value)} <input type="password" className="input" value={key} onChange={e => setKey(e.target.value)} autoComplete="new-password" onKeyDown={e => e.key === 'Enter' && handle()} placeholder="Host admin key" autoFocus
onKeyDown={e => e.key === 'Enter' && handle()} placeholder="Host admin key" autoFocus
style={{ marginBottom:12, textAlign:'center' }} /> style={{ marginBottom:12, textAlign:'center' }} />
<button className="btn btn-primary" onClick={handle} disabled={checking} style={{ width:'100%', justifyContent:'center' }}> <button className="btn btn-primary" onClick={handle} disabled={checking} style={{ width:'100%', justifyContent:'center' }}>
{checking ? 'Checking…' : 'Unlock'} {checking ? 'Checking…' : 'Unlock'}
@@ -431,7 +429,7 @@ export default function HostPanel({ onProfile, onHelp, onAbout }) {
{/* Toolbar */} {/* Toolbar */}
<div style={{ padding:'0 24px 12px', flexShrink:0, display:'flex', gap:8, alignItems:'center', flexWrap:'wrap' }}> <div style={{ padding:'0 24px 12px', flexShrink:0, display:'flex', gap:8, alignItems:'center', flexWrap:'wrap' }}>
<input value={search} onChange={e = autoComplete="new-password"> setSearch(e.target.value)} placeholder="Search tenants…" <input value={search} onChange={e => setSearch(e.target.value)} autoComplete="new-password" placeholder="Search tenants…"
className="input" style={{ flex:1, minWidth:160, fontSize:13 }} autoComplete="new-password" /> className="input" style={{ flex:1, minWidth:160, fontSize:13 }} autoComplete="new-password" />
<button className="btn btn-secondary btn-sm" onClick={load} disabled={loading}>{loading ? '…' : '↻ Refresh'}</button> <button className="btn btn-secondary btn-sm" onClick={load} disabled={loading}>{loading ? '…' : '↻ Refresh'}</button>
<button className="btn btn-secondary btn-sm" onClick={handleMigrateAll} disabled={migrating}>{migrating ? 'Migrating…' : '⬆ Migrate All'}</button> <button className="btn btn-secondary btn-sm" onClick={handleMigrateAll} disabled={migrating}>{migrating ? 'Migrating…' : '⬆ Migrate All'}</button>

View File

@@ -77,8 +77,7 @@ export default function ImageLightbox({ src, onClose }) {
touchAction: 'pinch-zoom', touchAction: 'pinch-zoom',
boxShadow: '0 8px 40px rgba(0,0,0,0.6)', boxShadow: '0 8px 40px rgba(0,0,0,0.6)',
}} }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()} />
/>
</div>, </div>,
document.body document.body
); );

View File

@@ -261,16 +261,14 @@ export default function Message({ message: msg, prevMessage, currentUser, onRepl
src={msg.image_url} src={msg.image_url}
alt="attachment" alt="attachment"
className="msg-image" className="msg-image"
onClick={() => setLightboxSrc(msg.image_url)} onClick={() => setLightboxSrc(msg.image_url)} />
/>
)} )}
{msg.content && ( {msg.content && (
isEmojiOnly(msg.content) && !msg.image_url isEmojiOnly(msg.content) && !msg.image_url
? <p className="msg-text emoji-msg">{msg.content}</p> ? <p className="msg-text emoji-msg">{msg.content}</p>
: <p : <p
className="msg-text" className="msg-text"
dangerouslySetInnerHTML={{ __html: formatMsgContent(msg.content) }} dangerouslySetInnerHTML={{ __html: formatMsgContent(msg.content) }} />
/>
)} )}
{msg.link_preview && <LinkPreview data={msg.link_preview} />} {msg.link_preview && <LinkPreview data={msg.link_preview} />}
</div> </div>
@@ -303,8 +301,7 @@ export default function Message({ message: msg, prevMessage, currentUser, onRepl
user={msgUser} user={msgUser}
anchorEl={avatarRef.current} anchorEl={avatarRef.current}
onClose={() => setShowProfile(false)} onClose={() => setShowProfile(false)}
onDirectMessage={onDirectMessage} onDirectMessage={onDirectMessage} />
/>
)} )}
{lightboxSrc && ( {lightboxSrc && (
<ImageLightbox src={lightboxSrc} onClose={() => setLightboxSrc(null)} /> <ImageLightbox src={lightboxSrc} onClose={() => setLightboxSrc(null)} />

View File

@@ -353,8 +353,7 @@ export default function MessageInput({ group, replyTo, onCancelReply, onSend, on
theme="light" theme="light"
previewPosition="none" previewPosition="none"
skinTonePosition="none" skinTonePosition="none"
maxFrequentRows={2} maxFrequentRows={2} />
/>
</div> </div>
)} )}
@@ -367,8 +366,7 @@ export default function MessageInput({ group, replyTo, onCancelReply, onSend, on
onChange={handleChange} onChange={handleChange}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
rows={1} rows={1}
style={{ resize: 'none' }} style={{ resize: 'none' }} />
/>
</div> </div>
<button <button

View File

@@ -282,7 +282,7 @@ export default function MobileEventForm({ event, eventTypes, userGroups, selecte
<div style={{ flex:1,overflowY:'auto' }}> <div style={{ flex:1,overflowY:'auto' }}>
{/* Title */} {/* Title */}
<div style={{ padding:'16px 20px',borderBottom:'1px solid var(--border)' }}> <div style={{ padding:'16px 20px',borderBottom:'1px solid var(--border)' }}>
<input value={title} onChange={e= autoComplete="new-password">setTitle(e.target.value)} placeholder="Add title" autoComplete="new-password" autoCorrect="off" autoCapitalize="sentences" spellCheck={false} style={{ width:'100%',border:'none',background:'transparent',fontSize:22,fontWeight:700,color:'var(--text-primary)',outline:'none' }}/> <input value={title} onChange={e => setTitle(e.target.value)} autoComplete="new-password" placeholder="Add title" autoComplete="new-password" autoCorrect="off" autoCapitalize="sentences" spellCheck={false} style={{ width:'100%',border:'none',background:'transparent',fontSize:22,fontWeight:700,color:'var(--text-primary)',outline:'none' }}/>
</div> </div>
{/* Event Type */} {/* Event Type */}
@@ -364,7 +364,7 @@ export default function MobileEventForm({ event, eventTypes, userGroups, selecte
{/* Location */} {/* Location */}
<MobileRow icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>}> <MobileRow icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>}>
<input value={location} onChange={e= autoComplete="new-password">setLocation(e.target.value)} placeholder="Add location" autoComplete="new-password" autoCorrect="off" autoCapitalize="off" spellCheck={false} style={{ width:'100%',border:'none',background:'transparent',fontSize:15,color:'var(--text-primary)',outline:'none' }}/> <input value={location} onChange={e => setLocation(e.target.value)} autoComplete="new-password" placeholder="Add location" autoComplete="new-password" autoCorrect="off" autoCapitalize="off" spellCheck={false} style={{ width:'100%',border:'none',background:'transparent',fontSize:15,color:'var(--text-primary)',outline:'none' }}/>
</MobileRow> </MobileRow>
{/* Description */} {/* Description */}
@@ -397,11 +397,9 @@ export default function MobileEventForm({ event, eventTypes, userGroups, selecte
<input <input
autoFocus autoFocus
value={newTypeName} value={newTypeName}
onChange={e= autoComplete="new-password">setNewTypeName(e.target.value)} onChange={e => setNewTypeName(e.target.value)} autoComplete="new-password" onKeyDown={e=>e.key==='Enter'&&createEventType()}
onKeyDown={e=>e.key==='Enter'&&createEventType()}
placeholder="Type name…" autoComplete="new-password" autoCorrect="off" autoCapitalize="words" spellCheck={false} placeholder="Type name…" autoComplete="new-password" autoCorrect="off" autoCapitalize="words" spellCheck={false}
style={{ width:'100%',padding:'12px 14px',border:'1px solid var(--border)',borderRadius:'var(--radius)',fontSize:16,marginBottom:12,boxSizing:'border-box',background:'var(--background)',color:'var(--text-primary)' }} style={{ width:'100%',padding:'12px 14px',border:'1px solid var(--border)',borderRadius:'var(--radius)',fontSize:16,marginBottom:12,boxSizing:'border-box',background:'var(--background)',color:'var(--text-primary)' }} />
/>
<div style={{ display:'flex',alignItems:'center',gap:12,marginBottom:16 }}> <div style={{ display:'flex',alignItems:'center',gap:12,marginBottom:16 }}>
<label style={{ fontSize:14,color:'var(--text-tertiary)',flexShrink:0 }}>Colour</label> <label style={{ fontSize:14,color:'var(--text-tertiary)',flexShrink:0 }}>Colour</label>
<button onClick={()=>setShowTypeColourPicker(true)} style={{ flex:1,height:40,borderRadius:'var(--radius)',border:'2px solid var(--border)',background:newTypeColour,cursor:'pointer' }}/> <button onClick={()=>setShowTypeColourPicker(true)} style={{ flex:1,height:40,borderRadius:'var(--radius)',border:'2px solid var(--border)',background:newTypeColour,cursor:'pointer' }}/>

View File

@@ -60,8 +60,7 @@ function MembersScreen({ group, allUsers, onBack }) {
<Header <Header
title={group.name} title={group.name}
onBack={onBack} onBack={onBack}
right={<span style={{ fontSize:13,color:'var(--text-tertiary)' }}>{members.length} member{members.length!==1?'s':''}</span>} right={<span style={{ fontSize:13,color:'var(--text-tertiary)' }}>{members.length} member{members.length!==1?'s':''}</span>} />
/>
{loading ? ( {loading ? (
<div style={{ textAlign:'center',padding:40,color:'var(--text-tertiary)' }}>Loading</div> <div style={{ textAlign:'center',padding:40,color:'var(--text-tertiary)' }}>Loading</div>
) : ( ) : (
@@ -70,11 +69,9 @@ function MembersScreen({ group, allUsers, onBack }) {
<div style={{ position:'relative' }}> <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> <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 <input
value={search} onChange={e= autoComplete="new-password">setSearch(e.target.value)} value={search} onChange={e => setSearch(e.target.value)} autoComplete="new-password" placeholder="Search users…"
placeholder="Search users…"
autoComplete="new-password" autoCorrect="off" autoCapitalize="off" spellCheck={false} autoComplete="new-password" 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'}} 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> </div>
<div style={{ padding:'4px 16px 4px',fontSize:11,fontWeight:700,color:'var(--text-tertiary)',textTransform:'uppercase',letterSpacing:'0.5px' }}> <div style={{ padding:'4px 16px 4px',fontSize:11,fontWeight:700,color:'var(--text-tertiary)',textTransform:'uppercase',letterSpacing:'0.5px' }}>
@@ -142,11 +139,10 @@ function MultiGroupDmsScreen({ userGroups, onBack }) {
<Header <Header
title="Multi-Group DMs" title="Multi-Group DMs"
onBack={onBack} onBack={onBack}
right={<button onClick={()=>setCreating(v=>!v)} style={{ background:'none',border:'none',cursor:'pointer',color:'var(--primary)',fontSize:24,lineHeight:1,padding:0 }}>+</button>} right={<button onClick={()=>setCreating(v=>!v)} style={{ background:'none',border:'none',cursor:'pointer',color:'var(--primary)',fontSize:24,lineHeight:1,padding:0 }}>+</button>} />
/>
{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= autoComplete="new-password">setNewName(e.target.value)} placeholder="DM name…" autoComplete="new-password" 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' }}/> <input autoFocus value={newName} onChange={e => setNewName(e.target.value)} autoComplete="new-password" placeholder="DM name…" autoComplete="new-password" 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' }}>
@@ -225,8 +221,7 @@ export default function MobileGroupManager({ onClose }) {
<Header <Header
title="Group Manager" title="Group Manager"
onBack={onClose} onBack={onClose}
right={tab==='groups' && <button onClick={()=>setCreating(v=>!v)} style={{ background:'none',border:'none',cursor:'pointer',color:'var(--primary)',fontSize:24,lineHeight:1,padding:0 }}>+</button>} right={tab==='groups' && <button onClick={()=>setCreating(v=>!v)} style={{ background:'none',border:'none',cursor:'pointer',color:'var(--primary)',fontSize:24,lineHeight:1,padding:0 }}>+</button>} />
/>
{/* Tab bar */} {/* Tab bar */}
<div style={{ display:'flex',background:'var(--surface)',borderBottom:'1px solid var(--border)',flexShrink:0 }}> <div style={{ display:'flex',background:'var(--surface)',borderBottom:'1px solid var(--border)',flexShrink:0 }}>
@@ -241,7 +236,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= autoComplete="new-password">setNewName(e.target.value)} onKeyDown={e=>e.key==='Enter'&&createGroup()} placeholder="Group name…" autoComplete="new-password" 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 }}/> <input autoFocus value={newName} onChange={e => setNewName(e.target.value)} autoComplete="new-password" onKeyDown={e=>e.key==='Enter'&&createGroup()} placeholder="Group name…" autoComplete="new-password" 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

@@ -100,10 +100,8 @@ export default function NewChatModal({ onClose, onCreated }) {
<input <input
className="input" className="input"
value={name} value={name}
onChange={e = autoComplete="new-password"> setName(e.target.value)} onChange={e => setName(e.target.value)} autoComplete="new-password" placeholder={namePlaceholder}
placeholder={namePlaceholder} autoComplete="new-password" autoCorrect="off" autoCapitalize="words" spellCheck={false} />
autoComplete="new-password" autoCorrect="off" autoCapitalize="words" spellCheck={false}
/>
</div> </div>
)} )}

View File

@@ -107,7 +107,7 @@ export default function ProfileModal({ onClose }) {
<input <input
className="input flex-1" className="input flex-1"
value={displayName} value={displayName}
onChange={async e = autoComplete="new-password"> { onChange={async e = autoComplete="new-password" > {
const val = e.target.value; const val = e.target.value;
setDisplayName(val); setDisplayName(val);
setDisplayNameWarning(''); setDisplayNameWarning('');
@@ -120,8 +120,7 @@ export default function ProfileModal({ onClose }) {
}} }}
placeholder={user?.name} placeholder={user?.name}
autoComplete="new-password" autoCorrect="off" autoCapitalize="words" spellCheck={false} autoComplete="new-password" autoCorrect="off" autoCapitalize="words" spellCheck={false}
style={{ borderColor: displayNameWarning ? '#e53935' : undefined }} style={{ borderColor: displayNameWarning ? '#e53935' : undefined }} />
/>
{displayName !== savedDisplayName ? null : savedDisplayName ? ( {displayName !== savedDisplayName ? null : savedDisplayName ? (
<button <button
className="btn btn-sm" className="btn btn-sm"
@@ -146,8 +145,7 @@ export default function ProfileModal({ onClose }) {
type="checkbox" type="checkbox"
checked={hideAdminTag} checked={hideAdminTag}
onChange={e => setHideAdminTag(e.target.checked)} onChange={e => setHideAdminTag(e.target.checked)}
style={{ accentColor: 'var(--primary)', width: 16, height: 16 }} style={{ accentColor: 'var(--primary)', width: 16, height: 16 }} />
/>
Hide "Admin" tag next to my name in messages Hide "Admin" tag next to my name in messages
</label> </label>
)} )}
@@ -156,8 +154,7 @@ export default function ProfileModal({ onClose }) {
type="checkbox" type="checkbox"
checked={allowDm} checked={allowDm}
onChange={e => setAllowDm(e.target.checked)} onChange={e => setAllowDm(e.target.checked)}
style={{ accentColor: 'var(--primary)', width: 16, height: 16 }} style={{ accentColor: 'var(--primary)', width: 16, height: 16 }} />
/>
Allow others to send me direct messages Allow others to send me direct messages
</label> </label>
<button className="btn btn-primary" onClick={handleSaveProfile} disabled={loading}> <button className="btn btn-primary" onClick={handleSaveProfile} disabled={loading}>
@@ -170,15 +167,15 @@ export default function ProfileModal({ onClose }) {
<div className="flex-col gap-3"> <div className="flex-col gap-3">
<div className="flex-col gap-1"> <div className="flex-col gap-1">
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Current Password</label> <label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Current Password</label>
<input className="input" type="password" value={currentPw} onChange={e = autoComplete="new-password"> setCurrentPw(e.target.value)} /> <input className="input" type="password" value={currentPw} onChange={e => setCurrentPw(e.target.value)} autoComplete="new-password" />
</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)' }}>New Password</label> <label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>New Password</label>
<input className="input" type="password" value={newPw} onChange={e = autoComplete="new-password"> setNewPw(e.target.value)} /> <input className="input" type="password" value={newPw} onChange={e => setNewPw(e.target.value)} autoComplete="new-password" />
</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)' }}>Confirm New Password</label> <label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Confirm New Password</label>
<input className="input" type="password" value={confirmPw} onChange={e = autoComplete="new-password"> setConfirmPw(e.target.value)} /> <input className="input" type="password" value={confirmPw} onChange={e => setConfirmPw(e.target.value)} autoComplete="new-password" />
</div> </div>
<button className="btn btn-primary" onClick={handleChangePassword} disabled={loading || !currentPw || !newPw}> <button className="btn btn-primary" onClick={handleChangePassword} disabled={loading || !currentPw || !newPw}>
{loading ? 'Changing...' : 'Change Password'} {loading ? 'Changing...' : 'Change Password'}

View File

@@ -101,7 +101,7 @@ function EventTypePopup({ userGroups, onSave, onClose, editing = null }) {
<div style={{ position:'absolute', top:'100%', left:0, zIndex:200, background:'var(--surface)', border:'1px solid var(--border)', borderRadius:'var(--radius)', padding:16, width:280, boxShadow:'0 4px 20px rgba(0,0,0,0.15)' }}> <div style={{ position:'absolute', top:'100%', left:0, zIndex:200, background:'var(--surface)', border:'1px solid var(--border)', borderRadius:'var(--radius)', padding:16, width:280, boxShadow:'0 4px 20px rgba(0,0,0,0.15)' }}>
<div style={{ marginBottom:10 }}> <div style={{ marginBottom:10 }}>
<label className="settings-section-label">Type Name</label> <label className="settings-section-label">Type Name</label>
<input className="input" value={name} onChange={e= autoComplete="new-password">setName(e.target.value)} style={{ marginTop:4 }} autoFocus /> <input className="input" value={name} onChange={e => setName(e.target.value)} autoComplete="new-password" style={{ marginTop:4 }} autoFocus />
</div> </div>
<div style={{ marginBottom:10 }}> <div style={{ marginBottom:10 }}>
<label className="settings-section-label">Colour</label> <label className="settings-section-label">Colour</label>
@@ -195,8 +195,7 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
return ( return (
<div style={{ display:'flex', flexDirection:'column', gap:0 }}> <div style={{ display:'flex', flexDirection:'column', gap:0 }}>
{/* Title */} {/* Title */}
<input className="input" placeholder="Add title" value={title} onChange={e= autoComplete="new-password">setTitle(e.target.value)} <input className="input" placeholder="Add title" value={title} onChange={e => setTitle(e.target.value)} autoComplete="new-password" style={{ fontSize:18, fontWeight:600, marginBottom:16, border:'none', borderBottom:'2px solid var(--border)', borderRadius:0, padding:'4px 0' }} />
style={{ fontSize:18, fontWeight:600, marginBottom:16, border:'none', borderBottom:'2px solid var(--border)', borderRadius:0, padding:'4px 0' }} />
{/* Date/Time */} {/* Date/Time */}
<Row label=""> <Row label="">
@@ -265,7 +264,7 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
{/* Location */} {/* Location */}
<Row label="Location"> <Row label="Location">
<input className="input" placeholder="Add location" value={location} onChange={e= autoComplete="new-password">setLocation(e.target.value)} /> <input className="input" placeholder="Add location" value={location} onChange={e => setLocation(e.target.value)} autoComplete="new-password" />
</Row> </Row>
{/* Description */} {/* Description */}
@@ -867,8 +866,7 @@ export default function ScheduleManagerModal({ onClose, isToolManager }) {
currentUserId={user?.id} currentUserId={user?.id}
onClose={()=>setSelectedEvent(null)} onClose={()=>setSelectedEvent(null)}
onEdit={()=>{ setEditingEvent(selectedEvent); setRightPanel('eventForm'); setSelectedEvent(null); }} onEdit={()=>{ setEditingEvent(selectedEvent); setRightPanel('eventForm'); setSelectedEvent(null); }}
onAvailabilityChange={()=>openEventDetail(selectedEvent)} onAvailabilityChange={()=>openEventDetail(selectedEvent)} />
/>
)} )}
</div> </div>
); );

View File

@@ -162,7 +162,7 @@ function MobileScheduleFilter({ selected, onMonthChange, view, eventTypes, filte
<div style={{padding:'8px 12px 12px',borderTop:'1px solid var(--border)'}}> <div style={{padding:'8px 12px 12px',borderTop:'1px solid var(--border)'}}>
<div style={{position:'relative',marginBottom:8}}> <div style={{position:'relative',marginBottom:8}}>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="2" style={{position:'absolute',left:9,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> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="2" style={{position:'absolute',left:9,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={filterKeyword} onChange={e= autoComplete="new-password">onFilterKeyword(e.target.value)} onFocus={onInputFocus} onBlur={onInputBlur} <input value={filterKeyword} onChange={e => onFilterKeyword(e.target.value)} autoComplete="new-password" onFocus={onInputFocus} onBlur={onInputBlur}
placeholder="Search events…" autoComplete="new-password" autoCorrect="off" autoCapitalize="off" spellCheck={false} placeholder="Search events…" autoComplete="new-password" autoCorrect="off" autoCapitalize="off" spellCheck={false}
style={{width:'100%',padding:'7px 8px 7px 28px',border:'1px solid var(--border)',borderRadius:'var(--radius)',background:'var(--background)',color:'var(--text-primary)',fontSize:13,boxSizing:'border-box'}}/> style={{width:'100%',padding:'7px 8px 7px 28px',border:'1px solid var(--border)',borderRadius:'var(--radius)',background:'var(--background)',color:'var(--text-primary)',fontSize:13,boxSizing:'border-box'}}/>
</div> </div>
@@ -201,7 +201,7 @@ function EventTypePopup({ userGroups, onSave, onClose, editing=null }) {
}; };
return ( return (
<div style={{position:'absolute',top:'100%',left:0,zIndex:300,background:'var(--surface)',border:'1px solid var(--border)',borderRadius:'var(--radius)',padding:16,width:270,boxShadow:'0 4px 20px rgba(0,0,0,0.2)'}}> <div style={{position:'absolute',top:'100%',left:0,zIndex:300,background:'var(--surface)',border:'1px solid var(--border)',borderRadius:'var(--radius)',padding:16,width:270,boxShadow:'0 4px 20px rgba(0,0,0,0.2)'}}>
<div style={{marginBottom:8}}><label className="settings-section-label">Name</label><input className="input" value={name} onChange={e= autoComplete="new-password">setName(e.target.value)} style={{marginTop:4}} autoFocus/></div> <div style={{marginBottom:8}}><label className="settings-section-label">Name</label><input className="input" value={name} onChange={e => setName(e.target.value)} autoComplete="new-password" style={{marginTop:4}} autoFocus/></div>
<div style={{marginBottom:8}}><label className="settings-section-label">Colour</label><input type="color" value={colour} onChange={e=>setColour(e.target.value)} style={{marginTop:4,width:'100%',height:32,padding:2,borderRadius:4,border:'1px solid var(--border)'}}/></div> <div style={{marginBottom:8}}><label className="settings-section-label">Colour</label><input type="color" value={colour} onChange={e=>setColour(e.target.value)} style={{marginTop:4,width:'100%',height:32,padding:2,borderRadius:4,border:'1px solid var(--border)'}}/></div>
<div style={{marginBottom:8}}><label className="settings-section-label">Default Group</label><select className="input" value={groupId} onChange={e=>setGroupId(e.target.value)} style={{marginTop:4}}><option value="">None</option>{userGroups.map(g=><option key={g.id} value={g.id}>{g.name}</option>)}</select></div> <div style={{marginBottom:8}}><label className="settings-section-label">Default Group</label><select className="input" value={groupId} onChange={e=>setGroupId(e.target.value)} style={{marginTop:4}}><option value="">None</option>{userGroups.map(g=><option key={g.id} value={g.id}>{g.name}</option>)}</select></div>
<div style={{marginBottom:12}}> <div style={{marginBottom:12}}>
@@ -412,8 +412,7 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
<div style={{minWidth:500}} onKeyDown={e=>{if(e.key==='Enter'&&e.target.tagName!=='TEXTAREA') e.preventDefault();}}> <div style={{minWidth:500}} onKeyDown={e=>{if(e.key==='Enter'&&e.target.tagName!=='TEXTAREA') e.preventDefault();}}>
{/* Title */} {/* Title */}
<div style={{marginBottom:20}}> <div style={{marginBottom:20}}>
<input className="input" placeholder="Add title" value={title} onChange={e= autoComplete="new-password">setTitle(e.target.value)} <input className="input" placeholder="Add title" value={title} onChange={e => setTitle(e.target.value)} autoComplete="new-password" style={{fontSize:20,fontWeight:700,border:'none',borderBottom:'2px solid var(--border)',borderRadius:0,padding:'4px 0',background:'transparent',width:'100%'}}/>
style={{fontSize:20,fontWeight:700,border:'none',borderBottom:'2px solid var(--border)',borderRadius:0,padding:'4px 0',background:'transparent',width:'100%'}}/>
</div> </div>
{/* Event Type */} {/* Event Type */}
@@ -500,7 +499,7 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
{/* Location */} {/* Location */}
<FormRow label="Location"> <FormRow label="Location">
<input className="input" placeholder="Add location" value={loc} onChange={e= autoComplete="new-password">setLoc(e.target.value)}/> <input className="input" placeholder="Add location" value={loc} onChange={e => setLoc(e.target.value)} autoComplete="new-password" />
</FormRow> </FormRow>
{/* Description */} {/* Description */}
@@ -673,7 +672,7 @@ function EventTypesPanel({ eventTypes, userGroups, onUpdated, isMobile=false })
<span style={{fontWeight:700,fontSize:16}}>{sheetMode==='create'?'New Event Type':'Edit Event Type'}</span> <span style={{fontWeight:700,fontSize:16}}>{sheetMode==='create'?'New Event Type':'Edit Event Type'}</span>
<button onClick={closeSheet} style={{background:'none',border:'none',cursor:'pointer',color:'var(--text-secondary)',fontSize:20,lineHeight:1}}></button> <button onClick={closeSheet} style={{background:'none',border:'none',cursor:'pointer',color:'var(--text-secondary)',fontSize:20,lineHeight:1}}></button>
</div> </div>
<input autoFocus value={sheetName} onChange={e= autoComplete="new-password">setSheetName(e.target.value)} onKeyDown={e=>e.key==='Enter'&&saveSheet()} placeholder="Type name…" <input autoFocus value={sheetName} onChange={e => setSheetName(e.target.value)} autoComplete="new-password" onKeyDown={e=>e.key==='Enter'&&saveSheet()} placeholder="Type name…"
style={{width:'100%',padding:'12px 14px',border:'1px solid var(--border)',borderRadius:'var(--radius)',fontSize:16,marginBottom:12,boxSizing:'border-box',background:'var(--background)',color:'var(--text-primary)'}}/> style={{width:'100%',padding:'12px 14px',border:'1px solid var(--border)',borderRadius:'var(--radius)',fontSize:16,marginBottom:12,boxSizing:'border-box',background:'var(--background)',color:'var(--text-primary)'}}/>
<div style={{display:'flex',alignItems:'center',gap:12,marginBottom:16}}> <div style={{display:'flex',alignItems:'center',gap:12,marginBottom:16}}>
<label style={{fontSize:14,color:'var(--text-tertiary)',flexShrink:0}}>Colour</label> <label style={{fontSize:14,color:'var(--text-tertiary)',flexShrink:0}}>Colour</label>
@@ -1248,10 +1247,8 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
className="input" className="input"
placeholder={`Keyword… (space = OR, "phrase")`} placeholder={`Keyword… (space = OR, "phrase")`}
value={filterKeyword} value={filterKeyword}
onChange={e= autoComplete="new-password">setFilterKeyword(e.target.value)} onChange={e => setFilterKeyword(e.target.value)} autoComplete="new-password" autoCorrect="off" autoCapitalize="off" spellCheck={false}
autoComplete="new-password" autoCorrect="off" autoCapitalize="off" spellCheck={false} style={{ marginBottom:8, fontSize:13 }} />
style={{ marginBottom:8, fontSize:13 }}
/>
<select <select
className="input" className="input"
value={filterTypeId} value={filterTypeId}
@@ -1331,8 +1328,7 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
onMonthChange={(dir, exactDate) => { onMonthChange={(dir, exactDate) => {
if(exactDate) { setSelDate(exactDate); } if(exactDate) { setSelDate(exactDate); }
else { const d=new Date(selDate); d.setMonth(d.getMonth()+dir); d.setDate(1); setSelDate(d); } else { const d=new Date(selDate); d.setMonth(d.getMonth()+dir); d.setDate(1); setSelDate(d); }
}} }} />
/>
)} )}
{/* Calendar or panel content */} {/* Calendar or panel content */}
@@ -1394,8 +1390,7 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
isToolManager={isToolManager} isToolManager={isToolManager}
onSave={handleSaved} onSave={handleSaved}
onCancel={()=>{setPanel('calendar');setEditingEvent(null);setFilterKeyword('');setFilterTypeId('');}} onCancel={()=>{setPanel('calendar');setEditingEvent(null);setFilterKeyword('');setFilterTypeId('');}}
onDelete={handleDelete} onDelete={handleDelete} />
/>
</div> </div>
)} )}
@@ -1431,8 +1426,7 @@ export default function SchedulePage({ isToolManager, isMobile, onProfile, onHel
// Update the list so the "awaiting response" dot disappears immediately // Update the list so the "awaiting response" dot disappears immediately
setEvents(prev => prev.map(e => e.id === detailEvent.id ? {...e, my_response: resp} : e)); setEvents(prev => prev.map(e => e.id === detailEvent.id ? {...e, my_response: resp} : e));
openDetail(detailEvent); openDetail(detailEvent);
}} }} />
/>
)} )}
</div> </div>
); );

View File

@@ -153,7 +153,7 @@ function RegistrationTab({ onFeaturesChanged }) {
<div style={{ marginBottom: 16 }}> <div style={{ marginBottom: 16 }}>
<div className="settings-section-label">Serial Number</div> <div className="settings-section-label">Serial Number</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 6 }}>
<input className="input flex-1" value={serialNumber} readOnly style={{ fontFamily: 'monospace', letterSpacing: 1 }} autoComplete="new-password"> <input className="input flex-1" value={serialNumber} readOnly style={{ fontFamily: 'monospace', letterSpacing: 1 }} autoComplete="new-password" />
<button className="btn btn-secondary btn-sm" onClick={handleCopySerial} style={{ flexShrink: 0 }}> <button className="btn btn-secondary btn-sm" onClick={handleCopySerial} style={{ flexShrink: 0 }}>
{copied ? '✓ Copied' : ( {copied ? '✓ Copied' : (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
@@ -167,7 +167,7 @@ function RegistrationTab({ onFeaturesChanged }) {
<div className="settings-section-label">Registration Code</div> <div className="settings-section-label">Registration Code</div>
<div style={{ display: 'flex', gap: 8, marginTop: 6 }}> <div style={{ display: 'flex', gap: 8, marginTop: 6 }}>
<input className="input flex-1" placeholder="Enter registration code" value={regCode} <input className="input flex-1" placeholder="Enter registration code" value={regCode}
onChange={e = autoComplete="new-password"> setRegCode(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleRegister()} /> onChange={e => setRegCode(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleRegister()} autoComplete="new-password" />
<button className="btn btn-primary btn-sm" onClick={handleRegister} disabled={regLoading}> <button className="btn btn-primary btn-sm" onClick={handleRegister} disabled={regLoading}>
{regLoading ? '…' : 'Register'} {regLoading ? '…' : 'Register'}
</button> </button>

View File

@@ -100,10 +100,7 @@ export default function SupportModal({ onClose }) {
className="input" className="input"
placeholder="Jane Smith" placeholder="Jane Smith"
value={name} value={name}
onChange={e = autoComplete="new-password"> setName(e.target.value)} onChange={e => setName(e.target.value)} autoComplete="new-password" maxLength={100} />
maxLength={100}
/>
</div> </div>
<div className="flex-col gap-1"> <div className="flex-col gap-1">
@@ -113,9 +110,7 @@ export default function SupportModal({ onClose }) {
type="email" type="email"
placeholder="jane@example.com" placeholder="jane@example.com"
value={email} value={email}
onChange={e = autoComplete="new-password"> setEmail(e.target.value)} onChange={e => setEmail(e.target.value)} autoComplete="new-password" maxLength={200} />
maxLength={200}
/>
</div> </div>
<div className="flex-col gap-1"> <div className="flex-col gap-1">
@@ -127,8 +122,7 @@ export default function SupportModal({ onClose }) {
onChange={e => setMessage(e.target.value)} onChange={e => setMessage(e.target.value)}
rows={4} rows={4}
maxLength={2000} maxLength={2000}
style={{ resize: 'vertical' }} style={{ resize: 'vertical' }} />
/>
<span className="text-xs" style={{ color: 'var(--text-tertiary)', alignSelf: 'flex-end' }}> <span className="text-xs" style={{ color: 'var(--text-tertiary)', alignSelf: 'flex-end' }}>
{message.length}/2000 {message.length}/2000
</span> </span>
@@ -157,8 +151,7 @@ export default function SupportModal({ onClose }) {
onChange={e => setCaptchaAnswer(e.target.value)} onChange={e => setCaptchaAnswer(e.target.value)}
style={{ width: 90 }} style={{ width: 90 }}
min={0} min={0}
max={999} max={999} />
/>
<button <button
type="button" type="button"
className="btn-icon" className="btn-icon"

View File

@@ -125,10 +125,7 @@ function UserRow({ u, onUpdated }) {
className="input" className="input"
style={{ flex: 1, fontSize: 13, padding: '5px 8px' }} style={{ flex: 1, fontSize: 13, padding: '5px 8px' }}
value={nameVal} value={nameVal}
onChange={e = autoComplete="new-password"> setNameVal(e.target.value)} onChange={e => setNameVal(e.target.value)} autoComplete="new-password" onKeyDown={e => { if (e.key === 'Enter') handleSaveName(); if (e.key === 'Escape') { setEditName(false); setNameVal(u.name); } }} />
onKeyDown={e => { if (e.key === 'Enter') handleSaveName(); if (e.key === 'Escape') { setEditName(false); setNameVal(u.name); } }}
/>
<button className="btn btn-primary btn-sm" onClick={handleSaveName}>Save</button> <button className="btn btn-primary btn-sm" onClick={handleSaveName}>Save</button>
<button className="btn btn-secondary btn-sm" onClick={() => { setEditName(false); setNameVal(u.name); }}></button> <button className="btn btn-secondary btn-sm" onClick={() => { setEditName(false); setNameVal(u.name); }}></button>
</div> </div>
@@ -167,10 +164,7 @@ function UserRow({ u, onUpdated }) {
type="text" type="text"
placeholder="New password (min 6)" placeholder="New password (min 6)"
value={resetPw} value={resetPw}
onChange={e = autoComplete="new-password"> setResetPw(e.target.value)} onChange={e => setResetPw(e.target.value)} autoComplete="new-password" onKeyDown={e => { if (e.key === 'Enter') handleResetPw(); if (e.key === 'Escape') { setShowReset(false); setResetPw(''); } }} />
onKeyDown={e => { if (e.key === 'Enter') handleResetPw(); if (e.key === 'Escape') { setShowReset(false); setResetPw(''); } }}
/>
<button className="btn btn-primary btn-sm" onClick={handleResetPw}>Set</button> <button className="btn btn-primary btn-sm" onClick={handleResetPw}>Set</button>
<button className="btn btn-secondary btn-sm" onClick={() => { setShowReset(false); setResetPw(''); }}></button> <button className="btn btn-secondary btn-sm" onClick={() => { setShowReset(false); setResetPw(''); }}></button>
</div> </div>

View File

@@ -40,15 +40,15 @@ export default function ChangePassword() {
<form onSubmit={submit} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> <form onSubmit={submit} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div className="flex-col gap-1"> <div className="flex-col gap-1">
<label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Current Password</label> <label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Current Password</label>
<input className="input" type="password" value={current} onChange={e = autoComplete="new-password"> setCurrent(e.target.value)} required /> <input className="input" type="password" value={current} onChange={e => setCurrent(e.target.value)} autoComplete="new-password" required />
</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)' }}>New Password</label> <label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>New Password</label>
<input className="input" type="password" value={next} onChange={e = autoComplete="new-password"> setNext(e.target.value)} required /> <input className="input" type="password" value={next} onChange={e => setNext(e.target.value)} autoComplete="new-password" required />
</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)' }}>Confirm New Password</label> <label className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Confirm New Password</label>
<input className="input" type="password" value={confirm} onChange={e = autoComplete="new-password"> setConfirm(e.target.value)} required /> <input className="input" type="password" value={confirm} onChange={e => setConfirm(e.target.value)} autoComplete="new-password" required />
</div> </div>
<button className="btn btn-primary" type="submit" disabled={loading}> <button className="btn btn-primary" type="submit" disabled={loading}>
{loading ? 'Saving...' : 'Set New Password'} {loading ? 'Saving...' : 'Set New Password'}

View File

@@ -349,8 +349,7 @@ export default function Chat() {
onSettings={() => { setDrawerOpen(false); setModal('settings'); }} onSettings={() => { setDrawerOpen(false); setModal('settings'); }}
onUsers={() => { setDrawerOpen(false); setPage('users'); }} onUsers={() => { setDrawerOpen(false); setPage('users'); }}
onHostPanel={() => { setDrawerOpen(false); setPage('hostpanel'); }} onHostPanel={() => { setDrawerOpen(false); setPage('hostpanel'); }}
features={features} currentPage={page} isMobile={isMobile} features={features} currentPage={page} isMobile={isMobile} />
/>
{modal === 'profile' && <ProfileModal onClose={() => setModal(null)} />} {modal === 'profile' && <ProfileModal onClose={() => setModal(null)} />}
{modal === 'settings' && <SettingsModal onClose={() => setModal(null)} onFeaturesChanged={setFeatures} />} {modal === 'settings' && <SettingsModal onClose={() => setModal(null)} onFeaturesChanged={setFeatures} />}
{modal === 'branding' && <BrandingModal onClose={() => setModal(null)} />} {modal === 'branding' && <BrandingModal onClose={() => setModal(null)} />}
@@ -376,8 +375,7 @@ export default function Chat() {
onSettings={() => { setDrawerOpen(false); setModal('settings'); }} onSettings={() => { setDrawerOpen(false); setModal('settings'); }}
onUsers={() => { setDrawerOpen(false); setPage('users'); }} onUsers={() => { setDrawerOpen(false); setPage('users'); }}
onHostPanel={() => { setDrawerOpen(false); setPage('hostpanel'); }} onHostPanel={() => { setDrawerOpen(false); setPage('hostpanel'); }}
features={features} currentPage={page} isMobile={isMobile} features={features} currentPage={page} isMobile={isMobile} />
/>
{modal === 'profile' && <ProfileModal onClose={() => setModal(null)} />} {modal === 'profile' && <ProfileModal onClose={() => setModal(null)} />}
{modal === 'settings' && <SettingsModal onClose={() => setModal(null)} onFeaturesChanged={setFeatures} />} {modal === 'settings' && <SettingsModal onClose={() => setModal(null)} onFeaturesChanged={setFeatures} />}
{modal === 'branding' && <BrandingModal onClose={() => setModal(null)} />} {modal === 'branding' && <BrandingModal onClose={() => setModal(null)} />}
@@ -407,8 +405,7 @@ export default function Chat() {
onHostPanel={() => { setDrawerOpen(false); setPage('hostpanel'); }} onHostPanel={() => { setDrawerOpen(false); setPage('hostpanel'); }}
features={features} features={features}
currentPage={page} currentPage={page}
isMobile={isMobile} isMobile={isMobile} />
/>
{modal === 'profile' && <ProfileModal onClose={() => setModal(null)} />} {modal === 'profile' && <ProfileModal onClose={() => setModal(null)} />}
{modal === 'settings' && <SettingsModal onClose={() => setModal(null)} onFeaturesChanged={setFeatures} />} {modal === 'settings' && <SettingsModal onClose={() => setModal(null)} onFeaturesChanged={setFeatures} />}
</div> </div>
@@ -426,8 +423,7 @@ export default function Chat() {
features={features} features={features}
onProfile={() => setModal('profile')} onProfile={() => setModal('profile')}
onHelp={() => setModal('help')} onHelp={() => setModal('help')}
onAbout={() => setModal('about')} onAbout={() => setModal('about')} />
/>
</div> </div>
<NavDrawer <NavDrawer
open={drawerOpen} open={drawerOpen}
@@ -442,8 +438,7 @@ export default function Chat() {
onHostPanel={() => { setDrawerOpen(false); setPage('hostpanel'); }} onHostPanel={() => { setDrawerOpen(false); setPage('hostpanel'); }}
features={features} features={features}
currentPage={page} currentPage={page}
isMobile={isMobile} isMobile={isMobile} />
/>
{modal === 'profile' && <ProfileModal onClose={() => setModal(null)} />} {modal === 'profile' && <ProfileModal onClose={() => setModal(null)} />}
{modal === 'settings' && <SettingsModal onClose={() => setModal(null)} onFeaturesChanged={setFeatures} />} {modal === 'settings' && <SettingsModal onClose={() => setModal(null)} onFeaturesChanged={setFeatures} />}
{modal === 'branding' && <BrandingModal onClose={() => setModal(null)} />} {modal === 'branding' && <BrandingModal onClose={() => setModal(null)} />}
@@ -483,8 +478,7 @@ export default function Chat() {
isMobile={isMobile} isMobile={isMobile}
onAbout={() => setModal('about')} onAbout={() => setModal('about')}
onHelp={() => setModal('help')} onHelp={() => setModal('help')}
onlineUserIds={onlineUserIds} onlineUserIds={onlineUserIds} />
/>
)} )}
{(!isMobile || !showSidebar) && ( {(!isMobile || !showSidebar) && (
@@ -493,8 +487,7 @@ export default function Chat() {
onBack={isMobile ? () => { setShowSidebar(true); setActiveGroupId(null); } : null} onBack={isMobile ? () => { setShowSidebar(true); setActiveGroupId(null); } : null}
onGroupUpdated={loadGroups} onGroupUpdated={loadGroups}
onDirectMessage={(g) => { loadGroups(); selectGroup(g.id); }} onDirectMessage={(g) => { loadGroups(); selectGroup(g.id); }}
onlineUserIds={onlineUserIds} onlineUserIds={onlineUserIds} />
/>
)} )}
</div> </div>
@@ -511,8 +504,7 @@ export default function Chat() {
onHostPanel={() => { setDrawerOpen(false); setPage('hostpanel'); }} onHostPanel={() => { setDrawerOpen(false); setPage('hostpanel'); }}
features={features} features={features}
currentPage={page} currentPage={page}
isMobile={isMobile} isMobile={isMobile} />
/>
{modal === 'profile' && <ProfileModal onClose={() => setModal(null)} />} {modal === 'profile' && <ProfileModal onClose={() => setModal(null)} />}
{modal === 'settings' && <SettingsModal onClose={() => setModal(null)} onFeaturesChanged={setFeatures} />} {modal === 'settings' && <SettingsModal onClose={() => setModal(null)} onFeaturesChanged={setFeatures} />}
{modal === 'branding' && <BrandingModal onClose={() => setModal(null)} />} {modal === 'branding' && <BrandingModal onClose={() => setModal(null)} />}

View File

@@ -24,8 +24,7 @@ function UserCheckList({ allUsers, selectedIds, onChange, onIF, onIB }) {
const filtered = allUsers.filter(u => (u.display_name||u.name).toLowerCase().includes(search.toLowerCase())); const filtered = allUsers.filter(u => (u.display_name||u.name).toLowerCase().includes(search.toLowerCase()));
return ( return (
<div> <div>
<input className="input" placeholder="Search users…" value={search} onChange={e = autoComplete="new-password"> setSearch(e.target.value)} <input className="input" placeholder="Search users…" value={search} onChange={e => setSearch(e.target.value)} autoComplete="new-password" style={{ marginBottom:8 }} autoComplete="new-password" onFocus={onIF} onBlur={onIB} />
style={{ marginBottom:8 }} autoComplete="new-password" onFocus={onIF} onBlur={onIB} />
<div style={{ maxHeight:220, overflowY:'auto', border:'1px solid var(--border)', borderRadius:'var(--radius)' }}> <div style={{ maxHeight:220, overflowY:'auto', border:'1px solid var(--border)', borderRadius:'var(--radius)' }}>
{filtered.map(u => ( {filtered.map(u => (
<label key={u.id} style={{ display:'flex', alignItems:'center', gap:10, padding:'8px 12px', borderBottom:'1px solid var(--border)', cursor:'pointer' }}> <label key={u.id} style={{ display:'flex', alignItems:'center', gap:10, padding:'8px 12px', borderBottom:'1px solid var(--border)', cursor:'pointer' }}>
@@ -139,8 +138,7 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) {
<div style={{ display:'flex', flexDirection:'column', gap:18, maxWidth: isMobile ? '100%' : 520 }}> <div style={{ display:'flex', flexDirection:'column', gap:18, maxWidth: isMobile ? '100%' : 520 }}>
<div> <div>
<label className="settings-section-label">Group Name</label> <label className="settings-section-label">Group Name</label>
<input className="input" value={editName} onChange={e = autoComplete="new-password"> setEditName(e.target.value)} <input className="input" value={editName} onChange={e => setEditName(e.target.value)} autoComplete="new-password" placeholder="e.g. Coaches" style={{ marginTop:6 }} autoComplete="new-password" onFocus={onIF} onBlur={onIB} />
placeholder="e.g. Coaches" style={{ marginTop:6 }} autoComplete="new-password" onFocus={onIF} onBlur={onIB} />
{isCreating && <p style={{ fontSize:12, color:'var(--text-tertiary)', marginTop:5 }}>A matching Direct Message group will be created automatically.</p>} {isCreating && <p style={{ fontSize:12, color:'var(--text-tertiary)', marginTop:5 }}>A matching Direct Message group will be created automatically.</p>}
</div> </div>
<div> <div>
@@ -248,7 +246,7 @@ function DirectMessagesTab({ allUserGroups, onRefresh, refreshKey, isMobile = fa
<div style={{ display:'flex', flexDirection:'column', gap:18, maxWidth: isMobile ? '100%' : 520 }}> <div style={{ display:'flex', flexDirection:'column', gap:18, maxWidth: isMobile ? '100%' : 520 }}>
<div> <div>
<label className="settings-section-label">DM Name</label> <label className="settings-section-label">DM Name</label>
<input className="input" value={dmName} onChange={e = autoComplete="new-password"> setDmName(e.target.value)} placeholder="e.g. Coaches + Players" style={{ marginTop:6 }} autoComplete="new-password" onFocus={onIF} onBlur={onIB} /> <input className="input" value={dmName} onChange={e => setDmName(e.target.value)} autoComplete="new-password" placeholder="e.g. Coaches + Players" style={{ marginTop:6 }} autoComplete="new-password" onFocus={onIF} onBlur={onIB} />
</div> </div>
<div> <div>
<label className="settings-section-label">Member Groups</label> <label className="settings-section-label">Member Groups</label>
@@ -434,7 +432,7 @@ function U2URestrictionsTab({ allUserGroups, isMobile = false, onIF, onIB }) {
Allowed Groups <span style={{ fontWeight:400, color:'var(--text-tertiary)' }}>({otherGroups.length - blockedIds.size} of {otherGroups.length} allowed)</span> Allowed Groups <span style={{ fontWeight:400, color:'var(--text-tertiary)' }}>({otherGroups.length - blockedIds.size} of {otherGroups.length} allowed)</span>
</label> </label>
<input className="input" placeholder="Search groups…" value={search} <input className="input" placeholder="Search groups…" value={search}
onChange={e = autoComplete="new-password"> setSearch(e.target.value)} style={{ marginBottom:8 }} onChange={e => setSearch(e.target.value)} autoComplete="new-password" style={{ marginBottom:8 }}
autoComplete="new-password" onFocus={onIF} onBlur={onIB} /> autoComplete="new-password" onFocus={onIF} onBlur={onIB} />
</div> </div>

View File

@@ -99,8 +99,7 @@ function Input({ label, value, onChange, placeholder, type = 'text', required, h
fontSize: 14, outline: 'none', background: '#fff', color: '#202124', fontSize: 14, outline: 'none', background: '#fff', color: '#202124',
transition: 'border-color 0.15s' }} transition: 'border-color 0.15s' }}
onFocus={e => e.target.style.borderColor = '#1a73e8'} onFocus={e => e.target.style.borderColor = '#1a73e8'}
onBlur={e => e.target.style.borderColor = '#e0e0e0'} onBlur={e => e.target.style.borderColor = '#e0e0e0'} />
/>
{hint && <span style={{ fontSize: 11, color: '#9aa0a6' }}>{hint}</span>} {hint && <span style={{ fontSize: 11, color: '#9aa0a6' }}>{hint}</span>}
</div> </div>
); );