v0.12.40 update
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "rosterchirp-backend",
|
"name": "rosterchirp-backend",
|
||||||
"version": "0.12.39",
|
"version": "0.12.40",
|
||||||
"description": "RosterChirp backend server",
|
"description": "RosterChirp backend server",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
2
build.sh
2
build.sh
@@ -13,7 +13,7 @@
|
|||||||
# ─────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="${1:-0.12.39}"
|
VERSION="${1:-0.12.40}"
|
||||||
ACTION="${2:-}"
|
ACTION="${2:-}"
|
||||||
REGISTRY="${REGISTRY:-}"
|
REGISTRY="${REGISTRY:-}"
|
||||||
IMAGE_NAME="rosterchirp-dev"
|
IMAGE_NAME="rosterchirp-dev"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "rosterchirp-frontend",
|
"name": "rosterchirp-frontend",
|
||||||
"version": "0.12.39",
|
"version": "0.12.40",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -74,38 +74,46 @@ function TimeInputMobile({ value, onChange }) {
|
|||||||
const [inputVal, setInputVal] = useState(fmt12(value));
|
const [inputVal, setInputVal] = useState(fmt12(value));
|
||||||
const wrapRef = useRef(null);
|
const wrapRef = useRef(null);
|
||||||
const listRef = useRef(null);
|
const listRef = useRef(null);
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => { setInputVal(fmt12(value)); }, [value]);
|
useEffect(() => { setInputVal(fmt12(value)); }, [value]);
|
||||||
|
|
||||||
|
// Programmatically disable autocomplete after mount
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.setAttribute('autocomplete', 'off');
|
||||||
|
inputRef.current.setAttribute('autocorrect', 'off');
|
||||||
|
inputRef.current.setAttribute('autocapitalize', 'off');
|
||||||
|
inputRef.current.setAttribute('spellcheck', 'false');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !listRef.current) return;
|
if (!open || !listRef.current) return;
|
||||||
const idx = TIME_SLOTS.findIndex(s => s.value === value);
|
const idx = TIME_SLOTS.findIndex(s => s.value === value);
|
||||||
if (idx >= 0) listRef.current.scrollTop = idx * 40 - 40;
|
if (idx >= 0) listRef.current.scrollTop = idx * 40 - 40;
|
||||||
}, [open, value]);
|
}, [open, value]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) return;
|
|
||||||
const h = e => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); };
|
|
||||||
document.addEventListener('mousedown', h);
|
|
||||||
return () => document.removeEventListener('mousedown', h);
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
const commit = (raw) => {
|
const commit = (raw) => {
|
||||||
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
|
||||||
|
ref={inputRef}
|
||||||
type="text"
|
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="new-password"
|
autoComplete="nope"
|
||||||
|
data-autocomplete="off"
|
||||||
|
inputMode="none"
|
||||||
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 && (
|
||||||
@@ -484,7 +492,7 @@ export default function MobileEventForm({ event, eventTypes, userGroups, selecte
|
|||||||
|
|
||||||
{/* form wrapper suppresses Chrome Android's autofill chip bar; autoComplete="new-password"
|
{/* 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="new-password" onSubmit={e => e.preventDefault()} style={{ flex:1,overflowY:'auto' }}>
|
<form autoComplete="nope" 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="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' }}/>
|
<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' }}/>
|
||||||
|
|||||||
@@ -149,10 +149,21 @@ function TimeInput({ value, onChange, style }) {
|
|||||||
const [inputVal, setInputVal] = useState(fmt12(value));
|
const [inputVal, setInputVal] = useState(fmt12(value));
|
||||||
const wrapRef = useRef(null);
|
const wrapRef = useRef(null);
|
||||||
const listRef = useRef(null);
|
const listRef = useRef(null);
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
// Keep display in sync when value changes externally
|
// Keep display in sync when value changes externally
|
||||||
useEffect(() => { setInputVal(fmt12(value)); }, [value]);
|
useEffect(() => { setInputVal(fmt12(value)); }, [value]);
|
||||||
|
|
||||||
|
// Programmatically disable autocomplete after mount
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.setAttribute('autocomplete', 'off');
|
||||||
|
inputRef.current.setAttribute('autocorrect', 'off');
|
||||||
|
inputRef.current.setAttribute('autocapitalize', 'off');
|
||||||
|
inputRef.current.setAttribute('spellcheck', 'false');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Scroll the dropdown so the selected slot is near the top
|
// Scroll the dropdown so the selected slot is near the top
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !listRef.current) return;
|
if (!open || !listRef.current) return;
|
||||||
@@ -185,6 +196,7 @@ function TimeInput({ value, onChange, style }) {
|
|||||||
return (
|
return (
|
||||||
<div ref={wrapRef} style={{ position: 'relative', ...style }}>
|
<div ref={wrapRef} style={{ position: 'relative', ...style }}>
|
||||||
<input
|
<input
|
||||||
|
ref={inputRef}
|
||||||
type="text"
|
type="text"
|
||||||
className="input"
|
className="input"
|
||||||
value={inputVal}
|
value={inputVal}
|
||||||
@@ -196,7 +208,9 @@ 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="new-password"
|
autoComplete="nope"
|
||||||
|
data-autocomplete="off"
|
||||||
|
inputMode="none"
|
||||||
placeholder="9:00 AM"
|
placeholder="9:00 AM"
|
||||||
/>
|
/>
|
||||||
{open && (
|
{open && (
|
||||||
@@ -666,7 +680,7 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc
|
|||||||
<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="new-password"
|
{/* 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="new-password" onSubmit={e => e.preventDefault()}>
|
<form autoComplete="nope" 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}}>
|
||||||
|
|||||||
@@ -38,6 +38,29 @@ html {
|
|||||||
font-size: 100%; /* inherits system font size — allows Android accessibility font scaling */
|
font-size: 100%; /* inherits system font size — allows Android accessibility font scaling */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Aggressively disable Chrome autocomplete on Android */
|
||||||
|
input:-webkit-autofill,
|
||||||
|
input:-webkit-autofill:hover,
|
||||||
|
input:-webkit-autofill:focus,
|
||||||
|
textarea:-webkit-autofill,
|
||||||
|
textarea:-webkit-autofill:hover,
|
||||||
|
textarea:-webkit-autofill:focus {
|
||||||
|
-webkit-box-shadow: 0 0 0 1000px white inset !important;
|
||||||
|
background-color: white !important;
|
||||||
|
background-image: none !important;
|
||||||
|
transition: background-color 5000s ease-in-out 0s !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disable autocomplete dropdown/panel */
|
||||||
|
input::-webkit-credentials-auto-fill-button,
|
||||||
|
input::-webkit-contacts-auto-fill-button {
|
||||||
|
display: none !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
position: absolute !important;
|
||||||
|
right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
button { font-family: var(--font); cursor: pointer; border: none; background: none; }
|
button { font-family: var(--font); cursor: pointer; border: none; background: none; }
|
||||||
input, textarea { font-family: var(--font); }
|
input, textarea { font-family: var(--font); }
|
||||||
a { color: inherit; text-decoration: none; }
|
a { color: inherit; text-decoration: none; }
|
||||||
|
|||||||
Reference in New Issue
Block a user