@fullstackdatasolutions/articles 1.0.0 → 1.2.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/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.0] - 2026-08-12
9
+
10
+ ### Added
11
+
12
+ - **`Breadcrumb` accepts `containerClassName`.** New optional prop, default `'max-w-7xl'` (unchanged from before), applied to the breadcrumb `<ol>` in place of the previously hardcoded value. Lets a consuming site align the breadcrumb bar's content width to its own reading column instead of the package's fixed default. Found on theroleplayersguild.com, where the breadcrumb's `max-w-7xl` sat visibly wider than the article body's `max-w-3xl` column beneath it.
13
+
14
+ ### Fixed
15
+
16
+ - **`ArticleDetailHero` byline never rendered the configured author avatar.** It always showed a generic user icon even when a single `authors[]` entry had a real `avatar` set (and even though `RelatedArticlesSection`/`ArticleCard` byline avatars worked correctly from the same data). Now resolves and renders the avatar image for the single-author case, matching the existing `ArticleCard` pattern; falls back to the icon when no avatar is set or multiple authors are configured, so existing output is unchanged unless a consumer already had `authors[].avatar` configured.
17
+
18
+ ## [1.1.0] - 2026-08-09
19
+
20
+ ### Added
21
+
22
+ - **Configurable heading font family.** New `ArticlesTheme.headerFontFamily`, optional and falling back to inherit/nothing when unset - existing consumers who don't set it see byte-identical output. Threaded through `ArticleDetailHero` (new optional `config` prop), `RelatedArticlesSection`, and `ArticleCard` (both already accepted `config`) via inline `style={{ fontFamily: config?.theme?.headerFontFamily }}`, and into `ArticlesPage`'s `buildThemeVars` as `--articles-header-font-family`, consistent with the package's existing CSS-var theming pattern. Lets a site use a distinct display face for headings (e.g. a serif) separate from its body `fontFamily`, without hardcoding either into the package. Found auditing theroleplayersguild.com's article pages, which use a serif heading treatment the package couldn't previously express.
23
+
24
+ ### Fixed
25
+
26
+ - **`ArticleCard` hardcoded an indigo hover color.** The title link's `hover:text-indigo-600` was a stray leftover unrelated to any theme token and would visibly clash with any site not using an indigo-ish primary color. Replaced with `hover:text-primary`, matching the token already used elsewhere in the same component.
27
+
8
28
  ## [1.0.0] - 2026-08-05
9
29
 
10
30
  ### Breaking
package/README.md CHANGED
@@ -194,6 +194,7 @@ export default async function Page({ params }: ArticlePageProps) {
194
194
  article={article}
195
195
  authors={authors}
196
196
  categoryBasePath="/articles/category"
197
+ config={siteConfig}
197
198
  />
198
199
  <ArticleViewTracker article={article} config={siteConfig} />
199
200
  {siteConfig.showToc !== false && article.toc && article.toc.length > 0 && (
@@ -580,17 +581,18 @@ All theme values are applied as CSS custom properties on a wrapper `<div>`. Comp
580
581
 
581
582
  ```ts
582
583
  theme: {
583
- fontFamily: "'Inter', sans-serif",
584
- headerColor: '#111827',
585
- textColor: '#6b7280',
586
- backgroundColor: '#ffffff',
587
- linkColor: '#4f46e5',
588
- linkHoverColor: '#4338ca',
589
- borderRadius: '0.5rem',
584
+ fontFamily: "'Inter', sans-serif",
585
+ headerFontFamily: "'Cinzel', serif",
586
+ headerColor: '#111827',
587
+ textColor: '#6b7280',
588
+ backgroundColor: '#ffffff',
589
+ linkColor: '#4f46e5',
590
+ linkHoverColor: '#4338ca',
591
+ borderRadius: '0.5rem',
590
592
  }
591
593
  ```
592
594
 
593
- All fields are optional. Omitted fields fall back to your Tailwind theme tokens.
595
+ All fields are optional. Omitted fields fall back to your Tailwind theme tokens. `headerFontFamily` styles article titles and card/section headings (`ArticleDetailHero`, `RelatedArticlesSection`, `ArticleCard`) separately from body text; omit it to let headings inherit `fontFamily` like everything else.
594
596
 
595
597
  ---
596
598
 
package/dist/index.cjs CHANGED
@@ -265,6 +265,7 @@ function getDisplayItems(items) {
265
265
  function Breadcrumb({
266
266
  items,
267
267
  className = "",
268
+ containerClassName = "max-w-7xl",
268
269
  showSchema = true,
269
270
  separator = ">"
270
271
  }) {
@@ -276,7 +277,7 @@ function Breadcrumb({
276
277
  {
277
278
  "aria-label": "Breadcrumb",
278
279
  className: `border-b border-border bg-background/80 px-4 py-3 text-sm text-muted-foreground ${className}`,
279
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ol", { className: "mx-auto flex max-w-7xl items-center gap-2 overflow-hidden", children: displayItems.map((item, index) => {
280
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ol", { className: `mx-auto flex items-center gap-2 overflow-hidden ${containerClassName}`, children: displayItems.map((item, index) => {
280
281
  const isLast = index === displayItems.length - 1;
281
282
  const key = `${item.name}-${index}`;
282
283
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("li", { className: "flex min-w-0 items-center gap-2", children: [
@@ -650,6 +651,7 @@ function emitArticleEvent(handler, event) {
650
651
  // src/ArticleCard.tsx
651
652
  var import_jsx_runtime8 = require("react/jsx-runtime");
652
653
  function ArticleCard({ article, config }) {
654
+ var _a;
653
655
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "rounded-lg border border-border bg-card text-card-foreground shadow-sm hover:shadow-lg transition-shadow duration-300", children: [
654
656
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "p-0", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
655
657
  import_image4.default,
@@ -663,14 +665,14 @@ function ArticleCard({ article, config }) {
663
665
  ) }),
664
666
  /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "p-6", children: [
665
667
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "flex gap-2 mb-3", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "bg-primary/10 text-primary text-xs font-medium px-2 py-1 rounded", children: article.category }) }),
666
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h3", { className: "text-lg font-semibold leading-none tracking-tight mb-2", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
667
- import_link6.default,
668
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
669
+ "h3",
668
670
  {
669
- href: `/articles/${article.slug}`,
670
- className: "hover:text-indigo-600 transition-colors",
671
- children: article.title
671
+ className: "text-lg font-semibold leading-none tracking-tight mb-2",
672
+ style: { fontFamily: (_a = config == null ? void 0 : config.theme) == null ? void 0 : _a.headerFontFamily },
673
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_link6.default, { href: `/articles/${article.slug}`, className: "hover:text-primary transition-colors", children: article.title })
672
674
  }
673
- ) }),
675
+ ),
674
676
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "text-muted-foreground mb-4 line-clamp-3", children: article.excerpt }),
675
677
  article.authorSlug && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
676
678
  import_link6.default,
@@ -993,6 +995,7 @@ function buildThemeVars(theme) {
993
995
  if (!theme) return {};
994
996
  const vars = {};
995
997
  if (theme.fontFamily) vars["--articles-font-family"] = theme.fontFamily;
998
+ if (theme.headerFontFamily) vars["--articles-header-font-family"] = theme.headerFontFamily;
996
999
  if (theme.headerColor) vars["--articles-header-color"] = theme.headerColor;
997
1000
  if (theme.textColor) vars["--articles-text-color"] = theme.textColor;
998
1001
  if (theme.backgroundColor) vars["--articles-bg-color"] = theme.backgroundColor;
@@ -1153,13 +1156,24 @@ var import_jsx_runtime13 = require("react/jsx-runtime");
1153
1156
  function toSlug(cat) {
1154
1157
  return cat.toLowerCase().replaceAll(/\s+/g, "-").replaceAll(/[^a-z0-9-]/g, "");
1155
1158
  }
1159
+ function resolveAvatarUrl(author, config) {
1160
+ if (!author.avatar) return null;
1161
+ if (author.avatar.startsWith("http://") || author.avatar.startsWith("https://")) {
1162
+ return author.avatar;
1163
+ }
1164
+ if (!(config == null ? void 0 : config.siteUrl)) return null;
1165
+ const siteUrl = config.siteUrl.replace(/\/$/, "");
1166
+ return `${siteUrl}/articles/authors/${author.slug}/${author.avatar.replace(/^\/+/, "")}`;
1167
+ }
1156
1168
  function ArticleDetailHero({
1157
1169
  article,
1158
1170
  categoryBasePath = "/articles/category",
1159
1171
  showDate = false,
1160
1172
  showAuthor = true,
1161
- authors = []
1173
+ authors = [],
1174
+ config
1162
1175
  }) {
1176
+ var _a;
1163
1177
  const showConfiguredAuthors = showAuthor && authors.length > 0;
1164
1178
  const showLegacyAuthor = showAuthor && authors.length === 0 && article.author.trim().length > 0;
1165
1179
  return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
@@ -1240,7 +1254,14 @@ function ArticleDetailHero({
1240
1254
  )
1241
1255
  }
1242
1256
  ),
1243
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("h1", { className: "text-4xl md:text-5xl font-bold mb-4", children: article.title }),
1257
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1258
+ "h1",
1259
+ {
1260
+ className: "text-4xl md:text-5xl font-bold mb-4",
1261
+ style: { fontFamily: (_a = config == null ? void 0 : config.theme) == null ? void 0 : _a.headerFontFamily },
1262
+ children: article.title
1263
+ }
1264
+ ),
1244
1265
  /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1245
1266
  "div",
1246
1267
  {
@@ -1261,24 +1282,36 @@ function ArticleDetailHero({
1261
1282
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react7.Clock, { className: "h-4 w-4" }),
1262
1283
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { children: article.readTime })
1263
1284
  ] }),
1264
- showConfiguredAuthors && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem" }, children: [
1265
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react7.User, { className: "h-4 w-4" }),
1266
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { children: [
1267
- "By",
1268
- " ",
1269
- authors.map((author, index) => /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_react4.Fragment, { children: [
1270
- index > 0 && ", ",
1271
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1272
- import_link8.default,
1273
- {
1274
- href: `/articles/authors/${author.slug}`,
1275
- className: "font-medium text-white underline-offset-4 hover:underline",
1276
- children: author.name
1277
- }
1278
- )
1279
- ] }, author.slug))
1280
- ] })
1281
- ] }),
1285
+ showConfiguredAuthors && (() => {
1286
+ const singleAvatarUrl = authors.length === 1 ? resolveAvatarUrl(authors[0], config) : null;
1287
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem" }, children: [
1288
+ singleAvatarUrl ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1289
+ import_image5.default,
1290
+ {
1291
+ src: singleAvatarUrl,
1292
+ alt: "",
1293
+ width: 20,
1294
+ height: 20,
1295
+ className: "h-5 w-5 rounded-full object-cover"
1296
+ }
1297
+ ) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react7.User, { className: "h-4 w-4" }),
1298
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { children: [
1299
+ "By",
1300
+ " ",
1301
+ authors.map((author, index) => /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_react4.Fragment, { children: [
1302
+ index > 0 && ", ",
1303
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1304
+ import_link8.default,
1305
+ {
1306
+ href: `/articles/authors/${author.slug}`,
1307
+ className: "font-medium text-white underline-offset-4 hover:underline",
1308
+ children: author.name
1309
+ }
1310
+ )
1311
+ ] }, author.slug))
1312
+ ] })
1313
+ ] });
1314
+ })(),
1282
1315
  showLegacyAuthor && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem" }, children: [
1283
1316
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react7.User, { className: "h-4 w-4" }),
1284
1317
  /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { children: [
@@ -2006,9 +2039,17 @@ function RelatedArticlesSection({
2006
2039
  fromSlug,
2007
2040
  source = "category"
2008
2041
  }) {
2042
+ var _a;
2009
2043
  if (articles.length === 0) return null;
2010
2044
  return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("section", { className: "mt-12 pt-8 border-t border-border", "aria-label": "Related articles", children: [
2011
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("h2", { className: "text-xl font-semibold text-foreground mb-6", children: heading != null ? heading : `More in ${category}` }),
2045
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2046
+ "h2",
2047
+ {
2048
+ className: "text-xl font-semibold text-foreground mb-6",
2049
+ style: { fontFamily: (_a = config == null ? void 0 : config.theme) == null ? void 0 : _a.headerFontFamily },
2050
+ children: heading != null ? heading : `More in ${category}`
2051
+ }
2052
+ ),
2012
2053
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "grid gap-6 sm:grid-cols-2 lg:grid-cols-3", children: articles.map((article) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2013
2054
  "div",
2014
2055
  {