Ремонт: техника — авто-группировка карточек по типу + перенос строк в названии
All checks were successful
Deploy Remont Site / deploy (push) Successful in 19s

- inline() поддерживает <br> в тексте заметки (было видно литеральным)
- карточки: «Модель<br>описание» → модель жирным + приглушённое описание
- колонка «Тип/Категория/Техника» → карточки группируются по типу с подзаголовками
  (пустая ячейка типа наследует значение сверху)
- таблица «Варианты техники» переведена в формат Тип|Название|Цена|Ссылка|Фото
- README: описан формат таблицы техники

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KjyzuPoH9T5k3oCbKEEazy
This commit is contained in:
Cosmo
2026-07-16 10:08:35 +00:00
parent b355119e8e
commit c2f65b2ea1
4 changed files with 58 additions and 15 deletions

View File

@@ -280,6 +280,7 @@ a.rooms-link:hover .room-name { color: var(--sage-deep); }
.opt__body { padding: 18px 20px 20px; display: flex; flex-direction: column; gap: 4px; flex: 1; } .opt__body { padding: 18px 20px 20px; display: flex; flex-direction: column; gap: 4px; flex: 1; }
.opt__no { font-size: 12px; letter-spacing: .12em; color: var(--sage-deep); font-weight: 600; } .opt__no { font-size: 12px; letter-spacing: .12em; color: var(--sage-deep); font-weight: 600; }
.opt__name { font-family: var(--ff-display); font-size: 20px; line-height: 1.15; } .opt__name { font-family: var(--ff-display); font-size: 20px; line-height: 1.15; }
.opt__desc { font-size: 13.5px; line-height: 1.5; color: var(--ink-soft); margin-top: 2px; }
.opt__price { font-family: var(--ff-display); font-size: 28px; margin-top: 8px; } .opt__price { font-family: var(--ff-display); font-size: 28px; margin-top: 8px; }
.opt__price span { font-size: 15px; color: var(--ink-soft); } .opt__price span { font-size: 15px; color: var(--ink-soft); }
.opt__link { .opt__link {
@@ -288,6 +289,15 @@ a.rooms-link:hover .room-name { color: var(--sage-deep); }
} }
.opt__link svg { width: 15px; height: 15px; transition: transform .2s; } .opt__link svg { width: 15px; height: 15px; transition: transform .2s; }
.opt__link:hover svg { transform: translate(2px,-2px); } .opt__link:hover svg { transform: translate(2px,-2px); }
/* группа карточек по типу (техника) */
.opt-group { margin-top: 34px; }
.opt-group:first-child { margin-top: 0; }
.opt-group__title {
display: flex; align-items: baseline; gap: 12px; margin: 0 0 16px;
font-family: var(--ff-display); font-size: 21px; letter-spacing: -.01em;
}
.opt-group__title span { font-size: 12px; letter-spacing: .06em; color: var(--ink-soft); font-weight: 400; }
.opt-group__title::after { content: ""; flex: 1; height: 1px; background: var(--line); align-self: center; }
@media (max-width: 860px) { .options { grid-template-columns: 1fr 1fr; } } @media (max-width: 860px) { .options { grid-template-columns: 1fr 1fr; } }
@media (max-width: 560px) { .options { grid-template-columns: 1fr; } } @media (max-width: 560px) { .options { grid-template-columns: 1fr; } }

View File

@@ -42,6 +42,7 @@ function inline(t) {
t = esc(t); t = esc(t);
t = t.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>'); t = t.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
t = t.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>'); t = t.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
t = t.replace(/&lt;br\s*\/?&gt;/gi, '<br>'); // разрешаем перенос строки <br> в тексте заметки
return t; return t;
} }
@@ -179,28 +180,54 @@ function renderCards(tbl) {
const iPrice = colIndex(H, /цена|price|₽|руб/i); const iPrice = colIndex(H, /цена|price|₽|руб/i);
const iLink = colIndex(H, /ссылк|link|url/i); const iLink = colIndex(H, /ссылк|link|url/i);
const iPhoto = colIndex(H, /фото|photo|картин|image/i); const iPhoto = colIndex(H, /фото|photo|картин|image/i);
const iName = H.findIndex((h, idx) => idx !== iNo && idx !== iPrice && idx !== iLink && idx !== iPhoto && h); const iCat = colIndex(H, /^(тип|категор|группа|техника|раздел)$/i);
const iName = H.findIndex((h, idx) => idx !== iNo && idx !== iPrice && idx !== iLink && idx !== iPhoto && idx !== iCat && h);
const cards = tbl.rows.map((r, idx) => { // одна карточка. no — подпись «Вариант N» (в пределах группы)
const no = iNo >= 0 && r[iNo] ? r[iNo] : String(idx + 1); const card = (r, no) => {
const name = iName >= 0 ? r[iName] : ''; const nameRaw = iName >= 0 ? (r[iName] || '') : '';
// «Модель<br>описание» → жирная модель + приглушённое описание
const [head, ...rest] = nameRaw.split(/<br\s*\/?>/i);
const desc = rest.join('<br>').trim();
const priceRaw = iPrice >= 0 ? r[iPrice] : ''; const priceRaw = iPrice >= 0 ? r[iPrice] : '';
const priceNum = (priceRaw.match(/[\d\s .]+/) || [''])[0].trim(); const priceNum = (priceRaw.match(/[\d\s .]+/) || [''])[0].trim();
const unit = /₽|руб/i.test(priceRaw) ? '₽' : ''; const unit = /₽|руб/i.test(priceRaw) ? '₽' : '';
const photo = iPhoto >= 0 ? parseWiki(r[iPhoto]) : null; const photo = iPhoto >= 0 ? parseWiki(r[iPhoto]) : null;
const url = iLink >= 0 ? extractUrl(r[iLink]) : ''; const url = iLink >= 0 ? extractUrl(r[iLink]) : '';
const media = photo const media = photo
? `<div class="opt__media" data-zoom-wrap><img src="${imgSrc(photo)}" alt="${esc(name)}" data-zoom loading="lazy"></div>` : ''; ? `<div class="opt__media" data-zoom-wrap><img src="${imgSrc(photo)}" alt="${esc(head)}" data-zoom loading="lazy"></div>` : '';
const price = priceNum const price = priceNum
? `<span class="opt__price">${esc(priceNum)}${unit ? ` <span>${unit}</span>` : ''}</span>` : ''; ? `<span class="opt__price">${esc(priceNum)}${unit ? ` <span>${unit}</span>` : ''}</span>` : '';
const link = url const link = url
? `<a class="opt__link" href="${esc(url)}" target="_blank" rel="noopener">Открыть на ${esc(domainLabel(url))} <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 17 17 7M8 7h9v9"/></svg></a>` : ''; ? `<a class="opt__link" href="${esc(url)}" target="_blank" rel="noopener">Открыть на ${esc(domainLabel(url))} <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 17 17 7M8 7h9v9"/></svg></a>` : '';
return `<article class="opt reveal">${media}<div class="opt__body"> return `<article class="opt reveal">${media}<div class="opt__body">
<span class="opt__no">Вариант ${esc(no)}</span> <span class="opt__no">Вариант ${esc(no)}</span>
<span class="opt__name">${inline(name)}</span> <span class="opt__name">${inline(head)}</span>
${desc ? `<span class="opt__desc">${inline(desc)}</span>` : ''}
${price}${link}</div></article>`; ${price}${link}</div></article>`;
};
// без колонки типа — простая сетка карточек
if (iCat < 0) {
const cards = tbl.rows.map((r, idx) => card(r, iNo >= 0 && r[iNo] ? r[iNo] : idx + 1)).join('\n');
return `<div class="options">${cards}</div>`;
}
// с колонкой типа — группируем по типу (пустая ячейка наследует тип сверху)
const groups = [];
let cur = null, lastCat = '';
tbl.rows.forEach((r) => {
const cat = (r[iCat] && r[iCat].trim()) || lastCat;
lastCat = cat;
if (!cur || cur.cat !== cat) { cur = { cat, rows: [] }; groups.push(cur); }
cur.rows.push(r);
});
return groups.map((g) => {
const cards = g.rows.map((r, i) => card(r, i + 1)).join('\n');
return `<div class="opt-group reveal">
<h4 class="opt-group__title">${inline(g.cat)}<span>${g.rows.length} ${plural(g.rows.length, 'вариант', 'варианта', 'вариантов')}</span></h4>
<div class="options">${cards}</div></div>`;
}).join('\n'); }).join('\n');
return `<div class="options">${cards}</div>`;
} }
function tableCell(c) { function tableCell(c) {

View File

@@ -26,6 +26,12 @@
- Подряд идущие картинки `![[файл.png]]`**галерея рендеров** (1 крупно / 2 / 3 / 4 в сетке). - Подряд идущие картинки `![[файл.png]]`**галерея рендеров** (1 крупно / 2 / 3 / 4 в сетке).
- **Таблица с колонкой «Фото» и без «Комментарий»** → красивые **карточки** (диваны, столы). - **Таблица с колонкой «Фото» и без «Комментарий»** → красивые **карточки** (диваны, столы).
Колонки: `№ | Название | Цена | Ссылка | Фото`. Ссылка — просто URL, кнопка подпишется сама. Колонки: `№ | Название | Цена | Ссылка | Фото`. Ссылка — просто URL, кнопка подпишется сама.
- В «Названии» можно писать модель + описание, разделяя переносом `<br>`:
`Haier C4F640<br>400 л, инвертор` → модель жирным, описание — приглушённой строкой под ней.
- Добавь колонку **«Тип»** (или «Категория», «Техника») — карточки **автоматически сгруппируются**
по типу с подзаголовками (Холодильник, Духовка…). Тип пиши в первой строке группы,
в остальных строках этой группы ячейку «Тип» **оставляй пустой** — они наследуют тип сверху.
Пример для техники: `Тип | Модель | Цена | Ссылка | Фото`.
- **Таблица с колонкой «Комментарий»** → аккуратная **сравнительная таблица** (техника). - **Таблица с колонкой «Комментарий»** → аккуратная **сравнительная таблица** (техника).
Если добавить колонку «Фото» — появятся миниатюры прямо в таблице. Если добавить колонку «Фото» — появятся миниатюры прямо в таблице.
- Картинка сразу перед нумерованным списком → блок **«вариант отделки»** (фото слева, список справа). - Картинка сразу перед нумерованным списком → блок **«вариант отделки»** (фото слева, список справа).

View File

@@ -35,14 +35,14 @@ swatches: Шалфей #7C8A66, Дуб #C7A579, Кремовый #EDE8DE, Гра
### Варианты техники ### Варианты техники
| Название | Цена | Ссылка | Фото | | Тип | Название | Цена | Ссылка | Фото |
| ------------------------------------------------------------------------------------------------ | -------- | ---------------------------------------------------------------------------------- | ----------------------------------------- | | --- | --- | --- | --- | --- |
| Варочная панель Haier | 28 000 ₽ | https://haieronline.ru/catalog/built-in/glass-ceramic-cove/hhy-y53nvb/ | ![[Pasted image 20260716000501.png\|154]] | | Варочная панель | Haier | 28 000 ₽ | https://haieronline.ru/catalog/built-in/glass-ceramic-cove/hhy-y53nvb/ | ![[Pasted image 20260716000501.png\|154]] |
| Духовка Haier HOD-PM08TGB<br>70 л, 8 режимов, 2 стекла | 29 000 ₽ | https://haieronline.ru/catalog/built-in/ovens/hodpm08tgb/ | ![[Pasted image 20260716000638.png\|176]] | | Духовка | Haier HOD-PM08TGB<br>70 л, 8 режимов, 2 стекла | 29 000 ₽ | https://haieronline.ru/catalog/built-in/ovens/hodpm08tgb/ | ![[Pasted image 20260716000638.png\|176]] |
| Духовка Haier HOX-P06HGBX<br>76 л, 6 режимов, 3 стекла | 30 000 ₽ | https://haieronline.ru/catalog/built-in/ovens/hoxp06hgbx/ | ![[Pasted image 20260716000625.png\|177]] | | | Haier HOX-P06HGBX<br>76 л, 6 режимов, 3 стекла | 30 000 ₽ | https://haieronline.ru/catalog/built-in/ovens/hoxp06hgbx/ | ![[Pasted image 20260716000625.png\|177]] |
| Холодильник LG GA-B509CLSL<br>419 л, инвентор, <br>3 ящика морозилки,<br> нулевой зазор | 50 000 ₽ | https://www.lg.com/ru/refrigerators/lg-GA-B509CLSL# | ![[Pasted image 20260716125354.png\|159]] | | Холодильник | LG GA-B509CLSL<br>419 л, инвентор, <br>3 ящика морозилки,<br> нулевой зазор | 50 000 ₽ | https://www.lg.com/ru/refrigerators/lg-GA-B509CLSL# | ![[Pasted image 20260716125354.png\|159]] |
| Холодильник HAIER C4F640CFMRGU1<br>400 л, инвентор, <br>4 ящика морозилки, нулевая зона свежести | 59 000 ₽ | https://haieronline.ru/catalog/appliances/fridges/kholodilnik-haier-c4f640cfmrgu1/ | ![[Pasted image 20260716125554.png\|155]] | | | HAIER C4F640CFMRGU1<br>400 л, инвентор, <br>4 ящика морозилки, нулевая зона свежести | 59 000 ₽ | https://haieronline.ru/catalog/appliances/fridges/kholodilnik-haier-c4f640cfmrgu1/ | ![[Pasted image 20260716125554.png\|155]] |
| | | | | | | | | | |
| Техника | Ссылка | Цена | Комментарий | Фото | | Техника | Ссылка | Цена | Комментарий | Фото |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---- | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---- |