diff --git a/README.md b/README.md index f4f9fb4..b2beeb6 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,11 @@ start.cmd запускает сервер и печатает адреса | **Переместить** | Вещь переезжает в другую коробку или место | | **Удалить** | Убирает запись совсем | +Найденное и содержимое коробок показывается плиткой, как витрина в магазине: +сколько колонок влезает в экран, столько и будет. Кнопки на карточках — +иконками: 📷 снять, 🖼 из галереи, ✏️ изменить, 🗑 убрать фото. При правке +карточка растягивается на всю ширину, чтобы поля были нормального размера. + Ввод структурный: **место → ёмкость → предмет**. Место и коробку выбираешь из списков, поэтому ИИ в самом ответственном месте не участвует и ошибиться разбором фразы негде. Новое место добавляется *внутрь* выбранного — так и diff --git a/templates/index.html b/templates/index.html index 2e5d5f0..900fe8a 100644 --- a/templates/index.html +++ b/templates/index.html @@ -85,6 +85,25 @@ .card { margin-top:12px; padding:13px; background:var(--card); border:1px solid var(--line); border-radius:var(--radius); } .card h3 { margin:0 0 4px; font-size:17px; } + + /* Плитка как в интернет-магазинах: сколько колонок влезет, столько и будет. + auto-fill с minmax — на телефоне обычно две, на планшете три-четыре. */ + .cards { display:grid; gap:10px; margin-top:12px; align-items:start; + grid-template-columns:repeat(auto-fill, minmax(150px, 1fr)); } + .cards .card { margin-top:0; padding:10px; } + .cards .card h3 { font-size:15px; line-height:1.25; } + .cards .addr { font-size:13px; } + .cards .meta { font-size:12px; } + /* Теги в плитке не показываем — они её распирают. Видны при правке. */ + .cards .card:not(.wide) .tags { display:none; } + /* Квадратные превью: ряд выглядит ровным, как витрина. */ + .cards .photo img { aspect-ratio:1/1; object-fit:cover; margin-top:8px; } + .cards .acts { gap:6px; } + .cards .acts button { min-width:0; flex:1 1 auto; padding:9px 2px; font-size:15px; } + /* Карточка с открытой правкой растягивается на всю ширину: в узкой колонке + поля ввода были бы непригодны. */ + .cards .card.wide { grid-column:1 / -1; } + .cards .card.wide .photo img { aspect-ratio:auto; object-fit:contain; } .addr { color:var(--accent); font-size:15px; } .addr.taken { color:var(--warn); } .meta { color:var(--dim); font-size:13px; margin-top:5px; } @@ -107,6 +126,7 @@ .tag.del { cursor:pointer; padding-right:6px; } .tag.del b { color:#e06c5a; margin-left:6px; font-weight:700; } .editHint { color:var(--dim); font-size:13px; margin:6px 0 0; } + .iconHint { font-size:13px; margin-top:8px; } .why { float:right; font-size:12px; color:var(--dim); } .pick { cursor:pointer; } .pick:active { border-color:var(--accent); } @@ -621,9 +641,11 @@ function actionBar(target, cardEl, hasPhoto) { const drop = document.createElement('button'); - // Камера и галерея — раздельно: одним полем оба сценария на Android не покрыть. + // Только иконки: карточка в плитке узкая, подписи в неё не влезают. + // Что означает какая — написано подсказкой под сеткой и в title. const cam = document.createElement('button'); - cam.textContent = '📷 Снять'; + cam.textContent = '📷'; + cam.title = 'Снять камерой'; cam.onclick = e => { e.stopPropagation(); photoTarget = {target, cardEl, btn: cam, dropBtn: drop}; @@ -631,8 +653,8 @@ function actionBar(target, cardEl, hasPhoto) { }; bar.appendChild(cam); + // Камера и галерея — раздельно: одним полем оба сценария на Android не покрыть. const gal = document.createElement('button'); - gal.className = 'narrow'; gal.textContent = '🖼'; gal.title = 'Выбрать из галереи'; gal.onclick = e => { @@ -643,16 +665,22 @@ function actionBar(target, cardEl, hasPhoto) { bar.appendChild(gal); const edit = document.createElement('button'); - edit.textContent = '✏️ Изменить'; + edit.textContent = '✏️'; + edit.title = 'Изменить'; edit.onclick = e => { e.stopPropagation(); const box = cardEl.querySelector('.edit'); - if (box) box.hidden = !box.hidden; + if (!box) return; + box.hidden = !box.hidden; + // В плитке раскрытая карточка занимает всю ширину, иначе поля ввода + // оказались бы в колонке шириной с ладонь. + cardEl.classList.toggle('wide', !box.hidden); }; bar.appendChild(edit); drop.className = 'warn'; - drop.textContent = '🗑 Фото'; + drop.textContent = '🗑'; + drop.title = 'Убрать фото'; drop.hidden = !hasPhoto; drop.onclick = async e => { e.stopPropagation(); @@ -664,7 +692,6 @@ function actionBar(target, cardEl, hasPhoto) { if (d.ok) { const ph = cardEl.querySelector('.photo'); if (ph) { ph.hidden = true; ph.innerHTML = ''; } - photo.textContent = '📷 Фото'; drop.hidden = true; } }; @@ -751,7 +778,28 @@ function itemEditor(it, cardEl) { return box; } -function render(box, items) { box.innerHTML = ''; (items || []).forEach(it => box.appendChild(card(it))); } +// Раскладывает карточки плиткой. Один предмет сеткой не оформляем — выглядело +// бы как обрезанная витрина. +function render(box, items) { + box.innerHTML = ''; + dropHint(box); + const list = items || []; + box.classList.toggle('cards', list.length > 1); + list.forEach(it => box.appendChild(card(it))); + if (list.length > 1) hintIcons(box); +} + +// Один раз под сеткой поясняем иконки: в плитке у кнопок нет подписей. +function hintIcons(box) { + const p = document.createElement('p'); + p.className = 'hint iconHint'; + p.textContent = '📷 снять · 🖼 из галереи · ✏️ изменить · 🗑 убрать фото'; + box.after(p); +} +function dropHint(box) { + const next = box.nextElementSibling; + if (next && next.classList.contains('iconHint')) next.remove(); +} // ---------- найти ---------- async function doFind() { @@ -808,8 +856,16 @@ async function openBox(code) { // Фото самой коробки: снял, как она выглядит и что в ней лежит. head.appendChild(actionBar({type:'container', code}, head, !!d.photo)); head.appendChild(boxEditor(code, head)); + + // Содержимое коробки — витриной, а не колонкой в одну карточку на строку. + const grid = document.createElement('div'); + const items = d.items || []; + grid.className = items.length > 1 ? 'cards' : ''; + items.forEach(it => grid.appendChild(card(it))); + + box.prepend(grid); box.prepend(head); - (d.items || []).forEach(it => box.insertBefore(card(it), head.nextSibling)); + if (items.length > 1) hintIcons(grid); window.scrollTo({top:0, behavior:'smooth'}); }