Modified - Finalized the manual-only password visibility toggle behavior for login and signup.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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}}">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user