Обзор и поиск — плиткой, как витрина в магазине
Раньше карточки шли одна под другой, и коробка с двумя десятками вещей превращалась в длинную колонку. Теперь сетка на CSS grid с auto-fill: сколько колонок влезает в экран, столько и будет — на телефоне обычно две, на планшете три-четыре. Чтобы плитка была компактной: - превью квадратные (object-fit: cover) — ряд выглядит ровным; - кнопки только иконками, подписи в них не влезали; что означает какая, написано подсказкой под сеткой и в title; - теги в плитке скрыты, они её распирали — видны при правке; - карточка с открытой правкой растягивается на всю ширину, иначе поля ввода оказались бы в колонке шириной с ладонь. Один результат сеткой не оформляется: выглядело бы как обрезанная витрина. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -83,6 +83,11 @@ start.cmd запускает сервер и печатает адреса
|
|||||||
| **Переместить** | Вещь переезжает в другую коробку или место |
|
| **Переместить** | Вещь переезжает в другую коробку или место |
|
||||||
| **Удалить** | Убирает запись совсем |
|
| **Удалить** | Убирает запись совсем |
|
||||||
|
|
||||||
|
Найденное и содержимое коробок показывается плиткой, как витрина в магазине:
|
||||||
|
сколько колонок влезает в экран, столько и будет. Кнопки на карточках —
|
||||||
|
иконками: 📷 снять, 🖼 из галереи, ✏️ изменить, 🗑 убрать фото. При правке
|
||||||
|
карточка растягивается на всю ширину, чтобы поля были нормального размера.
|
||||||
|
|
||||||
Ввод структурный: **место → ёмкость → предмет**. Место и коробку выбираешь из
|
Ввод структурный: **место → ёмкость → предмет**. Место и коробку выбираешь из
|
||||||
списков, поэтому ИИ в самом ответственном месте не участвует и ошибиться
|
списков, поэтому ИИ в самом ответственном месте не участвует и ошибиться
|
||||||
разбором фразы негде. Новое место добавляется *внутрь* выбранного — так и
|
разбором фразы негде. Новое место добавляется *внутрь* выбранного — так и
|
||||||
|
|||||||
+65
-9
@@ -85,6 +85,25 @@
|
|||||||
.card { margin-top:12px; padding:13px; background:var(--card);
|
.card { margin-top:12px; padding:13px; background:var(--card);
|
||||||
border:1px solid var(--line); border-radius:var(--radius); }
|
border:1px solid var(--line); border-radius:var(--radius); }
|
||||||
.card h3 { margin:0 0 4px; font-size:17px; }
|
.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 { color:var(--accent); font-size:15px; }
|
||||||
.addr.taken { color:var(--warn); }
|
.addr.taken { color:var(--warn); }
|
||||||
.meta { color:var(--dim); font-size:13px; margin-top:5px; }
|
.meta { color:var(--dim); font-size:13px; margin-top:5px; }
|
||||||
@@ -107,6 +126,7 @@
|
|||||||
.tag.del { cursor:pointer; padding-right:6px; }
|
.tag.del { cursor:pointer; padding-right:6px; }
|
||||||
.tag.del b { color:#e06c5a; margin-left:6px; font-weight:700; }
|
.tag.del b { color:#e06c5a; margin-left:6px; font-weight:700; }
|
||||||
.editHint { color:var(--dim); font-size:13px; margin:6px 0 0; }
|
.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); }
|
.why { float:right; font-size:12px; color:var(--dim); }
|
||||||
.pick { cursor:pointer; }
|
.pick { cursor:pointer; }
|
||||||
.pick:active { border-color:var(--accent); }
|
.pick:active { border-color:var(--accent); }
|
||||||
@@ -621,9 +641,11 @@ function actionBar(target, cardEl, hasPhoto) {
|
|||||||
|
|
||||||
const drop = document.createElement('button');
|
const drop = document.createElement('button');
|
||||||
|
|
||||||
// Камера и галерея — раздельно: одним полем оба сценария на Android не покрыть.
|
// Только иконки: карточка в плитке узкая, подписи в неё не влезают.
|
||||||
|
// Что означает какая — написано подсказкой под сеткой и в title.
|
||||||
const cam = document.createElement('button');
|
const cam = document.createElement('button');
|
||||||
cam.textContent = '📷 Снять';
|
cam.textContent = '📷';
|
||||||
|
cam.title = 'Снять камерой';
|
||||||
cam.onclick = e => {
|
cam.onclick = e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
photoTarget = {target, cardEl, btn: cam, dropBtn: drop};
|
photoTarget = {target, cardEl, btn: cam, dropBtn: drop};
|
||||||
@@ -631,8 +653,8 @@ function actionBar(target, cardEl, hasPhoto) {
|
|||||||
};
|
};
|
||||||
bar.appendChild(cam);
|
bar.appendChild(cam);
|
||||||
|
|
||||||
|
// Камера и галерея — раздельно: одним полем оба сценария на Android не покрыть.
|
||||||
const gal = document.createElement('button');
|
const gal = document.createElement('button');
|
||||||
gal.className = 'narrow';
|
|
||||||
gal.textContent = '🖼';
|
gal.textContent = '🖼';
|
||||||
gal.title = 'Выбрать из галереи';
|
gal.title = 'Выбрать из галереи';
|
||||||
gal.onclick = e => {
|
gal.onclick = e => {
|
||||||
@@ -643,16 +665,22 @@ function actionBar(target, cardEl, hasPhoto) {
|
|||||||
bar.appendChild(gal);
|
bar.appendChild(gal);
|
||||||
|
|
||||||
const edit = document.createElement('button');
|
const edit = document.createElement('button');
|
||||||
edit.textContent = '✏️ Изменить';
|
edit.textContent = '✏️';
|
||||||
|
edit.title = 'Изменить';
|
||||||
edit.onclick = e => {
|
edit.onclick = e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const box = cardEl.querySelector('.edit');
|
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);
|
bar.appendChild(edit);
|
||||||
|
|
||||||
drop.className = 'warn';
|
drop.className = 'warn';
|
||||||
drop.textContent = '🗑 Фото';
|
drop.textContent = '🗑';
|
||||||
|
drop.title = 'Убрать фото';
|
||||||
drop.hidden = !hasPhoto;
|
drop.hidden = !hasPhoto;
|
||||||
drop.onclick = async e => {
|
drop.onclick = async e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -664,7 +692,6 @@ function actionBar(target, cardEl, hasPhoto) {
|
|||||||
if (d.ok) {
|
if (d.ok) {
|
||||||
const ph = cardEl.querySelector('.photo');
|
const ph = cardEl.querySelector('.photo');
|
||||||
if (ph) { ph.hidden = true; ph.innerHTML = ''; }
|
if (ph) { ph.hidden = true; ph.innerHTML = ''; }
|
||||||
photo.textContent = '📷 Фото';
|
|
||||||
drop.hidden = true;
|
drop.hidden = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -751,7 +778,28 @@ function itemEditor(it, cardEl) {
|
|||||||
return box;
|
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() {
|
async function doFind() {
|
||||||
@@ -808,8 +856,16 @@ async function openBox(code) {
|
|||||||
// Фото самой коробки: снял, как она выглядит и что в ней лежит.
|
// Фото самой коробки: снял, как она выглядит и что в ней лежит.
|
||||||
head.appendChild(actionBar({type:'container', code}, head, !!d.photo));
|
head.appendChild(actionBar({type:'container', code}, head, !!d.photo));
|
||||||
head.appendChild(boxEditor(code, head));
|
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);
|
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'});
|
window.scrollTo({top:0, behavior:'smooth'});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user