@webnumseoagent/next 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/blog.tsx +45 -31
- package/dist/blog-toc.js +18 -1
- package/dist/blog.d.ts +1 -0
- package/dist/blog.js +20 -12
- package/package.json +1 -1
package/blog.tsx
CHANGED
|
@@ -23,7 +23,7 @@ const blogfs = () => import("./blog-fs");
|
|
|
23
23
|
// .blogfs.json (платформа пишет бренд-цвет владельца в accent + опц. токены оформления сайта).
|
|
24
24
|
// toc — оглавление статьи: enabled (вкл/выкл), variant (вид), motion (анимация), top (офсет sticky/якоря),
|
|
25
25
|
// css (маленький scoped-CSS под .sa-toc от Designer-агента — индивидуальный вид/анимация под стиль сайта).
|
|
26
|
-
export interface SeoBlogTocTheme { enabled?: boolean; variant?: string; motion?: boolean; top?: string; css?: string }
|
|
26
|
+
export interface SeoBlogTocTheme { enabled?: boolean; variant?: string; motion?: boolean; top?: string; maxh?: string; css?: string }
|
|
27
27
|
export interface SeoBlogTheme { accent?: string; border?: string; muted?: string; surface?: string; thBg?: string; radius?: string; toc?: SeoBlogTocTheme }
|
|
28
28
|
export interface SeoBlogSummary { slug: string; title: string; excerpt: string; cover: string | null; date: string | null }
|
|
29
29
|
export interface SeoBlogPostFull extends SeoBlogSummary { bodyHtml: string; theme?: SeoBlogTheme }
|
|
@@ -124,26 +124,34 @@ function safeTocCss(css?: string): string {
|
|
|
124
124
|
return css.slice(0, 4000);
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
const TOC_CSS = `.sa-article-wrap{max-width:
|
|
128
|
-
.sa-article-
|
|
129
|
-
|
|
127
|
+
const TOC_CSS = `.sa-article-wrap{max-width:1400px;margin:0 auto;padding:var(--sa-blog-top,clamp(5rem,8vw,7rem)) 1.5rem 3rem;container-type:inline-size}
|
|
128
|
+
.sa-article-grid{display:block}
|
|
129
|
+
.sa-article-grid:not(.has-toc){max-width:760px;margin:0 auto}
|
|
130
130
|
.sa-article{min-width:0;line-height:1.7}
|
|
131
|
-
.sa-
|
|
132
|
-
.
|
|
133
|
-
@
|
|
131
|
+
.sa-toc{font-size:.9rem}
|
|
132
|
+
/* Средняя ширина контента: оглавление слева + статья рядом, блок по центру (формат для узкого контента). */
|
|
133
|
+
@container (min-width:700px){.sa-article-grid.has-toc{display:grid;column-gap:2.5rem;grid-template-columns:250px minmax(0,760px);justify-content:center;align-items:start}
|
|
134
|
+
.sa-toc{position:sticky;top:var(--sa-toc-top,88px)}}
|
|
135
|
+
/* Широкий контент (ZeroMax): статья ПО ЦЕНТРУ вьюпорта, оглавление в левом гуттере, прижато к статье (капнуто ~300px). */
|
|
136
|
+
@container (min-width:1180px){.sa-article-grid.has-toc{grid-template-columns:minmax(0,1fr) minmax(0,760px) minmax(0,1fr);column-gap:0}
|
|
137
|
+
.sa-article-grid.has-toc>.sa-toc{grid-column:1;justify-self:end;width:min(100%,300px);margin-right:2.5rem}
|
|
138
|
+
.sa-article-grid.has-toc>.sa-article{grid-column:2}}
|
|
134
139
|
.sa-toc>summary{list-style:none;font-weight:700;font-size:.95rem;margin:0 0 .75rem;display:flex;align-items:center;justify-content:space-between;gap:.5rem;color:inherit;cursor:pointer}
|
|
135
140
|
.sa-toc>summary::-webkit-details-marker{display:none}
|
|
136
141
|
.sa-toc>summary .sa-toc-chev{transition:transform .2s ease;color:var(--sa-muted,#94a3b8)}
|
|
137
142
|
.sa-toc[open]>summary .sa-toc-chev{transform:rotate(90deg)}
|
|
138
143
|
@media(min-width:1024px){.sa-toc>summary{pointer-events:none}.sa-toc>summary .sa-toc-chev{display:none}}
|
|
139
|
-
.sa-toc-
|
|
144
|
+
.sa-toc-body{position:relative}
|
|
145
|
+
.sa-toc-list{display:flex;flex-direction:column;gap:.1rem;max-height:var(--sa-toc-maxh,300px);overflow-y:auto;overscroll-behavior:contain;scrollbar-width:none;-ms-overflow-style:none}
|
|
146
|
+
.sa-toc-list::-webkit-scrollbar{width:0;height:0;display:none}
|
|
140
147
|
.sa-toc-list a{display:block;text-decoration:none;color:var(--sa-muted,#64748b);padding:.35rem .6rem;border-radius:var(--sa-radius,8px);line-height:1.4;transition:color .15s ease,background .15s ease,border-color .15s ease}
|
|
141
148
|
.sa-toc-list a[data-level="3"]{padding-left:1.4rem;font-size:.85em}
|
|
142
149
|
.sa-toc-list a:hover{color:inherit}
|
|
143
150
|
.sa-toc-list a.is-active{color:var(--sa-accent,#2563eb)}
|
|
144
|
-
.sa-toc[data-sa-toc="minimal"] .sa-toc-
|
|
145
|
-
.sa-toc[data-sa-toc="minimal"] .sa-toc-list a{
|
|
146
|
-
.sa-toc[data-sa-toc="minimal"] .sa-toc-list a
|
|
151
|
+
.sa-toc[data-sa-toc="minimal"] .sa-toc-body::before{content:"";position:absolute;left:0;top:.2rem;bottom:.2rem;width:2px;background:var(--sa-border,#e5e7eb);z-index:0}
|
|
152
|
+
.sa-toc[data-sa-toc="minimal"] .sa-toc-list a{padding-left:.9rem;border-radius:0;box-shadow:inset 2px 0 0 transparent}
|
|
153
|
+
.sa-toc[data-sa-toc="minimal"] .sa-toc-list a[data-level="3"]{padding-left:1.7rem}
|
|
154
|
+
.sa-toc[data-sa-toc="minimal"] .sa-toc-list a.is-active{box-shadow:inset 2px 0 0 var(--sa-accent,#2563eb)}
|
|
147
155
|
.sa-toc[data-sa-toc="bordered"]{border:1px solid var(--sa-border,#e5e7eb);border-radius:var(--sa-radius,12px);padding:1rem;background:var(--sa-surface,transparent)}
|
|
148
156
|
.sa-toc[data-sa-toc="bordered"] .sa-toc-list a.is-active{background:var(--sa-th-bg,color-mix(in srgb,currentColor 8%,transparent))}
|
|
149
157
|
.sa-toc[data-sa-toc="pill"] .sa-toc-list a.is-active{background:var(--sa-accent,#2563eb);color:#fff}
|
|
@@ -231,28 +239,34 @@ export function SeoBlogArticle({ post, locale }: { post: SeoBlogPostFull; locale
|
|
|
231
239
|
const variant = toc.variant || "minimal";
|
|
232
240
|
const motion = toc.motion === false ? "off" : "on";
|
|
233
241
|
const label = L(TOC_LABEL, locale ?? "", "Contents");
|
|
234
|
-
const rootStyle = { ...themeStyle(post.theme), ...(toc.top ? { ["--sa-toc-top"]: toc.top } : {}) } as CSSProperties;
|
|
242
|
+
const rootStyle = { ...themeStyle(post.theme), ...(toc.top ? { ["--sa-toc-top"]: toc.top } : {}), ...(toc.maxh ? { ["--sa-toc-maxh"]: toc.maxh } : {}) } as CSSProperties;
|
|
235
243
|
return (
|
|
236
|
-
<div className=
|
|
244
|
+
<div className="sa-article-wrap" style={rootStyle}>
|
|
237
245
|
<style dangerouslySetInnerHTML={{ __html: BODY_CSS + TOC_CSS + (showToc ? safeTocCss(toc.css) : "") }} />
|
|
238
|
-
{
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
<
|
|
242
|
-
{
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
<
|
|
255
|
-
|
|
246
|
+
{/* Внутренний грид — CSS-контейнер .sa-article-wrap адаптирует его по ШИРИНЕ контента (@container). */}
|
|
247
|
+
<div className={showToc ? "sa-article-grid has-toc" : "sa-article-grid"}>
|
|
248
|
+
{showToc ? (
|
|
249
|
+
<details className="sa-toc" data-sa-toc={variant} data-motion={motion} open>
|
|
250
|
+
<summary>{label}<span className="sa-toc-chev" aria-hidden="true">›</span></summary>
|
|
251
|
+
{/* .sa-toc-body держит общий рельс (::before) вне скролла; скроллится только список. */}
|
|
252
|
+
<div className="sa-toc-body">
|
|
253
|
+
<nav className="sa-toc-list" aria-label={label}>
|
|
254
|
+
{items.map((it) => (
|
|
255
|
+
<a key={it.id} href={`#${it.id}`} data-toc-link={it.id} data-level={it.level}>{it.text}</a>
|
|
256
|
+
))}
|
|
257
|
+
</nav>
|
|
258
|
+
</div>
|
|
259
|
+
<SeoBlogTocSpy ids={items.map((it) => it.id)} />
|
|
260
|
+
</details>
|
|
261
|
+
) : null}
|
|
262
|
+
<article className="sa-article">
|
|
263
|
+
{post.cover ? <img src={post.cover} alt={post.title} style={{ width: "100%", aspectRatio: "3 / 2", objectFit: "cover", display: "block", borderRadius: "var(--sa-radius, 12px)", marginBottom: "1.5rem" }} /> : null}
|
|
264
|
+
<h1 style={{ fontSize: "2.2rem", lineHeight: 1.2, margin: "0 0 1rem" }}>{post.title}</h1>
|
|
265
|
+
{post.date ? <p style={{ color: "var(--sa-muted, #94a3b8)", margin: "0 0 1.5rem" }}>{fmtDate(post.date, locale)}</p> : null}
|
|
266
|
+
{/* Тело уже санитайзено при доставке; id-якоря добавлены buildToc при рендере. */}
|
|
267
|
+
<div className="seoagent-blog-body" dangerouslySetInnerHTML={{ __html: html }} />
|
|
268
|
+
</article>
|
|
269
|
+
</div>
|
|
256
270
|
</div>
|
|
257
271
|
);
|
|
258
272
|
}
|
package/dist/blog-toc.js
CHANGED
|
@@ -20,6 +20,21 @@ function SeoBlogTocSpy({ ids }) {
|
|
|
20
20
|
const heads = ids.map((id) => document.getElementById(id)).filter(Boolean);
|
|
21
21
|
if (!heads.length)
|
|
22
22
|
return;
|
|
23
|
+
const reduce = typeof matchMedia !== "undefined" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
24
|
+
// Держим активный пункт в видимой (обрезанной) зоне списка: подкручиваем сам скролл-контейнер
|
|
25
|
+
// .sa-toc-list, чтобы при скролле статьи выделенный заголовок не «уезжал» за пределы оглавления.
|
|
26
|
+
const keepInView = (a) => {
|
|
27
|
+
const box = a.closest(".sa-toc-list");
|
|
28
|
+
if (!box || box.scrollHeight <= box.clientHeight)
|
|
29
|
+
return;
|
|
30
|
+
const ar = a.getBoundingClientRect(), br = box.getBoundingClientRect();
|
|
31
|
+
const top = box.scrollTop + (ar.top - br.top);
|
|
32
|
+
const pad = 8;
|
|
33
|
+
if (top < box.scrollTop + pad)
|
|
34
|
+
box.scrollTo({ top: Math.max(0, top - pad), behavior: reduce ? "auto" : "smooth" });
|
|
35
|
+
else if (top + ar.height > box.scrollTop + box.clientHeight - pad)
|
|
36
|
+
box.scrollTo({ top: top + ar.height - box.clientHeight + pad, behavior: reduce ? "auto" : "smooth" });
|
|
37
|
+
};
|
|
23
38
|
let active = "";
|
|
24
39
|
const setActive = (id) => {
|
|
25
40
|
if (id === active)
|
|
@@ -33,8 +48,10 @@ function SeoBlogTocSpy({ ids }) {
|
|
|
33
48
|
else
|
|
34
49
|
a.removeAttribute("aria-current");
|
|
35
50
|
});
|
|
51
|
+
const cur = links.get(id);
|
|
52
|
+
if (cur)
|
|
53
|
+
keepInView(cur);
|
|
36
54
|
};
|
|
37
|
-
const reduce = typeof matchMedia !== "undefined" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
38
55
|
const onClick = (e) => {
|
|
39
56
|
const a = e.currentTarget;
|
|
40
57
|
const id = a.getAttribute("data-toc-link");
|
package/dist/blog.d.ts
CHANGED
package/dist/blog.js
CHANGED
|
@@ -142,26 +142,34 @@ function safeTocCss(css) {
|
|
|
142
142
|
return "";
|
|
143
143
|
return css.slice(0, 4000);
|
|
144
144
|
}
|
|
145
|
-
const TOC_CSS = `.sa-article-wrap{max-width:
|
|
146
|
-
.sa-article-
|
|
147
|
-
|
|
145
|
+
const TOC_CSS = `.sa-article-wrap{max-width:1400px;margin:0 auto;padding:var(--sa-blog-top,clamp(5rem,8vw,7rem)) 1.5rem 3rem;container-type:inline-size}
|
|
146
|
+
.sa-article-grid{display:block}
|
|
147
|
+
.sa-article-grid:not(.has-toc){max-width:760px;margin:0 auto}
|
|
148
148
|
.sa-article{min-width:0;line-height:1.7}
|
|
149
|
-
.sa-
|
|
150
|
-
.
|
|
151
|
-
@
|
|
149
|
+
.sa-toc{font-size:.9rem}
|
|
150
|
+
/* Средняя ширина контента: оглавление слева + статья рядом, блок по центру (формат для узкого контента). */
|
|
151
|
+
@container (min-width:700px){.sa-article-grid.has-toc{display:grid;column-gap:2.5rem;grid-template-columns:250px minmax(0,760px);justify-content:center;align-items:start}
|
|
152
|
+
.sa-toc{position:sticky;top:var(--sa-toc-top,88px)}}
|
|
153
|
+
/* Широкий контент (ZeroMax): статья ПО ЦЕНТРУ вьюпорта, оглавление в левом гуттере, прижато к статье (капнуто ~300px). */
|
|
154
|
+
@container (min-width:1180px){.sa-article-grid.has-toc{grid-template-columns:minmax(0,1fr) minmax(0,760px) minmax(0,1fr);column-gap:0}
|
|
155
|
+
.sa-article-grid.has-toc>.sa-toc{grid-column:1;justify-self:end;width:min(100%,300px);margin-right:2.5rem}
|
|
156
|
+
.sa-article-grid.has-toc>.sa-article{grid-column:2}}
|
|
152
157
|
.sa-toc>summary{list-style:none;font-weight:700;font-size:.95rem;margin:0 0 .75rem;display:flex;align-items:center;justify-content:space-between;gap:.5rem;color:inherit;cursor:pointer}
|
|
153
158
|
.sa-toc>summary::-webkit-details-marker{display:none}
|
|
154
159
|
.sa-toc>summary .sa-toc-chev{transition:transform .2s ease;color:var(--sa-muted,#94a3b8)}
|
|
155
160
|
.sa-toc[open]>summary .sa-toc-chev{transform:rotate(90deg)}
|
|
156
161
|
@media(min-width:1024px){.sa-toc>summary{pointer-events:none}.sa-toc>summary .sa-toc-chev{display:none}}
|
|
157
|
-
.sa-toc-
|
|
162
|
+
.sa-toc-body{position:relative}
|
|
163
|
+
.sa-toc-list{display:flex;flex-direction:column;gap:.1rem;max-height:var(--sa-toc-maxh,300px);overflow-y:auto;overscroll-behavior:contain;scrollbar-width:none;-ms-overflow-style:none}
|
|
164
|
+
.sa-toc-list::-webkit-scrollbar{width:0;height:0;display:none}
|
|
158
165
|
.sa-toc-list a{display:block;text-decoration:none;color:var(--sa-muted,#64748b);padding:.35rem .6rem;border-radius:var(--sa-radius,8px);line-height:1.4;transition:color .15s ease,background .15s ease,border-color .15s ease}
|
|
159
166
|
.sa-toc-list a[data-level="3"]{padding-left:1.4rem;font-size:.85em}
|
|
160
167
|
.sa-toc-list a:hover{color:inherit}
|
|
161
168
|
.sa-toc-list a.is-active{color:var(--sa-accent,#2563eb)}
|
|
162
|
-
.sa-toc[data-sa-toc="minimal"] .sa-toc-
|
|
163
|
-
.sa-toc[data-sa-toc="minimal"] .sa-toc-list a{
|
|
164
|
-
.sa-toc[data-sa-toc="minimal"] .sa-toc-list a
|
|
169
|
+
.sa-toc[data-sa-toc="minimal"] .sa-toc-body::before{content:"";position:absolute;left:0;top:.2rem;bottom:.2rem;width:2px;background:var(--sa-border,#e5e7eb);z-index:0}
|
|
170
|
+
.sa-toc[data-sa-toc="minimal"] .sa-toc-list a{padding-left:.9rem;border-radius:0;box-shadow:inset 2px 0 0 transparent}
|
|
171
|
+
.sa-toc[data-sa-toc="minimal"] .sa-toc-list a[data-level="3"]{padding-left:1.7rem}
|
|
172
|
+
.sa-toc[data-sa-toc="minimal"] .sa-toc-list a.is-active{box-shadow:inset 2px 0 0 var(--sa-accent,#2563eb)}
|
|
165
173
|
.sa-toc[data-sa-toc="bordered"]{border:1px solid var(--sa-border,#e5e7eb);border-radius:var(--sa-radius,12px);padding:1rem;background:var(--sa-surface,transparent)}
|
|
166
174
|
.sa-toc[data-sa-toc="bordered"] .sa-toc-list a.is-active{background:var(--sa-th-bg,color-mix(in srgb,currentColor 8%,transparent))}
|
|
167
175
|
.sa-toc[data-sa-toc="pill"] .sa-toc-list a.is-active{background:var(--sa-accent,#2563eb);color:#fff}
|
|
@@ -236,8 +244,8 @@ function SeoBlogArticle({ post, locale }) {
|
|
|
236
244
|
const variant = toc.variant || "minimal";
|
|
237
245
|
const motion = toc.motion === false ? "off" : "on";
|
|
238
246
|
const label = L(TOC_LABEL, locale ?? "", "Contents");
|
|
239
|
-
const rootStyle = { ...themeStyle(post.theme), ...(toc.top ? { ["--sa-toc-top"]: toc.top } : {}) };
|
|
240
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className:
|
|
247
|
+
const rootStyle = { ...themeStyle(post.theme), ...(toc.top ? { ["--sa-toc-top"]: toc.top } : {}), ...(toc.maxh ? { ["--sa-toc-maxh"]: toc.maxh } : {}) };
|
|
248
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "sa-article-wrap", style: rootStyle, children: [(0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: BODY_CSS + TOC_CSS + (showToc ? safeTocCss(toc.css) : "") } }), (0, jsx_runtime_1.jsxs)("div", { className: showToc ? "sa-article-grid has-toc" : "sa-article-grid", children: [showToc ? ((0, jsx_runtime_1.jsxs)("details", { className: "sa-toc", "data-sa-toc": variant, "data-motion": motion, open: true, children: [(0, jsx_runtime_1.jsxs)("summary", { children: [label, (0, jsx_runtime_1.jsx)("span", { className: "sa-toc-chev", "aria-hidden": "true", children: "\u203A" })] }), (0, jsx_runtime_1.jsx)("div", { className: "sa-toc-body", children: (0, jsx_runtime_1.jsx)("nav", { className: "sa-toc-list", "aria-label": label, children: items.map((it) => ((0, jsx_runtime_1.jsx)("a", { href: `#${it.id}`, "data-toc-link": it.id, "data-level": it.level, children: it.text }, it.id))) }) }), (0, jsx_runtime_1.jsx)(blog_toc_1.SeoBlogTocSpy, { ids: items.map((it) => it.id) })] })) : null, (0, jsx_runtime_1.jsxs)("article", { className: "sa-article", children: [post.cover ? (0, jsx_runtime_1.jsx)("img", { src: post.cover, alt: post.title, style: { width: "100%", aspectRatio: "3 / 2", objectFit: "cover", display: "block", borderRadius: "var(--sa-radius, 12px)", marginBottom: "1.5rem" } }) : null, (0, jsx_runtime_1.jsx)("h1", { style: { fontSize: "2.2rem", lineHeight: 1.2, margin: "0 0 1rem" }, children: post.title }), post.date ? (0, jsx_runtime_1.jsx)("p", { style: { color: "var(--sa-muted, #94a3b8)", margin: "0 0 1.5rem" }, children: fmtDate(post.date, locale) }) : null, (0, jsx_runtime_1.jsx)("div", { className: "seoagent-blog-body", dangerouslySetInnerHTML: { __html: html } })] })] })] }));
|
|
241
249
|
}
|
|
242
250
|
function fmtDate(d, locale) {
|
|
243
251
|
try {
|
package/package.json
CHANGED