Modified - Finalized the manual-only password visibility toggle behavior for login and signup.

This commit is contained in:
2026-05-10 03:41:03 +00:00
parent 1525c9c8ee
commit cbedf15d3b
6 changed files with 189 additions and 21 deletions
+1
View File
@@ -72,6 +72,7 @@ For the structural baseline used by this context, see `./.ai-structure.md`.
- Persistent formatting rule: events such as `Update Codex Settings` or other Codex-only memory/preference adjustments must not be added to `.codex-history.md`. Only actual project code changes belong there.
- Persistent formatting rule: in `.codex-history.md`, project change entries must begin with the sequential numeric ID followed immediately by the timestamp in square brackets, using the format `N - [YYYY-MM-DD HH:MM:SS]`, for example `4 - [2026-04-16 03:11:08]`.
- Persistent formatting rule: the application version stored in `.codex-history.md` must use the real repository-derived application version format, without the `Version:` label, for example `v.1.27.0-dev-38-g4b334df6d4`.
- Persistent history rule: append new details to an existing `.codex-history.md` task only while they are consecutive follow-ups to the same problem. If unrelated tasks have already happened in between, record the new change as a new task even if it revisits the same area or feature.
## Useful Notes
+10
View File
@@ -670,3 +670,13 @@ Project Change ID[date-time] - application-version - Type - Summary:
- 2 - I call that helper immediately before the build-load, architecture, and build-tags interactive menus, so the user gets an audible prompt exactly when human input is needed.
- 3 - I extended the same helper so VS Code and code-server terminals also get a high-visibility fallback banner when browser-controlled terminal bells do not produce an audible sound.
- 4 - I extended the same helper again to send a Linux desktop notification through `notify-send` before each interactive menu, while keeping the existing audio and terminal fallbacks for environments where no notification daemon is available.
133 - [2026-05-09 22:32:57] - v1.27.0-dev-125-g1525c9c8ee - Type: Modified - Finalized the manual-only password visibility toggle behavior for login and signup.
- 1 - I removed the static eye button from the login and signup templates and now create it from `web_src/js/features/password-visibility.ts` only when the current password value qualifies for manual reveal.
- 2 - The final reveal rules are now strict: prefilled or browser-autofilled passwords never show the eye, appending characters to an already non-empty password still keeps it hidden, and the eye appears only after a trusted manual empty-to-non-empty entry or a full manual replacement of a selected existing value.
- 3 - Once the eye has legitimately appeared, it stays visible through continued typing until the password is cleared again, preserves focus plus caret/selection on click, and keeps the signup confirm-password field synchronized while reveal mode is active.
- 4 - I converted the toggle into an overlaid control inside the password field, hid the browser-native reveal buttons, and kept the final transparent eye-button background styling that was adjusted manually in `web_src/css/user.css`.
134 - [2026-05-09 23:58:22] - v1.27.0-dev-125-g1525c9c8ee - Type: Modified - Ignored the local frontend hash artifact.
- 1 - I added `/.frontend.hash` to `.gitignore` so local frontend rebuild hash churn no longer appears in the repository working tree.
- 2 - I removed `.frontend.hash` from Git tracking with `git rm --cached`, so the ignore rule now fully suppresses future local hash churn instead of only affecting newly untracked copies.
+1 -4
View File
@@ -22,11 +22,8 @@
<label for="password" class="tw-flex-1">{{ctx.Locale.Tr "password"}}</label>
<a href="{{AppSubUrl}}/user/forgot_password" tabindex="4">{{ctx.Locale.Tr "auth.forgot_password"}}</a>
</div>
<div class="ui action input" data-global-init="initPasswordVisibilityToggle">
<div class="ui input js-password-toggle-group" data-global-init="initPasswordVisibilityToggle" data-show-label="{{ctx.Locale.Tr "auth.show_password"}}" data-hide-label="{{ctx.Locale.Tr "auth.hide_password"}}">
<input id="password" name="password" type="password" value="{{.password}}" autocomplete="current-password" required tabindex="2" class="js-password-toggle-source">
<button class="ui icon button js-password-toggle" type="button" aria-label="{{ctx.Locale.Tr "auth.show_password"}}" title="{{ctx.Locale.Tr "auth.show_password"}}" data-show-label="{{ctx.Locale.Tr "auth.show_password"}}" data-hide-label="{{ctx.Locale.Tr "auth.hide_password"}}">
{{svg "octicon-eye"}}
</button>
</div>
</div>
{{end}}
+1 -4
View File
@@ -55,11 +55,8 @@
{{if not .DisablePassword}}
<div class="required field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
<label for="password">{{ctx.Locale.Tr "password"}}</label>
<div class="ui action input" data-global-init="initPasswordVisibilityToggle">
<div class="ui input js-password-toggle-group" data-global-init="initPasswordVisibilityToggle" data-show-label="{{ctx.Locale.Tr "auth.show_password"}}" data-hide-label="{{ctx.Locale.Tr "auth.hide_password"}}">
<input id="password" name="password" type="password" value="{{.password}}" autocomplete="new-password" required class="js-password-toggle-source">
<button class="ui icon button js-password-toggle" type="button" aria-label="{{ctx.Locale.Tr "auth.show_password"}}" title="{{ctx.Locale.Tr "auth.show_password"}}" data-show-label="{{ctx.Locale.Tr "auth.show_password"}}" data-hide-label="{{ctx.Locale.Tr "auth.hide_password"}}">
{{svg "octicon-eye"}}
</button>
</div>
</div>
<div class="required field js-password-toggle-confirm-field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}">
+52
View File
@@ -162,3 +162,55 @@
.notifications-item:hover .notifications-updated {
display: none;
}
.js-password-toggle-group {
position: relative;
display: block;
width: 100%;
}
.js-password-toggle-source::-ms-reveal,
.js-password-toggle-source::-ms-clear,
.js-password-toggle-source::-webkit-credentials-auto-fill-button {
display: none !important;
visibility: hidden !important;
pointer-events: none !important;
width: 0 !important;
height: 0 !important;
margin: 0 !important;
padding: 0 !important;
}
.js-password-toggle-group > input {
width: 100%;
min-width: 0;
}
.js-password-toggle-group.has-visible-toggle > input {
padding-right: 3rem !important;
}
.js-password-toggle-group.has-visible-toggle > .button {
position: absolute;
top: 50%;
right: 0.35rem;
transform: translateY(-50%);
min-width: 2.25rem;
height: calc(100% - 0.7rem);
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
background: transparent !important;
color: var(--color-text-light-2) !important;
border: none !important;
border-radius: var(--border-radius) !important;
box-shadow: none !important;
z-index: 1;
}
.js-password-toggle-group.has-visible-toggle > .button:hover,
.js-password-toggle-group.has-visible-toggle > .button:focus {
background: transparent !important;
color: var(--color-text-light) !important;
}
+124 -13
View File
@@ -8,14 +8,62 @@ export function initPasswordVisibility(): void {
function initPasswordVisibilityToggle(container: HTMLElement) {
const passwordInput = container.querySelector<HTMLInputElement>('.js-password-toggle-source');
const toggleButton = container.querySelector<HTMLButtonElement>('.js-password-toggle');
if (!passwordInput || !toggleButton) return;
if (!passwordInput) return;
const form = container.closest('form');
const confirmField = form?.querySelector<HTMLElement>('.js-password-toggle-confirm-field');
const confirmInput = form?.querySelector<HTMLInputElement>('.js-password-toggle-confirm');
const showLabel = toggleButton.getAttribute('data-show-label')!;
const hideLabel = toggleButton.getAttribute('data-hide-label')!;
const showLabel = container.getAttribute('data-show-label')!;
const hideLabel = container.getAttribute('data-hide-label')!;
let wasEnteredManually = false;
let pendingManualInput = false;
let valueWasEmptyBeforeManualInput = passwordInput.value === '';
let fullValueWasSelectedBeforeManualInput = false;
const toggleButton = document.createElement('button');
toggleButton.className = 'ui icon button js-password-toggle';
toggleButton.type = 'button';
const removeToggleButtons = () => {
for (const button of container.querySelectorAll<HTMLButtonElement>('.js-password-toggle')) {
button.remove();
}
};
const isAutofilled = () => {
try {
if (passwordInput.matches(':autofill')) return true;
} catch {}
try {
if (passwordInput.matches(':-webkit-autofill')) return true;
} catch {}
return false;
};
container.classList.remove('has-visible-toggle');
removeToggleButtons();
const syncToggleButton = () => {
const isVisible = passwordInput.type === 'text';
toggleButton.setAttribute('aria-label', isVisible ? hideLabel : showLabel);
toggleButton.title = isVisible ? hideLabel : showLabel;
toggleButton.innerHTML = isVisible ? svg('octicon-eye-closed') : svg('octicon-eye');
};
const restoreCaretPosition = (selectionStart: number | null, selectionEnd: number | null, selectionDirection: string | null) => {
passwordInput.focus({preventScroll: true});
if (selectionStart === null || selectionEnd === null) return;
const direction = selectionDirection === 'backward' || selectionDirection === 'forward' ? selectionDirection : 'none';
const applySelection = () => {
try {
passwordInput.setSelectionRange(selectionStart, selectionEnd, direction);
} catch {}
};
applySelection();
window.requestAnimationFrame(applySelection);
};
const syncConfirmField = () => {
const isVisible = passwordInput.type === 'text';
@@ -28,21 +76,84 @@ function initPasswordVisibilityToggle(container: HTMLElement) {
confirmInput.required = true;
}
}
toggleButton.setAttribute('aria-label', isVisible ? hideLabel : showLabel);
toggleButton.title = isVisible ? hideLabel : showLabel;
toggleButton.innerHTML = isVisible ? svg('octicon-eye-closed') : svg('octicon-eye');
syncToggleButton();
};
toggleButton.addEventListener('click', () => {
passwordInput.type = passwordInput.type === 'password' ? 'text' : 'password';
const syncToggleAvailability = () => {
const canRevealPassword = wasEnteredManually && Boolean(passwordInput.value);
container.classList.toggle('has-visible-toggle', canRevealPassword);
if (!canRevealPassword && passwordInput.type === 'text') {
passwordInput.type = 'password';
}
if (canRevealPassword) {
if (!container.contains(toggleButton)) {
removeToggleButtons();
container.append(toggleButton);
}
toggleButton.tabIndex = 0;
} else {
removeToggleButtons();
}
syncConfirmField();
};
toggleButton.addEventListener('pointerdown', (event) => {
if (!container.classList.contains('has-visible-toggle')) return;
event.preventDefault();
});
passwordInput.addEventListener('input', () => {
if (passwordInput.type === 'text' && confirmInput) {
confirmInput.value = passwordInput.value;
toggleButton.addEventListener('click', () => {
if (!container.classList.contains('has-visible-toggle')) return;
const selectionStart = passwordInput.selectionStart;
const selectionEnd = passwordInput.selectionEnd;
const selectionDirection = passwordInput.selectionDirection;
passwordInput.type = passwordInput.type === 'password' ? 'text' : 'password';
syncConfirmField();
restoreCaretPosition(selectionStart, selectionEnd, selectionDirection);
});
passwordInput.addEventListener('beforeinput', (event) => {
const manualInputTypes = new Set([
'insertText',
'insertReplacementText',
'insertFromPaste',
'insertFromDrop',
'insertCompositionText',
'deleteContentBackward',
'deleteContentForward',
'deleteByCut',
'deleteByDrag',
'deleteByComposition',
]);
pendingManualInput = event.isTrusted && manualInputTypes.has(event.inputType);
if (pendingManualInput) {
valueWasEmptyBeforeManualInput = passwordInput.value === '';
fullValueWasSelectedBeforeManualInput =
passwordInput.value.length > 0 &&
passwordInput.selectionStart === 0 &&
passwordInput.selectionEnd === passwordInput.value.length;
}
});
syncConfirmField();
passwordInput.addEventListener('input', (event) => {
if (pendingManualInput && passwordInput.value === '') {
wasEnteredManually = false;
} else if (
pendingManualInput &&
passwordInput.value !== '' &&
(wasEnteredManually || valueWasEmptyBeforeManualInput || fullValueWasSelectedBeforeManualInput)
) {
wasEnteredManually = true;
} else if (isAutofilled() || (!pendingManualInput && event.isTrusted && !valueWasEmptyBeforeManualInput)) {
wasEnteredManually = false;
}
pendingManualInput = false;
fullValueWasSelectedBeforeManualInput = false;
if (passwordInput.type === 'text' && confirmInput) {
confirmInput.value = passwordInput.value;
}
syncToggleAvailability();
});
syncToggleAvailability();
}