input fix for chome
This commit is contained in:
33
CLAUDE.md
33
CLAUDE.md
@@ -858,6 +858,39 @@ docker compose exec redis redis-cli monitor
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Mobile Input / Auto-Fill Fixes
|
||||||
|
|
||||||
|
### CSS (`index.css`)
|
||||||
|
|
||||||
|
**Auto-fill styling** — prevents browser yellow/blue autofill background from breaking the theme:
|
||||||
|
```css
|
||||||
|
input:-webkit-autofill {
|
||||||
|
-webkit-box-shadow: 0 0 0 1000px var(--surface) inset !important;
|
||||||
|
-webkit-text-fill-color: var(--text-primary) !important;
|
||||||
|
transition: background-color 5000s ease-in-out 0s !important;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prevent iOS zoom on input focus** — iOS zooms in if font-size < 16px:
|
||||||
|
```css
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
input:focus, textarea:focus, select:focus { font-size: 16px !important; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Input Attributes
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|---|---|
|
||||||
|
| `MessageInput.jsx` | `autoComplete="off"`, `autoCorrect="off"`, `autoCapitalize="sentences"`, `spellCheck="true"` on message textarea |
|
||||||
|
| `PasswordInput.jsx` | Default `autoComplete="new-password"`, `autoCorrect="off"`, `autoCapitalize="off"`, `spellCheck="false"` (callers can override via props) |
|
||||||
|
| `Login.jsx` | Email: `autoComplete="email"`, `autoCorrect="off"`, `autoCapitalize="off"`, `spellCheck="false"`; Password: `autoComplete="current-password"` |
|
||||||
|
| `ChangePassword.jsx` | Current password: `autoComplete="current-password"`; new/confirm: inherit `new-password` default |
|
||||||
|
| `UserManagerPage.jsx` | Email: `autoComplete="email"`; First/Last name: `given-name`/`family-name`; Phone: `autoComplete="tel"` |
|
||||||
|
| `GroupManagerPage.jsx` | Fixed duplicate `autoComplete` attributes; search/name inputs use `autoComplete="off"` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Phase 3 — Read Replicas (Future)
|
## Phase 3 — Read Replicas (Future)
|
||||||
|
|
||||||
When write load on Postgres becomes a bottleneck (typically >100,000 concurrent active users):
|
When write load on Postgres becomes a bottleneck (typically >100,000 concurrent active users):
|
||||||
|
|||||||
1026
CLAUDE_input_update.md
Normal file
1026
CLAUDE_input_update.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -386,6 +386,10 @@ export default function MessageInput({ group, replyTo, onCancelReply, onSend, on
|
|||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onFocus={onInputFocus}
|
onFocus={onInputFocus}
|
||||||
rows={1}
|
rows={1}
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="sentences"
|
||||||
|
spellCheck="true"
|
||||||
style={{ resize: 'none' }} />
|
style={{ resize: 'none' }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ export default function PasswordInput({ className, style, wrapperStyle, ...input
|
|||||||
return (
|
return (
|
||||||
<div style={{ position: 'relative', display: 'flex', alignItems: 'center', ...wrapperStyle }}>
|
<div style={{ position: 'relative', display: 'flex', alignItems: 'center', ...wrapperStyle }}>
|
||||||
<input
|
<input
|
||||||
|
autoComplete="new-password"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
spellCheck="false"
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
type={show ? 'text' : 'password'}
|
type={show ? 'text' : 'password'}
|
||||||
className={className ?? 'input'}
|
className={className ?? 'input'}
|
||||||
|
|||||||
@@ -51,6 +51,18 @@ a { color: inherit; text-decoration: none; }
|
|||||||
/* Focus */
|
/* Focus */
|
||||||
:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
|
:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
|
||||||
|
|
||||||
|
/* Auto-fill styling — prevents browser yellow/blue override from breaking the theme */
|
||||||
|
input:-webkit-autofill {
|
||||||
|
-webkit-box-shadow: 0 0 0 1000px var(--surface) inset !important;
|
||||||
|
-webkit-text-fill-color: var(--text-primary) !important;
|
||||||
|
transition: background-color 5000s ease-in-out 0s !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent iOS from zooming in on focused inputs (requires font-size >= 16px) */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
input:focus, textarea:focus, select:focus { font-size: 16px !important; }
|
||||||
|
}
|
||||||
|
|
||||||
/* Utils */
|
/* Utils */
|
||||||
.flex { display: flex; }
|
.flex { display: flex; }
|
||||||
.flex-col { display: flex; flex-direction: column; }
|
.flex-col { display: flex; flex-direction: column; }
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ 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>
|
||||||
<PasswordInput value={current} onChange={e => setCurrent(e.target.value)} autoComplete="new-password" required />
|
<PasswordInput value={current} onChange={e => setCurrent(e.target.value)} autoComplete="current-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>
|
||||||
|
|||||||
@@ -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="new-password" style={{ marginBottom:8 }} autoComplete="new-password" onFocus={onIF} onBlur={onIB} />
|
<input className="input" placeholder="Search users…" value={search} onChange={e => setSearch(e.target.value)} autoComplete="off" 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="new-password" placeholder="e.g. Coaches" style={{ marginTop:6 }} autoComplete="new-password" onFocus={onIF} onBlur={onIB} />
|
<input className="input" value={editName} onChange={e => setEditName(e.target.value)} autoComplete="off" 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
|
||||||
|
|||||||
@@ -93,11 +93,11 @@ export default function Login() {
|
|||||||
<form onSubmit={handleSubmit} className="login-form">
|
<form onSubmit={handleSubmit} className="login-form">
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label>Email</label>
|
<label>Email</label>
|
||||||
<input className="input" type="email" value={email} onChange={e => setEmail(e.target.value)} required autoFocus placeholder="your@email.com" />
|
<input className="input" type="email" value={email} onChange={e => setEmail(e.target.value)} required autoFocus placeholder="your@email.com" autoComplete="email" autoCorrect="off" autoCapitalize="off" spellCheck="false" />
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label>Password</label>
|
<label>Password</label>
|
||||||
<PasswordInput value={password} onChange={e => setPassword(e.target.value)} required placeholder="••••••••" />
|
<PasswordInput value={password} onChange={e => setPassword(e.target.value)} required placeholder="••••••••" autoComplete="current-password" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="remember-me">
|
<label className="remember-me">
|
||||||
|
|||||||
@@ -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="new-password" onFocus={onIF} onBlur={onIB} />
|
autoComplete="email" 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="new-password" autoCapitalize="words" onFocus={onIF} onBlur={onIB} />
|
autoComplete="given-name" 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="new-password" autoCapitalize="words" onFocus={onIF} onBlur={onIB} />
|
autoComplete="family-name" 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="new-password" onFocus={onIF} onBlur={onIB} />
|
autoComplete="tel" onFocus={onIF} onBlur={onIB} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{lbl('App Role', true)}
|
{lbl('App Role', true)}
|
||||||
|
|||||||
Reference in New Issue
Block a user