chaingrow-blog 0.2.2 → 0.7.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/README.md +617 -139
- package/dist/blog-cards-D1j7ZqHK.d.ts +123 -0
- package/dist/blog-cards-DPfcmUUD.d.cts +123 -0
- package/dist/chunk-4EKET2DQ.js +34 -0
- package/dist/chunk-4EKET2DQ.js.map +1 -0
- package/dist/chunk-BXEQ6QKM.js +636 -0
- package/dist/chunk-BXEQ6QKM.js.map +1 -0
- package/dist/chunk-K3HNYPEP.js +176 -0
- package/dist/chunk-K3HNYPEP.js.map +1 -0
- package/dist/index.cjs +237 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -3
- package/dist/index.d.ts +20 -3
- package/dist/index.js +101 -3
- package/dist/index.js.map +1 -1
- package/dist/next/index.cjs +682 -11
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +168 -2
- package/dist/next/index.d.ts +168 -2
- package/dist/next/index.js +14 -42
- package/dist/next/index.js.map +1 -1
- package/dist/next/webhook.d.cts +1 -1
- package/dist/next/webhook.d.ts +1 -1
- package/dist/react/index.cjs +721 -37
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +189 -31
- package/dist/react/index.d.ts +189 -31
- package/dist/react/index.js +265 -65
- package/dist/react/index.js.map +1 -1
- package/dist/types-DvkCm1rh.d.cts +114 -0
- package/dist/types-DvkCm1rh.d.ts +114 -0
- package/package.json +1 -1
- package/dist/chunk-OTLGKSYD.js +0 -74
- package/dist/chunk-OTLGKSYD.js.map +0 -1
- package/dist/types-CoPD0ejB.d.cts +0 -53
- package/dist/types-CoPD0ejB.d.ts +0 -53
package/dist/react/index.d.ts
CHANGED
|
@@ -1,62 +1,220 @@
|
|
|
1
|
-
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
1
|
import * as React from 'react';
|
|
3
|
-
import {
|
|
4
|
-
export { F as FaqItem, S as SocialPost } from '../types-
|
|
2
|
+
import { b as BlockType, L as ListItem, a as Block, E as EmbeddedMedia, B as Blog } from '../types-DvkCm1rh.js';
|
|
3
|
+
export { F as FaqItem, I as Inline, M as Mark, S as SocialPost } from '../types-DvkCm1rh.js';
|
|
4
|
+
import { NextClientOptions } from '../next/index.js';
|
|
5
|
+
import { a as BlogJsonLdOptions } from '../blog-cards-D1j7ZqHK.js';
|
|
6
|
+
export { B as BlogCard, I as InterleavedSlot, T as ToBlogCardOptions, e as distributeMediaPositions, i as interleaveMedia, m as mergeBlogs, t as toBlogCard } from '../blog-cards-D1j7ZqHK.js';
|
|
7
|
+
import 'next';
|
|
5
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Override map for per-block components. Keys are the discriminants
|
|
11
|
+
* of `Block`. Consumers pass any subset; unspecified blocks render
|
|
12
|
+
* via the matching `Default*` component.
|
|
13
|
+
*
|
|
14
|
+
* v0.3.0 breaking change: the keys changed to match the new Block
|
|
15
|
+
* union. If you upgraded from 0.2.x, rename `list` → `bulletList` /
|
|
16
|
+
* `orderedList`, `quote` → `blockquote`, `code` → `codeBlock`, drop
|
|
17
|
+
* `image_placeholder` (images are now first-class via `image`).
|
|
18
|
+
*/
|
|
6
19
|
type BlockComponents = Partial<{
|
|
7
20
|
heading: React.ComponentType<{
|
|
8
|
-
level:
|
|
21
|
+
level: 1 | 2 | 3;
|
|
9
22
|
children: React.ReactNode;
|
|
23
|
+
className?: string;
|
|
10
24
|
}>;
|
|
11
25
|
paragraph: React.ComponentType<{
|
|
12
26
|
children: React.ReactNode;
|
|
27
|
+
className?: string;
|
|
13
28
|
}>;
|
|
14
|
-
|
|
15
|
-
items:
|
|
29
|
+
bulletList: React.ComponentType<{
|
|
30
|
+
items: ListItem[];
|
|
31
|
+
className?: string;
|
|
32
|
+
renderBlock: (b: Block, i: number) => React.ReactNode;
|
|
16
33
|
}>;
|
|
17
|
-
|
|
18
|
-
|
|
34
|
+
orderedList: React.ComponentType<{
|
|
35
|
+
items: ListItem[];
|
|
36
|
+
className?: string;
|
|
37
|
+
renderBlock: (b: Block, i: number) => React.ReactNode;
|
|
19
38
|
}>;
|
|
20
|
-
|
|
39
|
+
blockquote: React.ComponentType<{
|
|
21
40
|
children: React.ReactNode;
|
|
41
|
+
className?: string;
|
|
42
|
+
}>;
|
|
43
|
+
codeBlock: React.ComponentType<{
|
|
44
|
+
language?: string;
|
|
45
|
+
code: string;
|
|
46
|
+
className?: string;
|
|
22
47
|
}>;
|
|
23
48
|
image: React.ComponentType<{
|
|
24
|
-
src
|
|
49
|
+
src: string;
|
|
25
50
|
alt: string;
|
|
51
|
+
caption?: string;
|
|
52
|
+
width?: number;
|
|
53
|
+
height?: number;
|
|
54
|
+
mediaId?: string;
|
|
55
|
+
className?: string;
|
|
56
|
+
}>;
|
|
57
|
+
divider: React.ComponentType<{
|
|
58
|
+
className?: string;
|
|
59
|
+
}>;
|
|
60
|
+
/** Renders a single attached image/video when `<BlogRenderer>` distributes
|
|
61
|
+
* `blog.generatedMedia` across the body. Default renders a bare
|
|
62
|
+
* `<img>` or `<video controls>`. */
|
|
63
|
+
media: React.ComponentType<{
|
|
64
|
+
media: EmbeddedMedia;
|
|
26
65
|
}>;
|
|
27
66
|
wrapper: React.ComponentType<{
|
|
28
67
|
children: React.ReactNode;
|
|
29
68
|
blog: Blog;
|
|
30
69
|
}>;
|
|
31
70
|
}>;
|
|
32
|
-
|
|
71
|
+
/** Simple Tailwind-style className override map. Applied to the
|
|
72
|
+
* default component for each block type. `components` override takes
|
|
73
|
+
* precedence — if you replace the component, className won't reach it
|
|
74
|
+
* unless your override handles it. */
|
|
75
|
+
type BlockClassNames = Partial<Record<BlockType, string>>;
|
|
76
|
+
declare function BlockRenderer({ block, components, classNames, renderBlock, }: {
|
|
33
77
|
block: Block;
|
|
34
78
|
components?: BlockComponents;
|
|
35
|
-
|
|
36
|
-
|
|
79
|
+
classNames?: BlockClassNames;
|
|
80
|
+
renderBlock?: (b: Block, i: number) => React.ReactNode;
|
|
81
|
+
}): React.JSX.Element;
|
|
82
|
+
declare function BlogRenderer({ blog, components, classNames, }: {
|
|
37
83
|
blog: Blog;
|
|
38
84
|
components?: BlockComponents;
|
|
39
|
-
|
|
85
|
+
classNames?: BlockClassNames;
|
|
86
|
+
}): React.JSX.Element;
|
|
40
87
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
88
|
+
type BlogDetailComponents = {
|
|
89
|
+
/**
|
|
90
|
+
* Renders above the body. Default: og_image → h1 → time → description.
|
|
91
|
+
* Override to add tags, author chip, share buttons, read time, etc.
|
|
92
|
+
*/
|
|
93
|
+
Header?: (props: {
|
|
94
|
+
blog: Blog;
|
|
95
|
+
formatDate: (iso: string) => string;
|
|
96
|
+
}) => React.ReactNode;
|
|
97
|
+
/** Renders below the body. Default: nothing. */
|
|
98
|
+
Footer?: (props: {
|
|
99
|
+
blog: Blog;
|
|
100
|
+
}) => React.ReactNode;
|
|
101
|
+
/** Forwarded to <BlogRenderer components={...} />. */
|
|
102
|
+
blocks?: BlockComponents;
|
|
103
|
+
};
|
|
104
|
+
declare function BlogDetail({ blog, components, formatDate, }: {
|
|
105
|
+
blog: Blog;
|
|
106
|
+
components?: BlogDetailComponents;
|
|
107
|
+
formatDate?: (iso: string) => string;
|
|
108
|
+
}): React.JSX.Element;
|
|
109
|
+
|
|
110
|
+
type BlogIndexFormat = 'bare' | 'minimalist' | 'list';
|
|
111
|
+
type BlogIndexProps = {
|
|
112
|
+
/** Visual layout. `minimalist` (default) is a stark monochrome grid:
|
|
113
|
+
* square image, bold title, gray date — black-on-white in light mode,
|
|
114
|
+
* white-on-black in dark mode, no borders or shadows. `list` is a
|
|
115
|
+
* single-column feed with a thumbnail beside each entry. `bare` is
|
|
116
|
+
* unstyled semantic HTML for users wiring their own design. All three
|
|
117
|
+
* are emitted with self-contained styles — no CSS framework needed.
|
|
118
|
+
* For full design control, skip this component entirely and compose
|
|
119
|
+
* `listBlogs()` + your own JSX. */
|
|
120
|
+
format?: BlogIndexFormat;
|
|
121
|
+
/** Filter ChainGrow posts to a single language code (e.g. `'de'`, `'en'`). */
|
|
122
|
+
language?: string;
|
|
123
|
+
/** URL prefix for the generated card hrefs. Defaults to `/blog`.
|
|
124
|
+
* On locale-prefixed sites, derive it from the active locale so EN cards
|
|
125
|
+
* link to the EN route: `pathPrefix={locale === 'de' ? '/blog' : `/${locale}/blog`}`. */
|
|
126
|
+
pathPrefix?: string;
|
|
127
|
+
/** Full control over each card's href — takes precedence over `pathPrefix`.
|
|
128
|
+
* Use when the route depends on more than a static prefix, e.g.
|
|
129
|
+
* `hrefFor={(b) => (b.language === 'de' ? `/blog/${b.slug}` : `/${b.language}/blog/${b.slug}`)}`. */
|
|
130
|
+
hrefFor?: (blog: Blog) => string;
|
|
131
|
+
/** Override the date formatter shown on each card. */
|
|
132
|
+
formatDate?: (iso: string) => string;
|
|
133
|
+
} & NextClientOptions;
|
|
134
|
+
/**
|
|
135
|
+
* Default-batteries blog list. Calls `listBlogs()` for the configured language
|
|
136
|
+
* and renders one of three preset layouts. For full design control use
|
|
137
|
+
* `listBlogs()` directly and write your own page.
|
|
138
|
+
*/
|
|
139
|
+
declare function BlogIndex({ format, language, pathPrefix, hrefFor, formatDate, apiKey, apiUrl, revalidate, }?: BlogIndexProps): Promise<React.JSX.Element>;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Render an array of Schema.org graph items as a single
|
|
143
|
+
* `<script type="application/ld+json">` tag with a merged `@graph`.
|
|
144
|
+
* Google treats one `@graph` script as equivalent to N separate scripts, and
|
|
145
|
+
* a single tag keeps the DOM uncluttered for consumers who already ship their
|
|
146
|
+
* own schemas (Organization, breadcrumbs, WebSite, …).
|
|
147
|
+
*/
|
|
148
|
+
declare function JsonLdGraph({ items, }: {
|
|
149
|
+
items: Record<string, unknown>[];
|
|
150
|
+
}): React.JSX.Element | null;
|
|
151
|
+
/**
|
|
152
|
+
* Default-batteries JSON-LD for a chaingrow Blog: emits BlogPosting +
|
|
153
|
+
* FAQPage (if the post has FAQs) + any LLM-generated extras from
|
|
154
|
+
* `structured_data.jsonLd`. Consumers who already ship their own graph items
|
|
155
|
+
* (breadcrumbs, Organization, …) pass them via `extraSchemas` and they end
|
|
156
|
+
* up in the same `@graph`.
|
|
157
|
+
*/
|
|
158
|
+
declare function BlogJsonLd({ blog, extraSchemas, ...opts }: {
|
|
159
|
+
blog: Blog;
|
|
160
|
+
extraSchemas?: Record<string, unknown>[];
|
|
161
|
+
} & BlogJsonLdOptions): React.JSX.Element | null;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Default components for each Block type. All are headless — no
|
|
165
|
+
* classNames baked in. Consumers customize via:
|
|
166
|
+
* - `classNames={{ heading: "..." }}` on `<BlogRenderer>` — threads
|
|
167
|
+
* through to `className` on the Default component below.
|
|
168
|
+
* - `components={{ heading: MyHeading }}` — full replacement.
|
|
169
|
+
*/
|
|
170
|
+
declare const DefaultHeading: ({ level, children, className, }: {
|
|
171
|
+
level: 1 | 2 | 3;
|
|
46
172
|
children: React.ReactNode;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}) => react_jsx_runtime.JSX.Element;
|
|
51
|
-
declare const DefaultQuote: ({ children }: {
|
|
173
|
+
className?: string;
|
|
174
|
+
}) => React.JSX.Element;
|
|
175
|
+
declare const DefaultParagraph: ({ children, className, }: {
|
|
52
176
|
children: React.ReactNode;
|
|
53
|
-
|
|
54
|
-
|
|
177
|
+
className?: string;
|
|
178
|
+
}) => React.JSX.Element;
|
|
179
|
+
declare const DefaultBulletList: ({ items, className, renderBlock, }: {
|
|
180
|
+
items: ListItem[];
|
|
181
|
+
className?: string;
|
|
182
|
+
renderBlock: (b: Block, i: number) => React.ReactNode;
|
|
183
|
+
}) => React.JSX.Element;
|
|
184
|
+
declare const DefaultOrderedList: ({ items, className, renderBlock, }: {
|
|
185
|
+
items: ListItem[];
|
|
186
|
+
className?: string;
|
|
187
|
+
renderBlock: (b: Block, i: number) => React.ReactNode;
|
|
188
|
+
}) => React.JSX.Element;
|
|
189
|
+
declare const DefaultBlockquote: ({ children, className, }: {
|
|
55
190
|
children: React.ReactNode;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
191
|
+
className?: string;
|
|
192
|
+
}) => React.JSX.Element;
|
|
193
|
+
declare const DefaultCodeBlock: ({ language, code, className, }: {
|
|
194
|
+
language?: string;
|
|
195
|
+
code: string;
|
|
196
|
+
className?: string;
|
|
197
|
+
}) => React.JSX.Element;
|
|
198
|
+
declare const DefaultImage: ({ src, alt, caption, width, height, className, }: {
|
|
199
|
+
src: string;
|
|
59
200
|
alt: string;
|
|
60
|
-
|
|
201
|
+
caption?: string;
|
|
202
|
+
width?: number;
|
|
203
|
+
height?: number;
|
|
204
|
+
mediaId?: string;
|
|
205
|
+
className?: string;
|
|
206
|
+
}) => React.JSX.Element;
|
|
207
|
+
declare const DefaultDivider: ({ className }: {
|
|
208
|
+
className?: string;
|
|
209
|
+
}) => React.JSX.Element;
|
|
210
|
+
/**
|
|
211
|
+
* Default renderer for a single embedded media item. Bare HTML — no
|
|
212
|
+
* classNames, no styles. Override via `components.media` on `<BlogRenderer>`.
|
|
213
|
+
* `alt` falls back to the generation prompt, which is a sensible description
|
|
214
|
+
* string since it was the LLM input that produced the image.
|
|
215
|
+
*/
|
|
216
|
+
declare const DefaultMedia: ({ media }: {
|
|
217
|
+
media: EmbeddedMedia;
|
|
218
|
+
}) => React.JSX.Element;
|
|
61
219
|
|
|
62
|
-
export { Block, type BlockComponents, BlockRenderer, Blog, BlogRenderer,
|
|
220
|
+
export { Block, type BlockClassNames, type BlockComponents, BlockRenderer, BlockType, Blog, BlogDetail, type BlogDetailComponents, BlogIndex, type BlogIndexFormat, type BlogIndexProps, BlogJsonLd, BlogJsonLdOptions, BlogRenderer, DefaultBlockquote, DefaultBulletList, DefaultCodeBlock, DefaultDivider, DefaultHeading, DefaultImage, DefaultMedia, DefaultOrderedList, DefaultParagraph, EmbeddedMedia, JsonLdGraph, ListItem };
|
package/dist/react/index.js
CHANGED
|
@@ -1,79 +1,279 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
import {
|
|
2
|
+
mergeBlogs,
|
|
3
|
+
toBlogCard
|
|
4
|
+
} from "../chunk-4EKET2DQ.js";
|
|
5
|
+
import {
|
|
6
|
+
BlockRenderer,
|
|
7
|
+
BlogDetail,
|
|
8
|
+
BlogJsonLd,
|
|
9
|
+
BlogRenderer,
|
|
10
|
+
DefaultBlockquote,
|
|
11
|
+
DefaultBulletList,
|
|
12
|
+
DefaultCodeBlock,
|
|
13
|
+
DefaultDivider,
|
|
14
|
+
DefaultHeading,
|
|
15
|
+
DefaultImage,
|
|
16
|
+
DefaultMedia,
|
|
17
|
+
DefaultOrderedList,
|
|
18
|
+
DefaultParagraph,
|
|
19
|
+
JsonLdGraph,
|
|
20
|
+
listBlogs
|
|
21
|
+
} from "../chunk-BXEQ6QKM.js";
|
|
22
|
+
import {
|
|
23
|
+
distributeMediaPositions,
|
|
24
|
+
interleaveMedia
|
|
25
|
+
} from "../chunk-K3HNYPEP.js";
|
|
23
26
|
|
|
24
|
-
// src/react/
|
|
25
|
-
import { Fragment, jsx
|
|
26
|
-
var
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
// src/react/BlogIndex.tsx
|
|
28
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
29
|
+
var defaultFormatDate = (iso) => new Date(iso).toLocaleDateString();
|
|
30
|
+
async function BlogIndex({
|
|
31
|
+
format = "minimalist",
|
|
32
|
+
language,
|
|
33
|
+
pathPrefix,
|
|
34
|
+
hrefFor,
|
|
35
|
+
formatDate = defaultFormatDate,
|
|
36
|
+
apiKey,
|
|
37
|
+
apiUrl,
|
|
38
|
+
revalidate
|
|
39
|
+
} = {}) {
|
|
40
|
+
const blogs = await listBlogs(
|
|
41
|
+
{ status: "PUBLISHED", language },
|
|
42
|
+
{ apiKey, apiUrl, revalidate }
|
|
43
|
+
);
|
|
44
|
+
const cards = blogs.map((b) => toBlogCard(b, { pathPrefix, hrefFor }));
|
|
45
|
+
if (cards.length === 0) {
|
|
46
|
+
return /* @__PURE__ */ jsx(EmptyState, {});
|
|
47
|
+
}
|
|
48
|
+
if (format === "bare") return /* @__PURE__ */ jsx(BareLayout, { cards, formatDate });
|
|
49
|
+
if (format === "list") return /* @__PURE__ */ jsx(ListLayout, { cards, formatDate });
|
|
50
|
+
return /* @__PURE__ */ jsx(MinimalistLayout, { cards, formatDate });
|
|
51
|
+
}
|
|
52
|
+
function EmptyState() {
|
|
53
|
+
return /* @__PURE__ */ jsx("p", { children: "No posts yet." });
|
|
54
|
+
}
|
|
55
|
+
function BareLayout({
|
|
56
|
+
cards,
|
|
57
|
+
formatDate
|
|
30
58
|
}) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
return /* @__PURE__ */ jsx("ul", { children: cards.map((card) => /* @__PURE__ */ jsxs("li", { children: [
|
|
60
|
+
/* @__PURE__ */ jsx("a", { href: card.href, children: /* @__PURE__ */ jsx("h2", { children: card.title }) }),
|
|
61
|
+
card.published_at && /* @__PURE__ */ jsx("time", { dateTime: card.published_at, children: formatDate(card.published_at) }),
|
|
62
|
+
card.description && /* @__PURE__ */ jsx("p", { children: card.description })
|
|
63
|
+
] }, card.id)) });
|
|
64
|
+
}
|
|
65
|
+
var MINIMALIST_CSS = `
|
|
66
|
+
.cg-blog-min-container {
|
|
67
|
+
max-width: 1400px;
|
|
68
|
+
margin: 4rem auto;
|
|
69
|
+
padding: 0 2rem;
|
|
70
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
71
|
+
-webkit-font-smoothing: antialiased;
|
|
72
|
+
-moz-osx-font-smoothing: grayscale;
|
|
73
|
+
}
|
|
74
|
+
.cg-blog-min-grid {
|
|
75
|
+
display: grid;
|
|
76
|
+
grid-template-columns: repeat(3, 1fr);
|
|
77
|
+
column-gap: 2rem;
|
|
78
|
+
row-gap: 4rem;
|
|
79
|
+
list-style: none;
|
|
80
|
+
padding: 0;
|
|
81
|
+
margin: 0;
|
|
82
|
+
}
|
|
83
|
+
.cg-blog-min-grid > li { list-style: none; }
|
|
84
|
+
.cg-blog-min-card {
|
|
85
|
+
display: block;
|
|
86
|
+
text-decoration: none;
|
|
87
|
+
color: inherit;
|
|
88
|
+
}
|
|
89
|
+
.cg-blog-min-card:focus-visible {
|
|
90
|
+
outline: 2px solid currentColor;
|
|
91
|
+
outline-offset: 4px;
|
|
92
|
+
border-radius: 4px;
|
|
93
|
+
}
|
|
94
|
+
.cg-blog-min-image {
|
|
95
|
+
width: 100%;
|
|
96
|
+
aspect-ratio: 1 / 1;
|
|
97
|
+
object-fit: cover;
|
|
98
|
+
display: block;
|
|
99
|
+
border-radius: 6px;
|
|
100
|
+
background: rgba(128, 128, 128, 0.08);
|
|
101
|
+
margin-bottom: 1.25rem;
|
|
102
|
+
transition: opacity 200ms ease;
|
|
103
|
+
}
|
|
104
|
+
.cg-blog-min-card:hover .cg-blog-min-image {
|
|
105
|
+
opacity: 0.88;
|
|
106
|
+
}
|
|
107
|
+
.cg-blog-min-no-image {
|
|
108
|
+
width: 100%;
|
|
109
|
+
aspect-ratio: 1 / 1;
|
|
110
|
+
background: rgba(128, 128, 128, 0.08);
|
|
111
|
+
border-radius: 6px;
|
|
112
|
+
margin-bottom: 1.25rem;
|
|
113
|
+
}
|
|
114
|
+
.cg-blog-min-title {
|
|
115
|
+
font-size: 1.25rem;
|
|
116
|
+
font-weight: 600;
|
|
117
|
+
margin: 0 0 0.5rem;
|
|
118
|
+
line-height: 1.35;
|
|
119
|
+
color: inherit;
|
|
120
|
+
letter-spacing: -0.01em;
|
|
121
|
+
}
|
|
122
|
+
.cg-blog-min-date {
|
|
123
|
+
font-size: 0.8125rem;
|
|
124
|
+
color: currentColor;
|
|
125
|
+
opacity: 0.55;
|
|
126
|
+
margin: 0;
|
|
127
|
+
font-weight: 400;
|
|
128
|
+
}
|
|
129
|
+
@media (max-width: 1024px) {
|
|
130
|
+
.cg-blog-min-grid {
|
|
131
|
+
grid-template-columns: repeat(2, 1fr);
|
|
132
|
+
column-gap: 1.5rem;
|
|
133
|
+
row-gap: 3rem;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
@media (max-width: 640px) {
|
|
137
|
+
.cg-blog-min-container {
|
|
138
|
+
margin: 2.5rem auto;
|
|
139
|
+
padding: 0 1.25rem;
|
|
140
|
+
}
|
|
141
|
+
.cg-blog-min-grid {
|
|
142
|
+
grid-template-columns: 1fr;
|
|
143
|
+
row-gap: 2.5rem;
|
|
58
144
|
}
|
|
145
|
+
.cg-blog-min-title { font-size: 1.125rem; }
|
|
146
|
+
}
|
|
147
|
+
`;
|
|
148
|
+
function MinimalistLayout({
|
|
149
|
+
cards,
|
|
150
|
+
formatDate
|
|
151
|
+
}) {
|
|
152
|
+
return /* @__PURE__ */ jsxs("div", { className: "cg-blog-min-container", children: [
|
|
153
|
+
/* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: MINIMALIST_CSS } }),
|
|
154
|
+
/* @__PURE__ */ jsx("ul", { className: "cg-blog-min-grid", children: cards.map((card) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("a", { href: card.href, className: "cg-blog-min-card", children: [
|
|
155
|
+
card.og_image ? (
|
|
156
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
157
|
+
/* @__PURE__ */ jsx(
|
|
158
|
+
"img",
|
|
159
|
+
{
|
|
160
|
+
src: card.og_image,
|
|
161
|
+
alt: card.title,
|
|
162
|
+
className: "cg-blog-min-image",
|
|
163
|
+
loading: "lazy"
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
) : /* @__PURE__ */ jsx("div", { className: "cg-blog-min-no-image", "aria-hidden": "true" }),
|
|
167
|
+
/* @__PURE__ */ jsx("h2", { className: "cg-blog-min-title", children: card.title }),
|
|
168
|
+
card.published_at && /* @__PURE__ */ jsx("p", { className: "cg-blog-min-date", children: formatDate(card.published_at) })
|
|
169
|
+
] }) }, card.id)) })
|
|
170
|
+
] });
|
|
171
|
+
}
|
|
172
|
+
var LIST_CSS = `
|
|
173
|
+
.cg-blog-list { list-style: none; padding: 0; margin: 0; }
|
|
174
|
+
.cg-blog-list-item {
|
|
175
|
+
margin-bottom: 2rem;
|
|
176
|
+
padding-bottom: 2rem;
|
|
177
|
+
border-bottom: 1px solid currentColor;
|
|
178
|
+
}
|
|
179
|
+
.cg-blog-list-item { border-bottom-color: color-mix(in srgb, currentColor 15%, transparent); }
|
|
180
|
+
.cg-blog-list-item:last-child {
|
|
181
|
+
margin-bottom: 0;
|
|
182
|
+
padding-bottom: 0;
|
|
183
|
+
border-bottom: none;
|
|
184
|
+
}
|
|
185
|
+
.cg-blog-list-link {
|
|
186
|
+
display: flex;
|
|
187
|
+
gap: 1.5rem;
|
|
188
|
+
text-decoration: none;
|
|
189
|
+
color: inherit;
|
|
190
|
+
align-items: flex-start;
|
|
191
|
+
}
|
|
192
|
+
.cg-blog-list-image {
|
|
193
|
+
flex-shrink: 0;
|
|
194
|
+
width: 12rem;
|
|
195
|
+
height: 8rem;
|
|
196
|
+
object-fit: cover;
|
|
197
|
+
border-radius: 0.5rem;
|
|
198
|
+
background: rgba(128, 128, 128, 0.08);
|
|
199
|
+
transition: opacity 200ms ease;
|
|
200
|
+
}
|
|
201
|
+
.cg-blog-list-link:hover .cg-blog-list-image { opacity: 0.88; }
|
|
202
|
+
.cg-blog-list-body { flex: 1; min-width: 0; }
|
|
203
|
+
.cg-blog-list-date {
|
|
204
|
+
font-size: 0.75rem;
|
|
205
|
+
color: currentColor;
|
|
206
|
+
opacity: 0.55;
|
|
207
|
+
margin: 0 0 0.5rem 0;
|
|
208
|
+
}
|
|
209
|
+
.cg-blog-list-title {
|
|
210
|
+
font-size: 1.5rem;
|
|
211
|
+
font-weight: 700;
|
|
212
|
+
margin: 0 0 0.5rem 0;
|
|
213
|
+
line-height: 1.3;
|
|
214
|
+
color: inherit;
|
|
215
|
+
transition: opacity 150ms ease;
|
|
216
|
+
}
|
|
217
|
+
.cg-blog-list-link:hover .cg-blog-list-title { opacity: 0.7; }
|
|
218
|
+
.cg-blog-list-description {
|
|
219
|
+
font-size: 1rem;
|
|
220
|
+
color: currentColor;
|
|
221
|
+
opacity: 0.65;
|
|
222
|
+
margin: 0;
|
|
223
|
+
line-height: 1.6;
|
|
224
|
+
display: -webkit-box;
|
|
225
|
+
-webkit-line-clamp: 2;
|
|
226
|
+
-webkit-box-orient: vertical;
|
|
227
|
+
overflow: hidden;
|
|
228
|
+
}
|
|
229
|
+
@media (max-width: 640px) {
|
|
230
|
+
.cg-blog-list-link { flex-direction: column; }
|
|
231
|
+
.cg-blog-list-image { width: 100%; height: 12rem; }
|
|
59
232
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
233
|
+
`;
|
|
234
|
+
function ListLayout({
|
|
235
|
+
cards,
|
|
236
|
+
formatDate
|
|
63
237
|
}) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
238
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
239
|
+
/* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: LIST_CSS } }),
|
|
240
|
+
/* @__PURE__ */ jsx("ul", { className: "cg-blog-list", children: cards.map((card) => /* @__PURE__ */ jsx("li", { className: "cg-blog-list-item", children: /* @__PURE__ */ jsxs("a", { href: card.href, className: "cg-blog-list-link", children: [
|
|
241
|
+
card.og_image && // eslint-disable-next-line @next/next/no-img-element
|
|
242
|
+
/* @__PURE__ */ jsx(
|
|
243
|
+
"img",
|
|
244
|
+
{
|
|
245
|
+
src: card.og_image,
|
|
246
|
+
alt: card.title,
|
|
247
|
+
className: "cg-blog-list-image"
|
|
248
|
+
}
|
|
249
|
+
),
|
|
250
|
+
/* @__PURE__ */ jsxs("div", { className: "cg-blog-list-body", children: [
|
|
251
|
+
card.published_at && /* @__PURE__ */ jsx("p", { className: "cg-blog-list-date", children: formatDate(card.published_at) }),
|
|
252
|
+
/* @__PURE__ */ jsx("h2", { className: "cg-blog-list-title", children: card.title }),
|
|
253
|
+
card.description && /* @__PURE__ */ jsx("p", { className: "cg-blog-list-description", children: card.description })
|
|
254
|
+
] })
|
|
255
|
+
] }) }, card.id)) })
|
|
256
|
+
] });
|
|
68
257
|
}
|
|
69
258
|
export {
|
|
70
259
|
BlockRenderer,
|
|
260
|
+
BlogDetail,
|
|
261
|
+
BlogIndex,
|
|
262
|
+
BlogJsonLd,
|
|
71
263
|
BlogRenderer,
|
|
72
|
-
|
|
264
|
+
DefaultBlockquote,
|
|
265
|
+
DefaultBulletList,
|
|
266
|
+
DefaultCodeBlock,
|
|
267
|
+
DefaultDivider,
|
|
73
268
|
DefaultHeading,
|
|
74
269
|
DefaultImage,
|
|
75
|
-
|
|
270
|
+
DefaultMedia,
|
|
271
|
+
DefaultOrderedList,
|
|
76
272
|
DefaultParagraph,
|
|
77
|
-
|
|
273
|
+
JsonLdGraph,
|
|
274
|
+
distributeMediaPositions,
|
|
275
|
+
interleaveMedia,
|
|
276
|
+
mergeBlogs,
|
|
277
|
+
toBlogCard
|
|
78
278
|
};
|
|
79
279
|
//# sourceMappingURL=index.js.map
|
package/dist/react/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/defaults.tsx","../../src/react/BlogRenderer.tsx"],"sourcesContent":["import * as React from 'react';\n\nexport const DefaultHeading = ({\n level,\n children,\n}: {\n level: number;\n children: React.ReactNode;\n}) => {\n const Tag = (level === 2 ? 'h2' : level === 4 ? 'h4' : 'h3') as\n | 'h2'\n | 'h3'\n | 'h4';\n return <Tag>{children}</Tag>;\n};\n\nexport const DefaultParagraph = ({\n children,\n}: {\n children: React.ReactNode;\n}) => <p>{children}</p>;\n\nexport const DefaultList = ({ items }: { items: string[] }) => (\n <ul>\n {items.map((item, i) => (\n <li key={i}>{item}</li>\n ))}\n </ul>\n);\n\nexport const DefaultQuote = ({ children }: { children: React.ReactNode }) => (\n <blockquote>{children}</blockquote>\n);\n\nexport const DefaultCode = ({ children }: { children: React.ReactNode }) => (\n <pre>\n <code>{children}</code>\n </pre>\n);\n\nexport const DefaultImage = ({\n src,\n alt,\n}: {\n src?: string;\n alt: string;\n}) => {\n if (!src) return null;\n // eslint-disable-next-line @next/next/no-img-element\n return <img src={src} alt={alt} />;\n};\n","import * as React from 'react';\nimport type { Blog, Block } from '../types';\nimport {\n DefaultHeading,\n DefaultParagraph,\n DefaultList,\n DefaultQuote,\n DefaultCode,\n DefaultImage,\n} from './defaults';\n\nexport type BlockComponents = Partial<{\n heading: React.ComponentType<{ level: number; children: React.ReactNode }>;\n paragraph: React.ComponentType<{ children: React.ReactNode }>;\n list: React.ComponentType<{ items: string[] }>;\n quote: React.ComponentType<{ children: React.ReactNode }>;\n code: React.ComponentType<{ children: React.ReactNode }>;\n image: React.ComponentType<{ src?: string; alt: string }>;\n wrapper: React.ComponentType<{ children: React.ReactNode; blog: Blog }>;\n}>;\n\nconst parseListItems = (content: string): string[] =>\n content\n .split('\\n')\n .map((line) => line.replace(/^\\s*[-•*]\\s*/, '').trim())\n .filter(Boolean);\n\nexport function BlockRenderer({\n block,\n components = {},\n}: {\n block: Block;\n components?: BlockComponents;\n}) {\n switch (block.type) {\n case 'heading': {\n const Heading = components.heading ?? DefaultHeading;\n return <Heading level={block.level ?? 2}>{block.content}</Heading>;\n }\n case 'paragraph': {\n const Paragraph = components.paragraph ?? DefaultParagraph;\n return <Paragraph>{block.content}</Paragraph>;\n }\n case 'list': {\n const List = components.list ?? DefaultList;\n return <List items={parseListItems(block.content)} />;\n }\n case 'quote': {\n const Quote = components.quote ?? DefaultQuote;\n return <Quote>{block.content}</Quote>;\n }\n case 'code': {\n const Code = components.code ?? DefaultCode;\n return <Code>{block.content}</Code>;\n }\n case 'image_placeholder': {\n const Image = components.image ?? DefaultImage;\n return <Image alt={block.content} />;\n }\n default:\n return null;\n }\n}\n\nexport function BlogRenderer({\n blog,\n components = {},\n}: {\n blog: Blog;\n components?: BlockComponents;\n}) {\n const Wrapper = components.wrapper;\n const content = (\n <>\n {blog.blocks.map((block, i) => (\n <BlockRenderer key={i} block={block} components={components} />\n ))}\n </>\n );\n if (Wrapper) return <Wrapper blog={blog}>{content}</Wrapper>;\n return content;\n}\n"],"mappings":";AAaS;AAXF,IAAM,iBAAiB,CAAC;AAAA,EAC7B;AAAA,EACA;AACF,MAGM;AACJ,QAAM,MAAO,UAAU,IAAI,OAAO,UAAU,IAAI,OAAO;AAIvD,SAAO,oBAAC,OAAK,UAAS;AACxB;AAEO,IAAM,mBAAmB,CAAC;AAAA,EAC/B;AACF,MAEM,oBAAC,OAAG,UAAS;AAEZ,IAAM,cAAc,CAAC,EAAE,MAAM,MAClC,oBAAC,QACE,gBAAM,IAAI,CAAC,MAAM,MAChB,oBAAC,QAAY,kBAAJ,CAAS,CACnB,GACH;AAGK,IAAM,eAAe,CAAC,EAAE,SAAS,MACtC,oBAAC,gBAAY,UAAS;AAGjB,IAAM,cAAc,CAAC,EAAE,SAAS,MACrC,oBAAC,SACC,8BAAC,UAAM,UAAS,GAClB;AAGK,IAAM,eAAe,CAAC;AAAA,EAC3B;AAAA,EACA;AACF,MAGM;AACJ,MAAI,CAAC,IAAK,QAAO;AAEjB,SAAO,oBAAC,SAAI,KAAU,KAAU;AAClC;;;ACba,SAoCT,UApCS,OAAAA,YAAA;AAhBb,IAAM,iBAAiB,CAAC,YACtB,QACG,MAAM,IAAI,EACV,IAAI,CAAC,SAAS,KAAK,QAAQ,gBAAgB,EAAE,EAAE,KAAK,CAAC,EACrD,OAAO,OAAO;AAEZ,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA,aAAa,CAAC;AAChB,GAGG;AACD,UAAQ,MAAM,MAAM;AAAA,IAClB,KAAK,WAAW;AACd,YAAM,UAAU,WAAW,WAAW;AACtC,aAAO,gBAAAA,KAAC,WAAQ,OAAO,MAAM,SAAS,GAAI,gBAAM,SAAQ;AAAA,IAC1D;AAAA,IACA,KAAK,aAAa;AAChB,YAAM,YAAY,WAAW,aAAa;AAC1C,aAAO,gBAAAA,KAAC,aAAW,gBAAM,SAAQ;AAAA,IACnC;AAAA,IACA,KAAK,QAAQ;AACX,YAAM,OAAO,WAAW,QAAQ;AAChC,aAAO,gBAAAA,KAAC,QAAK,OAAO,eAAe,MAAM,OAAO,GAAG;AAAA,IACrD;AAAA,IACA,KAAK,SAAS;AACZ,YAAM,QAAQ,WAAW,SAAS;AAClC,aAAO,gBAAAA,KAAC,SAAO,gBAAM,SAAQ;AAAA,IAC/B;AAAA,IACA,KAAK,QAAQ;AACX,YAAM,OAAO,WAAW,QAAQ;AAChC,aAAO,gBAAAA,KAAC,QAAM,gBAAM,SAAQ;AAAA,IAC9B;AAAA,IACA,KAAK,qBAAqB;AACxB,YAAM,QAAQ,WAAW,SAAS;AAClC,aAAO,gBAAAA,KAAC,SAAM,KAAK,MAAM,SAAS;AAAA,IACpC;AAAA,IACA;AACE,aAAO;AAAA,EACX;AACF;AAEO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA,aAAa,CAAC;AAChB,GAGG;AACD,QAAM,UAAU,WAAW;AAC3B,QAAM,UACJ,gBAAAA,KAAA,YACG,eAAK,OAAO,IAAI,CAAC,OAAO,MACvB,gBAAAA,KAAC,iBAAsB,OAAc,cAAjB,CAAyC,CAC9D,GACH;AAEF,MAAI,QAAS,QAAO,gBAAAA,KAAC,WAAQ,MAAa,mBAAQ;AAClD,SAAO;AACT;","names":["jsx"]}
|
|
1
|
+
{"version":3,"sources":["../../src/react/BlogIndex.tsx"],"sourcesContent":["import * as React from 'react';\nimport { listBlogs, type NextClientOptions } from '../next/index';\nimport { toBlogCard, type BlogCard } from '../blog-cards';\nimport type { Blog } from '../types';\n\nexport type { BlogCard };\n\nexport type BlogIndexFormat = 'bare' | 'minimalist' | 'list';\n\nexport type BlogIndexProps = {\n /** Visual layout. `minimalist` (default) is a stark monochrome grid:\n * square image, bold title, gray date — black-on-white in light mode,\n * white-on-black in dark mode, no borders or shadows. `list` is a\n * single-column feed with a thumbnail beside each entry. `bare` is\n * unstyled semantic HTML for users wiring their own design. All three\n * are emitted with self-contained styles — no CSS framework needed.\n * For full design control, skip this component entirely and compose\n * `listBlogs()` + your own JSX. */\n format?: BlogIndexFormat;\n /** Filter ChainGrow posts to a single language code (e.g. `'de'`, `'en'`). */\n language?: string;\n /** URL prefix for the generated card hrefs. Defaults to `/blog`.\n * On locale-prefixed sites, derive it from the active locale so EN cards\n * link to the EN route: `pathPrefix={locale === 'de' ? '/blog' : `/${locale}/blog`}`. */\n pathPrefix?: string;\n /** Full control over each card's href — takes precedence over `pathPrefix`.\n * Use when the route depends on more than a static prefix, e.g.\n * `hrefFor={(b) => (b.language === 'de' ? `/blog/${b.slug}` : `/${b.language}/blog/${b.slug}`)}`. */\n hrefFor?: (blog: Blog) => string;\n /** Override the date formatter shown on each card. */\n formatDate?: (iso: string) => string;\n} & NextClientOptions;\n\nconst defaultFormatDate = (iso: string): string =>\n new Date(iso).toLocaleDateString();\n\n/**\n * Default-batteries blog list. Calls `listBlogs()` for the configured language\n * and renders one of three preset layouts. For full design control use\n * `listBlogs()` directly and write your own page.\n */\nexport async function BlogIndex({\n format = 'minimalist',\n language,\n pathPrefix,\n hrefFor,\n formatDate = defaultFormatDate,\n apiKey,\n apiUrl,\n revalidate,\n}: BlogIndexProps = {}): Promise<React.JSX.Element> {\n const blogs = await listBlogs(\n { status: 'PUBLISHED', language },\n { apiKey, apiUrl, revalidate },\n );\n const cards = blogs.map((b) => toBlogCard(b, { pathPrefix, hrefFor }));\n\n if (cards.length === 0) {\n return <EmptyState />;\n }\n\n if (format === 'bare') return <BareLayout cards={cards} formatDate={formatDate} />;\n if (format === 'list') return <ListLayout cards={cards} formatDate={formatDate} />;\n return <MinimalistLayout cards={cards} formatDate={formatDate} />;\n}\n\nfunction EmptyState(): React.JSX.Element {\n return <p>No posts yet.</p>;\n}\n\nfunction BareLayout({\n cards,\n formatDate,\n}: {\n cards: BlogCard[];\n formatDate: (iso: string) => string;\n}): React.JSX.Element {\n return (\n <ul>\n {cards.map((card) => (\n <li key={card.id}>\n <a href={card.href}>\n <h2>{card.title}</h2>\n </a>\n {card.published_at && (\n <time dateTime={card.published_at}>\n {formatDate(card.published_at)}\n </time>\n )}\n {card.description && <p>{card.description}</p>}\n </li>\n ))}\n </ul>\n );\n}\n\n// Color strategy: SDK never hardcodes light/dark. Title and date use\n// \\`color: inherit\\` / \\`currentColor\\` so they pick up whatever color\n// the host app's body/page is using. The host's theme toggle (Tailwind\n// \\`dark:\\` class, \\`data-theme\\`, \\`prefers-color-scheme\\`, anything) drives\n// the SDK automatically — no SDK-side theme knowledge needed.\n//\n// Date fades to 55% of currentColor — that gives the \"lighter gray\"\n// against any background (black-on-white → gray; white-on-black → gray).\n//\n// Image backgrounds use a translucent neutral so the placeholder is\n// visible on both light and dark host bodies without us picking a side.\nconst MINIMALIST_CSS = `\n.cg-blog-min-container {\n max-width: 1400px;\n margin: 4rem auto;\n padding: 0 2rem;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n.cg-blog-min-grid {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n column-gap: 2rem;\n row-gap: 4rem;\n list-style: none;\n padding: 0;\n margin: 0;\n}\n.cg-blog-min-grid > li { list-style: none; }\n.cg-blog-min-card {\n display: block;\n text-decoration: none;\n color: inherit;\n}\n.cg-blog-min-card:focus-visible {\n outline: 2px solid currentColor;\n outline-offset: 4px;\n border-radius: 4px;\n}\n.cg-blog-min-image {\n width: 100%;\n aspect-ratio: 1 / 1;\n object-fit: cover;\n display: block;\n border-radius: 6px;\n background: rgba(128, 128, 128, 0.08);\n margin-bottom: 1.25rem;\n transition: opacity 200ms ease;\n}\n.cg-blog-min-card:hover .cg-blog-min-image {\n opacity: 0.88;\n}\n.cg-blog-min-no-image {\n width: 100%;\n aspect-ratio: 1 / 1;\n background: rgba(128, 128, 128, 0.08);\n border-radius: 6px;\n margin-bottom: 1.25rem;\n}\n.cg-blog-min-title {\n font-size: 1.25rem;\n font-weight: 600;\n margin: 0 0 0.5rem;\n line-height: 1.35;\n color: inherit;\n letter-spacing: -0.01em;\n}\n.cg-blog-min-date {\n font-size: 0.8125rem;\n color: currentColor;\n opacity: 0.55;\n margin: 0;\n font-weight: 400;\n}\n@media (max-width: 1024px) {\n .cg-blog-min-grid {\n grid-template-columns: repeat(2, 1fr);\n column-gap: 1.5rem;\n row-gap: 3rem;\n }\n}\n@media (max-width: 640px) {\n .cg-blog-min-container {\n margin: 2.5rem auto;\n padding: 0 1.25rem;\n }\n .cg-blog-min-grid {\n grid-template-columns: 1fr;\n row-gap: 2.5rem;\n }\n .cg-blog-min-title { font-size: 1.125rem; }\n}\n`;\n\nfunction MinimalistLayout({\n cards,\n formatDate,\n}: {\n cards: BlogCard[];\n formatDate: (iso: string) => string;\n}): React.JSX.Element {\n return (\n <div className=\"cg-blog-min-container\">\n {/* eslint-disable-next-line react/no-danger */}\n <style dangerouslySetInnerHTML={{ __html: MINIMALIST_CSS }} />\n <ul className=\"cg-blog-min-grid\">\n {cards.map((card) => (\n <li key={card.id}>\n <a href={card.href} className=\"cg-blog-min-card\">\n {card.og_image ? (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={card.og_image}\n alt={card.title}\n className=\"cg-blog-min-image\"\n loading=\"lazy\"\n />\n ) : (\n <div className=\"cg-blog-min-no-image\" aria-hidden=\"true\" />\n )}\n <h2 className=\"cg-blog-min-title\">{card.title}</h2>\n {card.published_at && (\n <p className=\"cg-blog-min-date\">\n {formatDate(card.published_at)}\n </p>\n )}\n </a>\n </li>\n ))}\n </ul>\n </div>\n );\n}\n\n// Same color strategy as the minimalist layout and the article CSS:\n// \\`color: inherit\\` / \\`currentColor\\` with opacity for muted variants, and\n// translucent neutrals for image placeholders — the host app's theme\n// system drives everything, no \\`prefers-color-scheme\\` rules needed.\nconst LIST_CSS = `\n.cg-blog-list { list-style: none; padding: 0; margin: 0; }\n.cg-blog-list-item {\n margin-bottom: 2rem;\n padding-bottom: 2rem;\n border-bottom: 1px solid currentColor;\n}\n.cg-blog-list-item { border-bottom-color: color-mix(in srgb, currentColor 15%, transparent); }\n.cg-blog-list-item:last-child {\n margin-bottom: 0;\n padding-bottom: 0;\n border-bottom: none;\n}\n.cg-blog-list-link {\n display: flex;\n gap: 1.5rem;\n text-decoration: none;\n color: inherit;\n align-items: flex-start;\n}\n.cg-blog-list-image {\n flex-shrink: 0;\n width: 12rem;\n height: 8rem;\n object-fit: cover;\n border-radius: 0.5rem;\n background: rgba(128, 128, 128, 0.08);\n transition: opacity 200ms ease;\n}\n.cg-blog-list-link:hover .cg-blog-list-image { opacity: 0.88; }\n.cg-blog-list-body { flex: 1; min-width: 0; }\n.cg-blog-list-date {\n font-size: 0.75rem;\n color: currentColor;\n opacity: 0.55;\n margin: 0 0 0.5rem 0;\n}\n.cg-blog-list-title {\n font-size: 1.5rem;\n font-weight: 700;\n margin: 0 0 0.5rem 0;\n line-height: 1.3;\n color: inherit;\n transition: opacity 150ms ease;\n}\n.cg-blog-list-link:hover .cg-blog-list-title { opacity: 0.7; }\n.cg-blog-list-description {\n font-size: 1rem;\n color: currentColor;\n opacity: 0.65;\n margin: 0;\n line-height: 1.6;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n@media (max-width: 640px) {\n .cg-blog-list-link { flex-direction: column; }\n .cg-blog-list-image { width: 100%; height: 12rem; }\n}\n`;\n\nfunction ListLayout({\n cards,\n formatDate,\n}: {\n cards: BlogCard[];\n formatDate: (iso: string) => string;\n}): React.JSX.Element {\n return (\n <>\n {/* eslint-disable-next-line react/no-danger */}\n <style dangerouslySetInnerHTML={{ __html: LIST_CSS }} />\n <ul className=\"cg-blog-list\">\n {cards.map((card) => (\n <li key={card.id} className=\"cg-blog-list-item\">\n <a href={card.href} className=\"cg-blog-list-link\">\n {card.og_image && (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={card.og_image}\n alt={card.title}\n className=\"cg-blog-list-image\"\n />\n )}\n <div className=\"cg-blog-list-body\">\n {card.published_at && (\n <p className=\"cg-blog-list-date\">\n {formatDate(card.published_at)}\n </p>\n )}\n <h2 className=\"cg-blog-list-title\">{card.title}</h2>\n {card.description && (\n <p className=\"cg-blog-list-description\">{card.description}</p>\n )}\n </div>\n </a>\n </li>\n ))}\n </ul>\n </>\n );\n}\n"],"mappings":"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","names":[]}
|