fix(entries): size the multiline field from its content instead of a fixed height

A manual resize was lost on reopen: the row is rebuilt on every open, and remembering the height would not survive a restart either since localStorage is wiped when the ephemeral port changes the origin. Deriving it from the content removes the state entirely - the field is always the right size.
This commit is contained in:
r-zakarya
2026-07-31 18:31:26 +01:00
parent 2cab20eb76
commit 9dc9f625d6
3 changed files with 18 additions and 3 deletions
+1 -1
View File
@@ -78,7 +78,7 @@ Préparer : 3 entrées `A`, `B`, `C`, dont `A` dans le dossier `Work` avec le ta
### A12 · pas de fausse alerte (non-régression d'A11) ### A12 · pas de fausse alerte (non-régression d'A11)
Aucun de ces enchaînements ne doit afficher « Entry changed elsewhere » : Aucun de ces enchaînements ne doit afficher « Entry changed elsewhere » :
- **A12a** : `A` ouverte + dirty → Save → modifier encore → Save (le rechargement post-save re-cale la référence) - **A12a** : `A` ouverte + dirty → Save → modifier encore → Save (le rechargement post-save re-cale la référence)✅
- **A12b** : `A` ouverte + dirty → batch *Add tag* / *Move to folder* sur `A` → Save (l'éditeur a absorbé le changement) - **A12b** : `A` ouverte + dirty → batch *Add tag* / *Move to folder* sur `A` → Save (l'éditeur a absorbé le changement)
- **A12c** : `A` ouverte + dirty → supprimer son dossier → Save (cf. A7) - **A12c** : `A` ouverte + dirty → supprimer son dossier → Save (cf. A7)
- **A12d** : `A` ouverte + dirty → *Fetch missing* favicons → Save (l'icône est hors payload éditeur) - **A12d** : `A` ouverte + dirty → *Fetch missing* favicons → Save (l'icône est hors payload éditeur)
+2 -1
View File
@@ -1604,11 +1604,12 @@ input[type="range"]::-webkit-slider-thumb {
/* Multiline custom field (Notes). The row stays a grid, so the textarea just /* Multiline custom field (Notes). The row stays a grid, so the textarea just
grows in its own column and the buttons keep their places. */ grows in its own column and the buttons keep their places. */
.so-custom-multiline { .so-custom-multiline {
min-height: 72px; min-height: 56px;
resize: vertical; resize: vertical;
align-self: stretch; align-self: stretch;
font-family: inherit; font-family: inherit;
line-height: 1.5; line-height: 1.5;
overflow-y: auto; /* JS caps the auto-grow; past that it scrolls */
} }
/* Editable combobox for option-backed custom fields (card brand, expiry /* Editable combobox for option-backed custom fields (card brand, expiry
year…). Input fills the cell; the arrow drops a menu of ALL options. */ year…). Input fills the cell; the arrow drops a menu of ALL options. */
+15 -1
View File
@@ -4161,6 +4161,12 @@ function soCustomFieldsField() {
return wrap; return wrap;
} }
// Grow a textarea to fit its content, up to a cap past which it scrolls.
function autoSizeTextarea(ta, maxPx = 320) {
ta.style.height = 'auto';
ta.style.height = Math.min(ta.scrollHeight, maxPx) + 'px';
}
function buildCustomFieldRow(field, idx, rerender) { function buildCustomFieldRow(field, idx, rerender) {
const row = el('div', { class: 'so-custom-row' }); const row = el('div', { class: 'so-custom-row' });
const labelInput = el('input', { const labelInput = el('input', {
@@ -4189,10 +4195,18 @@ function buildCustomFieldRow(field, idx, rerender) {
if (field.multiline) { if (field.multiline) {
valueInput = el('textarea', { valueInput = el('textarea', {
class: 'so-input so-custom-value so-custom-multiline', class: 'so-input so-custom-value so-custom-multiline',
rows: 5, placeholder: 'Value', rows: 3, placeholder: 'Value',
autocomplete: 'off', autocorrect: 'off', spellcheck: 'false', autocomplete: 'off', autocorrect: 'off', spellcheck: 'false',
}); });
valueInput.classList.toggle('is-masked', !!field.is_secret); valueInput.classList.toggle('is-masked', !!field.is_secret);
// Height follows the content instead of being remembered: a manual
// resize would be lost anyway (re-rendered on every open, and
// localStorage is wiped each launch when the port changes).
const grow = () => autoSizeTextarea(valueInput);
valueInput.addEventListener('input', grow);
// scrollHeight reads 0 until the element is in the DOM — same
// render-time vs DOM-attachment trap as renderSoChips (CLAUDE.md).
setTimeout(grow, 0);
} else { } else {
valueInput = el('input', { valueInput = el('input', {
type: field.is_secret ? 'password' : 'text', type: field.is_secret ? 'password' : 'text',