@bygghammar/cms-platform 0.76.0 → 0.77.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
@@ -1,3 +1,11 @@
1
+ ## 0.77.0
2
+
3
+ - Global Sections list: preview thumbnails now use the section's first block type
4
+ and show a text snippet from the saved content (including Rich text `html`),
5
+ instead of a blank generic placeholder. Multi-block sections show a count badge.
6
+ - Shared thumbnail: text detection reads `html`, `body`, `lead`, `subtitle` and
7
+ `heading`, with locale fallback and HTML stripped to plain text.
8
+
1
9
  ## 0.76.0
2
10
 
3
11
  - Rich text editor colours: swatches are now derived from the consuming site's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bygghammar/cms-platform",
3
- "version": "0.76.0",
3
+ "version": "0.77.0",
4
4
  "description": "Shared CMS platform: blocks, block types, i18n core, UI primitives and design-token contract used by every Bygghammar/Voyara site.",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -27,6 +27,24 @@ export type GlobalSectionsListDeps = {
27
27
  previewSrc: (key: string) => string;
28
28
  };
29
29
 
30
+ /** Saved sections store an array of block instances; shapes may vary by age. */
31
+ function blockList(blocks: unknown): Record<string, unknown>[] {
32
+ if (!Array.isArray(blocks)) return [];
33
+ return blocks.filter((b): b is Record<string, unknown> => !!b && typeof b === "object");
34
+ }
35
+
36
+ function blockCount(blocks: unknown): number {
37
+ return blockList(blocks).length;
38
+ }
39
+
40
+ /** Block type of the first block, so the thumbnail can draw its wireframe. */
41
+ function firstBlockType(blocks: unknown): string | undefined {
42
+ const first = blockList(blocks)[0];
43
+ const type = first?.["type"];
44
+ return typeof type === "string" && type ? type : undefined;
45
+ }
46
+
47
+
30
48
  /**
31
49
  * Library screen listing global sections — reusable groups of blocks that update
32
50
  * everywhere at once. All data access and navigation is host-supplied.
@@ -98,7 +116,12 @@ export function GlobalSectionsListView({ deps }: { deps: GlobalSectionsListDeps
98
116
  onClick={() => setPreview({ key: g.key, name: g.name })}
99
117
  className="group relative block"
100
118
  >
101
- <DataThumb data={g.blocks} />
119
+ <DataThumb data={g.blocks} blockType={firstBlockType(g.blocks)} />
120
+ {blockCount(g.blocks) > 1 && (
121
+ <span className="absolute bottom-1 right-1 rounded bg-ink/70 px-1 text-[0.55rem] leading-4 text-white">
122
+ {blockCount(g.blocks)}
123
+ </span>
124
+ )}
102
125
  <span className="absolute inset-0 hidden items-center justify-center bg-ink/50 text-white group-hover:flex">
103
126
  <Eye className="size-4" />
104
127
  </span>
@@ -64,6 +64,42 @@ function findImage(node: unknown, depth = 0): string | null {
64
64
  return null;
65
65
  }
66
66
 
67
+ const ENTITIES: Record<string, string> = {
68
+ "&nbsp;": " ",
69
+ "&amp;": "&",
70
+ "&lt;": "<",
71
+ "&gt;": ">",
72
+ "&quot;": '"',
73
+ "&#39;": "'",
74
+ "&apos;": "'",
75
+ };
76
+
77
+ /** Plain-text snippet from sanitized editor HTML (no DOM needed, SSR-safe). */
78
+ function htmlToText(html: string): string {
79
+ const withBreaks = html.replace(/<(br|\/p|\/h[1-6]|\/li|\/div)[^>]*>/gi, " ");
80
+ const stripped = withBreaks.replace(/<[^>]*>/g, "");
81
+ const decoded = stripped
82
+ .replace(/&nbsp;|&amp;|&lt;|&gt;|&quot;|&#39;|&apos;/gi, (m) => ENTITIES[m.toLowerCase()] ?? m)
83
+ .replace(/&#(\d+);/g, (_, code: string) => String.fromCodePoint(Number(code)));
84
+ return decoded.replace(/\s+/g, " ").trim();
85
+ }
86
+
87
+ /** Text keys, in the order a block would show them. */
88
+ const TEXT_KEYS = ["title", "heading", "name", "label", "eyebrow", "html", "body", "lead", "subtitle"];
89
+
90
+ /** Picks a string out of a plain string or a localized map (active locale first). */
91
+ function localizedString(value: unknown): string | null {
92
+ if (typeof value === "string") return value.trim() ? value : null;
93
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
94
+ const map = value as Record<string, unknown>;
95
+ const ordered = ["sv", "en", ...Object.keys(map)];
96
+ for (const locale of ordered) {
97
+ const candidate = map[locale];
98
+ if (typeof candidate === "string" && candidate.trim()) return candidate;
99
+ }
100
+ return null;
101
+ }
102
+
67
103
  function findText(node: unknown, depth = 0): string | null {
68
104
  if (node == null || depth > 4) return null;
69
105
  if (Array.isArray(node)) {
@@ -75,13 +111,16 @@ function findText(node: unknown, depth = 0): string | null {
75
111
  }
76
112
  if (typeof node !== "object") return null;
77
113
  const obj = node as Record<string, unknown>;
78
- for (const key of ["title", "name", "label", "eyebrow"]) {
79
- const value = obj[key];
80
- if (typeof value === "string" && value.trim()) return value;
81
- if (value && typeof value === "object") {
82
- const sv = (value as Record<string, unknown>).sv;
83
- if (typeof sv === "string" && sv.trim()) return sv;
84
- }
114
+ const nested = obj["props"];
115
+ if (nested && typeof nested === "object") {
116
+ const found = findText(nested, depth + 1);
117
+ if (found) return found;
118
+ }
119
+ for (const key of TEXT_KEYS) {
120
+ const raw = localizedString(obj[key]);
121
+ if (!raw) continue;
122
+ const text = key === "html" || /<[a-z][^>]*>/i.test(raw) ? htmlToText(raw) : raw.trim();
123
+ if (text) return text.length > 120 ? `${text.slice(0, 120)}…` : text;
85
124
  }
86
125
  return null;
87
126
  }
@@ -372,7 +411,7 @@ export function DataThumb({
372
411
 
373
412
  return (
374
413
  <div className="relative shrink-0 border border-ink/10 bg-white overflow-hidden" style={style}>
375
- {text && !blockType ? (
414
+ {text ? (
376
415
  <div className="flex size-full flex-col justify-center gap-1.5 p-2">
377
416
  <div className="h-1 w-6 bg-sea/60" />
378
417
  <div className="text-[0.5rem] leading-tight text-ink/70 line-clamp-2 font-serif">