@bygghammar/cms-platform 0.76.0 → 0.78.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,17 @@
|
|
|
1
|
+
## 0.78.0
|
|
2
|
+
|
|
3
|
+
- Countdown block: the unit tiles now always stay on a single row on mobile.
|
|
4
|
+
Tiles shrink to fit (with smaller mobile padding, digits, labels and gaps)
|
|
5
|
+
instead of wrapping; desktop sizing and all colour/layout settings unchanged.
|
|
6
|
+
|
|
7
|
+
## 0.77.0
|
|
8
|
+
|
|
9
|
+
- Global Sections list: preview thumbnails now use the section's first block type
|
|
10
|
+
and show a text snippet from the saved content (including Rich text `html`),
|
|
11
|
+
instead of a blank generic placeholder. Multi-block sections show a count badge.
|
|
12
|
+
- Shared thumbnail: text detection reads `html`, `body`, `lead`, `subtitle` and
|
|
13
|
+
`heading`, with locale fallback and HTML stripped to plain text.
|
|
14
|
+
|
|
1
15
|
## 0.76.0
|
|
2
16
|
|
|
3
17
|
- 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.
|
|
3
|
+
"version": "0.78.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
|
+
" ": " ",
|
|
69
|
+
"&": "&",
|
|
70
|
+
"<": "<",
|
|
71
|
+
">": ">",
|
|
72
|
+
""": '"',
|
|
73
|
+
"'": "'",
|
|
74
|
+
"'": "'",
|
|
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(/ |&|<|>|"|'|'/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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
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">
|
|
@@ -10,12 +10,28 @@ import { useCountdownReplacement } from "../countdown-replacement";
|
|
|
10
10
|
import type { CountdownProps } from "../types";
|
|
11
11
|
|
|
12
12
|
const TILE_SIZE = {
|
|
13
|
-
sm: {
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
sm: {
|
|
14
|
+
box: "min-w-0 flex-1 px-2 py-2 sm:min-w-14 sm:flex-none sm:px-3 sm:py-2.5",
|
|
15
|
+
digit: "text-xl sm:text-2xl md:text-3xl",
|
|
16
|
+
unit: "text-[0.5rem] sm:text-[0.55rem]",
|
|
17
|
+
},
|
|
18
|
+
md: {
|
|
19
|
+
box: "min-w-0 flex-1 px-2 py-2.5 sm:min-w-20 sm:flex-none sm:px-4 sm:py-3.5",
|
|
20
|
+
digit: "text-2xl sm:text-3xl md:text-5xl",
|
|
21
|
+
unit: "text-[0.5rem] sm:text-[0.6rem]",
|
|
22
|
+
},
|
|
23
|
+
lg: {
|
|
24
|
+
box: "min-w-0 flex-1 px-2 py-3 sm:min-w-28 sm:flex-none sm:px-6 sm:py-5",
|
|
25
|
+
digit: "text-2xl sm:text-4xl md:text-6xl",
|
|
26
|
+
unit: "text-[0.5rem] sm:text-[0.68rem]",
|
|
27
|
+
},
|
|
16
28
|
} as const;
|
|
17
29
|
|
|
18
|
-
const GAP = {
|
|
30
|
+
const GAP = {
|
|
31
|
+
sm: "gap-1.5 sm:gap-2",
|
|
32
|
+
md: "gap-1.5 sm:gap-3 md:gap-4",
|
|
33
|
+
lg: "gap-2 sm:gap-5 md:gap-7",
|
|
34
|
+
} as const;
|
|
19
35
|
const PADDING = {
|
|
20
36
|
compact: "py-8 md:py-10",
|
|
21
37
|
normal: "py-14 md:py-20",
|
|
@@ -149,11 +165,12 @@ export function CountdownBlock({ props: p }: { props: CountdownProps }) {
|
|
|
149
165
|
<>
|
|
150
166
|
{heading(p.title, p.body)}
|
|
151
167
|
<div
|
|
152
|
-
className={`mt-8 flex flex-
|
|
168
|
+
className={`mt-8 flex w-full flex-nowrap items-stretch sm:w-auto sm:flex-wrap ${GAP[p.gap ?? "md"]} ${align.row}`}
|
|
153
169
|
aria-live="off"
|
|
154
170
|
>
|
|
155
171
|
{units.map((u, i) => (
|
|
156
|
-
<div key={i} className="flex items-stretch">
|
|
172
|
+
<div key={i} className="flex min-w-0 flex-1 items-stretch sm:flex-none">
|
|
173
|
+
|
|
157
174
|
<div
|
|
158
175
|
className={`flex flex-col items-center justify-center bg-[var(--cd-tile-bg)] ${size.box} ${border} ${shadow}`}
|
|
159
176
|
style={{ borderRadius: "var(--cd-radius)" }}
|