bug fixes

This commit is contained in:
2026-03-29 13:29:33 -04:00
parent 3c52b60907
commit 6d65b9af62
4 changed files with 40 additions and 39 deletions

View File

@@ -94,18 +94,18 @@ function TimeInputMobile({ value, onChange }) {
const parsed = parseTypedTime(raw); const parsed = parseTypedTime(raw);
if (parsed) { onChange(parsed); setInputVal(fmt12(parsed)); } if (parsed) { onChange(parsed); setInputVal(fmt12(parsed)); }
else setInputVal(fmt12(value)); else setInputVal(fmt12(value));
setOpen(false);
}; };
return ( return (
<div ref={wrapRef} style={{ position: 'relative', display: 'inline-block' }}> <div ref={wrapRef} style={{ position: 'relative', display: 'inline-block' }}>
<input <input
type="text"
value={inputVal} value={inputVal}
onChange={e => setInputVal(e.target.value)} onChange={e => setInputVal(e.target.value)}
onFocus={() => setOpen(true)} onFocus={() => setOpen(true)}
onBlur={e => setTimeout(() => commit(e.target.value), 150)} onBlur={e => setTimeout(() => commit(e.target.value), 150)}
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); commit(inputVal); } if (e.key === 'Escape') { setInputVal(fmt12(value)); setOpen(false); } }} onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); commit(inputVal); } if (e.key === 'Escape') { setInputVal(fmt12(value)); setOpen(false); } }}
autoComplete="off" autoComplete="new-password"
style={{ fontSize: 15, color: 'var(--primary)', fontWeight: 600, background: 'transparent', border: 'none', outline: 'none', cursor: 'text', width: 90 }} style={{ fontSize: 15, color: 'var(--primary)', fontWeight: 600, background: 'transparent', border: 'none', outline: 'none', cursor: 'text', width: 90 }}
/> />
{open && ( {open && (
@@ -255,7 +255,7 @@ function RecurrenceSheet({ value, onChange, onClose }) {
<div style={{ marginBottom:16 }}> <div style={{ marginBottom:16 }}>
<div style={{ fontSize:12,color:'var(--text-tertiary)',marginBottom:8 }}>Repeats every</div> <div style={{ fontSize:12,color:'var(--text-tertiary)',marginBottom:8 }}>Repeats every</div>
<div style={{ display:'flex',gap:10 }}> <div style={{ display:'flex',gap:10 }}>
<input type="number" className="input" min={1} max={99} value={customRule.interval||1} onChange={e => upd('interval',Math.max(1,parseInt(e.target.value)||1))} autoComplete="off" style={{ width:70,textAlign:'center',fontSize:16 }}/> <input type="number" className="input" min={1} max={99} value={customRule.interval||1} onChange={e => upd('interval',Math.max(1,parseInt(e.target.value)||1))} autoComplete="new-password" style={{ width:70,textAlign:'center',fontSize:16 }}/>
<select className="input" value={customRule.unit||'week'} onChange={e=>upd('unit',e.target.value)} style={{ flex:1,fontSize:14 }}> <select className="input" value={customRule.unit||'week'} onChange={e=>upd('unit',e.target.value)} style={{ flex:1,fontSize:14 }}>
{['day','week','month','year'].map(u=><option key={u} value={u}>{u}{(customRule.interval||1)>1?'s':''}</option>)} {['day','week','month','year'].map(u=><option key={u} value={u}>{u}{(customRule.interval||1)>1?'s':''}</option>)}
</select> </select>
@@ -282,8 +282,8 @@ function RecurrenceSheet({ value, onChange, onClose }) {
{(customRule.ends||'never')===val&&<div style={{ width:10,height:10,borderRadius:'50%',background:'var(--primary)' }}/>} {(customRule.ends||'never')===val&&<div style={{ width:10,height:10,borderRadius:'50%',background:'var(--primary)' }}/>}
</div> </div>
<span style={{ flex:1,fontSize:15 }}>{lbl}</span> <span style={{ flex:1,fontSize:15 }}>{lbl}</span>
{val==='on'&&(customRule.ends||'never')==='on'&&<input type="date" className="input" value={customRule.endDate||''} onChange={e => upd('endDate',e.target.value)} autoComplete="off" style={{ width:150 }}/>} {val==='on'&&(customRule.ends||'never')==='on'&&<input type="date" className="input" value={customRule.endDate||''} onChange={e => upd('endDate',e.target.value)} autoComplete="new-password" style={{ width:150 }}/>}
{val==='after'&&(customRule.ends||'never')==='after'&&<><input type="number" className="input" min={1} max={999} value={customRule.endCount||13} onChange={e => upd('endCount',parseInt(e.target.value)||1)} autoComplete="off" style={{ width:64,textAlign:'center' }}/><span style={{ fontSize:13,color:'var(--text-tertiary)' }}>occurrences</span></>} {val==='after'&&(customRule.ends||'never')==='after'&&<><input type="number" className="input" min={1} max={999} value={customRule.endCount||13} onChange={e => upd('endCount',parseInt(e.target.value)||1)} autoComplete="new-password" style={{ width:64,textAlign:'center' }}/><span style={{ fontSize:13,color:'var(--text-tertiary)' }}>occurrences</span></>}
</div> </div>
))} ))}
</div> </div>
@@ -482,12 +482,12 @@ export default function MobileEventForm({ event, eventTypes, userGroups, selecte
<button onClick={handle} disabled={saving} style={{ background:'var(--primary)',border:'none',cursor:'pointer',color:'white',borderRadius:20,padding:'8px 20px',fontSize:14,fontWeight:700,opacity:saving?0.6:1 }}>{saving?'…':'Save'}</button> <button onClick={handle} disabled={saving} style={{ background:'var(--primary)',border:'none',cursor:'pointer',color:'white',borderRadius:20,padding:'8px 20px',fontSize:14,fontWeight:700,opacity:saving?0.6:1 }}>{saving?'…':'Save'}</button>
</div> </div>
{/* form wrapper suppresses Chrome Android's autofill chip bar; autoComplete="off" {/* form wrapper suppresses Chrome Android's autofill chip bar; autoComplete="new-password"
on individual inputs is ignored by Chrome but respected on the form element */} on individual inputs is ignored by Chrome but respected on the form element */}
<form autoComplete="off" onSubmit={e => e.preventDefault()} style={{ flex:1,overflowY:'auto' }}> <form autoComplete="new-password" onSubmit={e => e.preventDefault()} 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 => setTitle(e.target.value)} autoComplete="off" placeholder="Add title" 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" 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 */}
@@ -576,12 +576,12 @@ 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 => setLocation(e.target.value)} autoComplete="off" placeholder="Add location" 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" 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 */}
<MobileRow icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="21" y1="10" x2="3" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="21" y1="18" x2="3" y2="18"/></svg>} border={false}> <MobileRow icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="21" y1="10" x2="3" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="21" y1="18" x2="3" y2="18"/></svg>} border={false}>
<textarea value={description} onChange={e=>setDescription(e.target.value)} placeholder="Add description" rows={3} autoComplete="off" autoCorrect="off" spellCheck={false} style={{ width:'100%',border:'none',background:'transparent',fontSize:15,color:'var(--text-primary)',outline:'none',resize:'none' }}/> <textarea value={description} onChange={e=>setDescription(e.target.value)} placeholder="Add description" rows={3} autoComplete="new-password" autoCorrect="off" spellCheck={false} style={{ width:'100%',border:'none',background:'transparent',fontSize:15,color:'var(--text-primary)',outline:'none',resize:'none' }}/>
</MobileRow> </MobileRow>
{/* Delete */} {/* Delete */}
@@ -610,7 +610,7 @@ export default function MobileEventForm({ event, eventTypes, userGroups, selecte
<input <input
autoFocus autoFocus
value={newTypeName} value={newTypeName}
onChange={e => setNewTypeName(e.target.value)} autoComplete="off" onKeyDown={e=>e.key==='Enter'&&createEventType()} onChange={e => setNewTypeName(e.target.value)} autoComplete="new-password" onKeyDown={e=>e.key==='Enter'&&createEventType()}
placeholder="Type name…" autoCorrect="off" autoCapitalize="words" spellCheck={false} placeholder="Type name…" 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 }}>

View File

@@ -185,6 +185,7 @@ function TimeInput({ value, onChange, style }) {
return ( return (
<div ref={wrapRef} style={{ position: 'relative', ...style }}> <div ref={wrapRef} style={{ position: 'relative', ...style }}>
<input <input
type="text"
className="input" className="input"
value={inputVal} value={inputVal}
onChange={e => setInputVal(e.target.value)} onChange={e => setInputVal(e.target.value)}
@@ -195,7 +196,7 @@ function TimeInput({ value, onChange, style }) {
}} }}
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); commit(inputVal); } if (e.key === 'Escape') { setInputVal(fmt12(value)); setOpen(false); } }} onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); commit(inputVal); } if (e.key === 'Escape') { setInputVal(fmt12(value)); setOpen(false); } }}
style={{ width: '100%', cursor: 'text' }} style={{ width: '100%', cursor: 'text' }}
autoComplete="off" autoComplete="new-password"
placeholder="9:00 AM" placeholder="9:00 AM"
/> />
{open && ( {open && (
@@ -350,7 +351,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 => onFilterKeyword(e.target.value)} autoComplete="off" onFocus={onInputFocus} onBlur={onInputBlur} <input value={filterKeyword} onChange={e => onFilterKeyword(e.target.value)} autoComplete="new-password" onFocus={onInputFocus} onBlur={onInputBlur}
placeholder="Search events…" autoCorrect="off" autoCapitalize="off" spellCheck={false} placeholder="Search events…" 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>
@@ -389,7 +390,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 => setName(e.target.value)} autoComplete="off" autoCorrect="off" 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" autoCorrect="off" 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}}>
@@ -453,7 +454,7 @@ function CustomRecurrenceFields({ rule, onChange }) {
<div style={{border:'1px solid var(--border)',borderRadius:'var(--radius)',padding:12,display:'flex',flexDirection:'column',gap:10}}> <div style={{border:'1px solid var(--border)',borderRadius:'var(--radius)',padding:12,display:'flex',flexDirection:'column',gap:10}}>
<div style={{display:'flex',alignItems:'center',gap:8,fontSize:13}}> <div style={{display:'flex',alignItems:'center',gap:8,fontSize:13}}>
<span style={{color:'var(--text-tertiary)'}}>Every</span> <span style={{color:'var(--text-tertiary)'}}>Every</span>
<input type="number" className="input" min={1} max={99} value={rule.interval||1} onChange={e => upd('interval',Math.max(1,parseInt(e.target.value)||1))} autoComplete="off" style={{width:60,textAlign:'center'}}/> <input type="number" className="input" min={1} max={99} value={rule.interval||1} onChange={e => upd('interval',Math.max(1,parseInt(e.target.value)||1))} autoComplete="new-password" style={{width:60,textAlign:'center'}}/>
<select className="input" value={rule.unit||'week'} onChange={e=>upd('unit',e.target.value)} style={{flex:1}}> <select className="input" value={rule.unit||'week'} onChange={e=>upd('unit',e.target.value)} style={{flex:1}}>
{['day','week','month','year'].map(u=><option key={u} value={u}>{u}{(rule.interval||1)>1?'s':''}</option>)} {['day','week','month','year'].map(u=><option key={u} value={u}>{u}{(rule.interval||1)>1?'s':''}</option>)}
</select> </select>
@@ -475,8 +476,8 @@ function CustomRecurrenceFields({ rule, onChange }) {
<label key={val} style={{display:'flex',alignItems:'center',gap:10,marginBottom:6,fontSize:13,cursor:'pointer'}}> <label key={val} style={{display:'flex',alignItems:'center',gap:10,marginBottom:6,fontSize:13,cursor:'pointer'}}>
<input type="radio" name="recur_ends" checked={(rule.ends||'never')===val} onChange={()=>upd('ends',val)}/> <input type="radio" name="recur_ends" checked={(rule.ends||'never')===val} onChange={()=>upd('ends',val)}/>
{lbl} {lbl}
{val==='on' && (rule.ends||'never')==='on' && <input type="date" className="input" value={rule.endDate||''} onChange={e => upd('endDate',e.target.value)} autoComplete="off" style={{marginLeft:8,flex:1}}/>} {val==='on' && (rule.ends||'never')==='on' && <input type="date" className="input" value={rule.endDate||''} onChange={e => upd('endDate',e.target.value)} autoComplete="new-password" style={{marginLeft:8,flex:1}}/>}
{val==='after' && (rule.ends||'never')==='after' && <><input type="number" className="input" min={1} max={999} value={rule.endCount||13} onChange={e => upd('endCount',parseInt(e.target.value)||1)} autoComplete="off" style={{width:64,textAlign:'center',marginLeft:8}}/><span style={{color:'var(--text-tertiary)'}}>occurrences</span></>} {val==='after' && (rule.ends||'never')==='after' && <><input type="number" className="input" min={1} max={999} value={rule.endCount||13} onChange={e => upd('endCount',parseInt(e.target.value)||1)} autoComplete="new-password" style={{width:64,textAlign:'center',marginLeft:8}}/><span style={{color:'var(--text-tertiary)'}}>occurrences</span></>}
</label> </label>
))} ))}
</div> </div>
@@ -663,13 +664,13 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
return ( return (
<> <>
<div style={{width:'100%',maxWidth:1024,overflowX:'auto'}}> <div style={{width:'100%',maxWidth:1024,overflowX:'auto'}}>
{/* form wrapper suppresses Chrome Android's autofill chip bar; autoComplete="off" {/* form wrapper suppresses Chrome Android's autofill chip bar; autoComplete="new-password"
on individual inputs is ignored by Chrome but respected on the form element */} on individual inputs is ignored by Chrome but respected on the form element */}
<form autoComplete="off" onSubmit={e => e.preventDefault()}> <form autoComplete="new-password" onSubmit={e => e.preventDefault()}>
<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 => setTitle(e.target.value)} autoComplete="off" autoCorrect="off" autoCapitalize="sentences" style={{fontSize:20,fontWeight:700,border:'none',borderBottom:'2px solid var(--border)',borderRadius:0,padding:'4px 0',background:'transparent',width:'100%'}}/> <input className="input" placeholder="Add title" value={title} onChange={e => setTitle(e.target.value)} autoComplete="new-password" autoCorrect="off" autoCapitalize="sentences" 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 */}
@@ -688,7 +689,7 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
<FormRow label="Date & Time"> <FormRow label="Date & Time">
<div style={{display:'flex',flexDirection:'column',gap:8}}> <div style={{display:'flex',flexDirection:'column',gap:8}}>
<div style={{display:'flex',alignItems:'center',gap:8,flexWrap:'nowrap'}}> <div style={{display:'flex',alignItems:'center',gap:8,flexWrap:'nowrap'}}>
<input type="date" className="input" value={sd} onChange={e => setSd(e.target.value)} autoComplete="off" style={{width:150,flexShrink:0}}/> <input type="date" className="input" value={sd} onChange={e => setSd(e.target.value)} autoComplete="new-password" style={{width:150,flexShrink:0}}/>
{!allDay&&( {!allDay&&(
<> <>
<TimeInput value={st} onChange={setSt} style={{width:120,flexShrink:0}}/> <TimeInput value={st} onChange={setSt} style={{width:120,flexShrink:0}}/>
@@ -697,7 +698,7 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
setEt(newEt); userSetEndTime.current=true; setEt(newEt); userSetEndTime.current=true;
if(sd===ed && newEt<=st){ const d=new Date(buildISO(sd,st)); d.setDate(d.getDate()+1); const p=n=>String(n).padStart(2,'0'); setEd(`${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}`); } if(sd===ed && newEt<=st){ const d=new Date(buildISO(sd,st)); d.setDate(d.getDate()+1); const p=n=>String(n).padStart(2,'0'); setEd(`${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}`); }
}} style={{width:120,flexShrink:0}}/> }} style={{width:120,flexShrink:0}}/>
<input type="date" className="input" value={ed} onChange={e => {setEd(e.target.value);userSetEndTime.current=true;}} autoComplete="off" style={{width:150,flexShrink:0}}/> <input type="date" className="input" value={ed} onChange={e => {setEd(e.target.value);userSetEndTime.current=true;}} autoComplete="new-password" style={{width:150,flexShrink:0}}/>
</> </>
)} )}
</div> </div>
@@ -755,12 +756,12 @@ 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 => setLoc(e.target.value)} autoComplete="off" autoCorrect="off" autoCapitalize="off" /> <input className="input" placeholder="Add location" value={loc} onChange={e => setLoc(e.target.value)} autoComplete="new-password" autoCorrect="off" autoCapitalize="off" />
</FormRow> </FormRow>
{/* Description */} {/* Description */}
<FormRow label="Description"> <FormRow label="Description">
<textarea className="input" placeholder="Add description" value={desc} onChange={e=>setDesc(e.target.value)} rows={3} autoComplete="off" autoCorrect="off" style={{resize:'vertical'}}/> <textarea className="input" placeholder="Add description" value={desc} onChange={e=>setDesc(e.target.value)} rows={3} autoComplete="new-password" autoCorrect="off" style={{resize:'vertical'}}/>
</FormRow> </FormRow>
<div style={{display:'flex',gap:8,marginTop:8}}> <div style={{display:'flex',gap:8,marginTop:8}}>
@@ -993,7 +994,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 => setSheetName(e.target.value)} autoComplete="off" autoCorrect="off" onKeyDown={e=>e.key==='Enter'&&saveSheet()} placeholder="Type name…" <input autoFocus value={sheetName} onChange={e => setSheetName(e.target.value)} autoComplete="new-password" autoCorrect="off" 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>
@@ -1715,7 +1716,7 @@ 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 => { setFilterKeyword(e.target.value); if (!e.target.value) setFilterFromDate(null); }} autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false} onChange={e => { setFilterKeyword(e.target.value); if (!e.target.value) setFilterFromDate(null); }} 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"

View File

@@ -26,7 +26,7 @@ function UserCheckList({ allUsers, selectedIds, onChange, onIF, onIB }) {
.sort((a, b) => (a.display_name||a.name).localeCompare(b.display_name||b.name)); .sort((a, b) => (a.display_name||a.name).localeCompare(b.display_name||b.name));
return ( return (
<div> <div>
<input className="input" placeholder="Search users…" value={search} onChange={e => setSearch(e.target.value)} autoComplete="off" style={{ marginBottom:8 }} onFocus={onIF} onBlur={onIB} /> <input className="input" placeholder="Search users…" value={search} onChange={e => setSearch(e.target.value)} autoComplete="new-password" style={{ marginBottom:8 }} 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' }}>
@@ -201,7 +201,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 => setEditName(e.target.value)} autoComplete="off" placeholder="e.g. Coaches" style={{ marginTop:6 }} onFocus={onIF} onBlur={onIB} /> <input className="input" value={editName} onChange={e => setEditName(e.target.value)} autoComplete="new-password" placeholder="e.g. Coaches" style={{ marginTop:6 }} onFocus={onIF} onBlur={onIB} />
{isCreating && !noDm && <p style={{ fontSize:12, color:'var(--text-tertiary)', marginTop:5 }}>A matching Direct Message group will be created automatically.</p>} {isCreating && !noDm && <p style={{ fontSize:12, color:'var(--text-tertiary)', marginTop:5 }}>A matching Direct Message group will be created automatically.</p>}
<label style={{ display:'flex', alignItems:'center', gap:8, marginTop:8, cursor: (selected && selected.dm_group_id) ? 'not-allowed' : 'pointer', opacity: (selected && selected.dm_group_id) ? 0.5 : 1 }}> <label style={{ display:'flex', alignItems:'center', gap:8, marginTop:8, cursor: (selected && selected.dm_group_id) ? 'not-allowed' : 'pointer', opacity: (selected && selected.dm_group_id) ? 0.5 : 1 }}>
<input <input
@@ -376,7 +376,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 => setDmName(e.target.value)} autoComplete="off" placeholder="e.g. Coaches + Players" style={{ marginTop:6 }} 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 }} onFocus={onIF} onBlur={onIB} />
</div> </div>
<div> <div>
<label className="settings-section-label">Member Groups</label> <label className="settings-section-label">Member Groups</label>
@@ -602,7 +602,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 => setSearch(e.target.value)} autoComplete="off" style={{ marginBottom:8 }} onChange={e => setSearch(e.target.value)} autoComplete="new-password" style={{ marginBottom:8 }}
onFocus={onIF} onBlur={onIB} /> onFocus={onIF} onBlur={onIB} />
</div> </div>
@@ -741,9 +741,9 @@ export default function GroupManagerPage({ isMobile = false, onProfile, onHelp,
</div> </div>
)} )}
{/* form wrapper suppresses Chrome Android's autofill chip bar; autoComplete="off" {/* form wrapper suppresses Chrome Android's autofill chip bar; autoComplete="new-password"
on individual inputs is ignored by Chrome but respected on the form element */} on individual inputs is ignored by Chrome but respected on the form element */}
<form autoComplete="off" onSubmit={e => e.preventDefault()}> <form autoComplete="new-password" onSubmit={e => e.preventDefault()}>
{/* Content */} {/* Content */}
<div style={{ flex:1, display:'flex', overflow: isMobile ? 'auto' : 'hidden', paddingBottom: isMobile ? 'calc(82px + env(safe-area-inset-bottom, 0px))' : 0 }}> <div style={{ flex:1, display:'flex', overflow: isMobile ? 'auto' : 'hidden', paddingBottom: isMobile ? 'calc(82px + env(safe-area-inset-bottom, 0px))' : 0 }}>
{tab==='all' && <AllGroupsTab allUsers={allUsers} onRefresh={onRefresh} isMobile={isMobile} onIF={onIF} onIB={onIB} />} {tab==='all' && <AllGroupsTab allUsers={allUsers} onRefresh={onRefresh} isMobile={isMobile} onIF={onIF} onIB={onIB} />}

View File

@@ -241,7 +241,7 @@ function UserForm({ user, userPass, allUserGroups, onDone, onCancel, isMobile, o
value={email} onChange={e => setEmail(e.target.value)} value={email} onChange={e => setEmail(e.target.value)}
disabled={isEdit} disabled={isEdit}
style={{ width:'100%', ...(isEdit ? { opacity:0.6, cursor:'not-allowed' } : {}) }} style={{ width:'100%', ...(isEdit ? { opacity:0.6, cursor:'not-allowed' } : {}) }}
autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false" onFocus={onIF} onBlur={onIB} /> autoComplete="new-password" autoCorrect="off" autoCapitalize="off" spellCheck="false" onFocus={onIF} onBlur={onIB} />
</div> </div>
{/* Row 2: First Name + Last Name */} {/* Row 2: First Name + Last Name */}
@@ -250,13 +250,13 @@ function UserForm({ user, userPass, allUserGroups, onDone, onCancel, isMobile, o
{lbl('First Name', true)} {lbl('First Name', true)}
<input className="input" placeholder="Jane" <input className="input" placeholder="Jane"
value={firstName} onChange={e => setFirstName(e.target.value)} value={firstName} onChange={e => setFirstName(e.target.value)}
autoComplete="off" autoCapitalize="words" onFocus={onIF} onBlur={onIB} /> autoComplete="new-password" autoCapitalize="words" onFocus={onIF} onBlur={onIB} />
</div> </div>
<div> <div>
{lbl('Last Name', true)} {lbl('Last Name', true)}
<input className="input" placeholder="Smith" <input className="input" placeholder="Smith"
value={lastName} onChange={e => setLastName(e.target.value)} value={lastName} onChange={e => setLastName(e.target.value)}
autoComplete="off" autoCapitalize="words" onFocus={onIF} onBlur={onIB} /> autoComplete="new-password" autoCapitalize="words" onFocus={onIF} onBlur={onIB} />
</div> </div>
</div> </div>
@@ -266,7 +266,7 @@ function UserForm({ user, userPass, allUserGroups, onDone, onCancel, isMobile, o
{lbl('Phone', false, '(optional)')} {lbl('Phone', false, '(optional)')}
<input className="input" type="tel" placeholder="+1 555 000 0000" <input className="input" type="tel" placeholder="+1 555 000 0000"
value={phone} onChange={e => setPhone(e.target.value)} value={phone} onChange={e => setPhone(e.target.value)}
autoComplete="off" onFocus={onIF} onBlur={onIB} /> autoComplete="new-password" onFocus={onIF} onBlur={onIB} />
</div> </div>
<div> <div>
{lbl('App Role', true)} {lbl('App Role', true)}
@@ -621,9 +621,9 @@ export default function UserManagerPage({ isMobile = false, onProfile, onHelp, o
)} )}
{/* Content */} {/* Content */}
{/* form wrapper suppresses Chrome Android's autofill chip bar; autoComplete="off" {/* form wrapper suppresses Chrome Android's autofill chip bar; autoComplete="new-password"
on individual inputs is ignored by Chrome but respected on the form element */} on individual inputs is ignored by Chrome but respected on the form element */}
<form autoComplete="off" onSubmit={e => e.preventDefault()}> <form autoComplete="new-password" onSubmit={e => e.preventDefault()}>
<div style={{ flex:1, display:'flex', flexDirection:'column', overflow:'hidden', minHeight:0, background:'var(--background)' }}> <div style={{ flex:1, display:'flex', flexDirection:'column', overflow:'hidden', minHeight:0, background:'var(--background)' }}>
{/* LIST VIEW */} {/* LIST VIEW */}
@@ -632,7 +632,7 @@ export default function UserManagerPage({ isMobile = false, onProfile, onHelp, o
<div style={{ padding:'16px 16px 8px', flexShrink:0 }}> <div style={{ padding:'16px 16px 8px', flexShrink:0 }}>
<input className="input" placeholder="Search users…" value={search} onChange={e => setSearch(e.target.value)} <input className="input" placeholder="Search users…" value={search} onChange={e => setSearch(e.target.value)}
onFocus={onIF} onBlur={onIB} onFocus={onIF} onBlur={onIB}
autoComplete="off" autoCorrect="off" spellCheck={false} autoComplete="new-password" autoCorrect="off" spellCheck={false}
style={{ width:'100%', maxWidth: isMobile ? '100%' : 400 }} /> style={{ width:'100%', maxWidth: isMobile ? '100%' : 400 }} />
</div> </div>
<div style={{ flex:1, overflowY:'auto', padding:'0 16px', paddingBottom: isMobile ? 'calc(82px + env(safe-area-inset-bottom, 0px))' : 16, overscrollBehavior:'contain' }}> <div style={{ flex:1, overflowY:'auto', padding:'0 16px', paddingBottom: isMobile ? 'calc(82px + env(safe-area-inset-bottom, 0px))' : 16, overscrollBehavior:'contain' }}>