@bygghammar/cms-platform 0.30.1 → 0.31.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,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.31.0
|
|
4
|
+
|
|
5
|
+
- Added `backoffice/SharedContentListView` — the shared-content library screen
|
|
6
|
+
with host-supplied section registry, override chips and preview thumbnails.
|
|
7
|
+
- Added `backoffice/seo/SeoListView` — the SEO index (`SeoListView`) and page
|
|
8
|
+
shell (`SeoDetailView`) around the host's page SEO panel.
|
|
9
|
+
|
|
3
10
|
## 0.30.0
|
|
4
11
|
|
|
5
12
|
- Added `backoffice/PagesListView` — the page-builder index with public URLs per
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bygghammar/cms-platform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.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": false,
|
|
@@ -135,7 +135,9 @@
|
|
|
135
135
|
"./backoffice/GlobalSectionBuilderView": "./src/backoffice/GlobalSectionBuilderView.tsx",
|
|
136
136
|
"./backoffice/ReviewsView": "./src/backoffice/ReviewsView.tsx",
|
|
137
137
|
"./backoffice/PagesListView": "./src/backoffice/PagesListView.tsx",
|
|
138
|
-
"./backoffice/BlocksLibraryView": "./src/backoffice/BlocksLibraryView.tsx"
|
|
138
|
+
"./backoffice/BlocksLibraryView": "./src/backoffice/BlocksLibraryView.tsx",
|
|
139
|
+
"./backoffice/SharedContentListView": "./src/backoffice/SharedContentListView.tsx",
|
|
140
|
+
"./backoffice/seo/SeoListView": "./src/backoffice/seo/SeoListView.tsx"
|
|
139
141
|
},
|
|
140
142
|
"dependencies": {
|
|
141
143
|
"fflate": "^0.8.3"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-agnostic "Shared content" library screen.
|
|
3
|
+
*
|
|
4
|
+
* Lists the site-wide content records (one per section key). The host supplies
|
|
5
|
+
* the section registry, override state, thumbnails and navigation, so every
|
|
6
|
+
* site gets the same screen without the package knowing its content model.
|
|
7
|
+
*/
|
|
8
|
+
import { useState, type ReactNode } from "react";
|
|
9
|
+
import { Check, ChevronRight, Eye } from "lucide-react";
|
|
10
|
+
|
|
11
|
+
import { PreviewLightbox } from "./PreviewLightbox";
|
|
12
|
+
|
|
13
|
+
export type SharedContentSection = {
|
|
14
|
+
key: string;
|
|
15
|
+
label: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type SharedContentListDeps = {
|
|
20
|
+
sections: readonly SharedContentSection[];
|
|
21
|
+
/** True when this key has a database override (shows the "edited" chip). */
|
|
22
|
+
hasOverride: (key: string) => boolean;
|
|
23
|
+
/** Navigate to the editor for one section key. */
|
|
24
|
+
openEditor: (key: string) => void;
|
|
25
|
+
/** Preview URL for one section key, e.g. `/preview/section/${key}`. */
|
|
26
|
+
previewSrc: (key: string) => string;
|
|
27
|
+
/** Thumbnail for one section key; host-rendered so it can read its own data. */
|
|
28
|
+
renderThumb: (key: string) => ReactNode;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function SharedContentListView({ deps }: { deps: SharedContentListDeps }): ReactNode {
|
|
32
|
+
const { sections, hasOverride, openEditor, previewSrc, renderThumb } = deps;
|
|
33
|
+
const [preview, setPreview] = useState<{ key: string; label: string } | null>(null);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="p-8 md:p-12 w-full">
|
|
37
|
+
<div className="text-[0.7rem] uppercase tracking-[0.22em] text-ink/55">Shared content</div>
|
|
38
|
+
<h1 className="mt-2 font-serif text-3xl md:text-4xl text-ink">Shared content</h1>
|
|
39
|
+
<p className="mt-4 text-ink/65 max-w-2xl">
|
|
40
|
+
Site-wide content records — one per key. Any page using the matching block reads this
|
|
41
|
+
content, so editing here updates every page at once. For global blocks (reusable groups of
|
|
42
|
+
blocks) with their own layout, use Global blocks instead. Thumbnails refresh when you save.
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
<ul className="mt-10 divide-y divide-ink/10 border-y border-ink/10">
|
|
46
|
+
{sections.map((section) => (
|
|
47
|
+
<li key={section.key}>
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
onClick={() => openEditor(section.key)}
|
|
51
|
+
className="flex w-full items-center gap-4 py-5 hover:bg-white transition-colors px-4 -mx-4 text-left"
|
|
52
|
+
>
|
|
53
|
+
<span
|
|
54
|
+
role="button"
|
|
55
|
+
tabIndex={0}
|
|
56
|
+
title="Preview section"
|
|
57
|
+
onClick={(e) => {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
e.stopPropagation();
|
|
60
|
+
setPreview({ key: section.key, label: section.label });
|
|
61
|
+
}}
|
|
62
|
+
onKeyDown={(e) => {
|
|
63
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
e.stopPropagation();
|
|
66
|
+
setPreview({ key: section.key, label: section.label });
|
|
67
|
+
}}
|
|
68
|
+
className="group relative shrink-0"
|
|
69
|
+
>
|
|
70
|
+
{renderThumb(section.key)}
|
|
71
|
+
<span className="absolute inset-0 hidden items-center justify-center bg-ink/50 text-white group-hover:flex">
|
|
72
|
+
<Eye className="size-4" />
|
|
73
|
+
</span>
|
|
74
|
+
</span>
|
|
75
|
+
<span className="flex-1 min-w-0">
|
|
76
|
+
<span className="flex items-center gap-3">
|
|
77
|
+
<span className="font-serif text-lg text-ink">{section.label}</span>
|
|
78
|
+
{hasOverride(section.key) && (
|
|
79
|
+
<span className="inline-flex items-center gap-1 text-[0.65rem] uppercase tracking-[0.18em] text-sea">
|
|
80
|
+
<Check className="size-3" />
|
|
81
|
+
edited
|
|
82
|
+
</span>
|
|
83
|
+
)}
|
|
84
|
+
</span>
|
|
85
|
+
{section.description && (
|
|
86
|
+
<span className="mt-1 block text-sm text-ink/60">{section.description}</span>
|
|
87
|
+
)}
|
|
88
|
+
</span>
|
|
89
|
+
<ChevronRight className="size-5 text-ink/40" />
|
|
90
|
+
</button>
|
|
91
|
+
</li>
|
|
92
|
+
))}
|
|
93
|
+
</ul>
|
|
94
|
+
|
|
95
|
+
<PreviewLightbox
|
|
96
|
+
open={preview != null}
|
|
97
|
+
onClose={() => setPreview(null)}
|
|
98
|
+
src={preview ? previewSrc(preview.key) : ""}
|
|
99
|
+
title={preview?.label ?? ""}
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-agnostic SEO index and page shell.
|
|
3
|
+
*
|
|
4
|
+
* The host resolves titles, slugs and override state from its own page registry
|
|
5
|
+
* plus CMS overrides; these components own only presentation and navigation.
|
|
6
|
+
*/
|
|
7
|
+
import type { ReactNode } from "react";
|
|
8
|
+
import { Check, ChevronLeft, ChevronRight } from "lucide-react";
|
|
9
|
+
|
|
10
|
+
export type SeoListRow = {
|
|
11
|
+
/** Page key, e.g. `index` or a CMS-only page key. */
|
|
12
|
+
key: string;
|
|
13
|
+
/** Title shown in the default locale. */
|
|
14
|
+
title: string;
|
|
15
|
+
/** Public path in the default locale, e.g. `/` or `/kontakt`. */
|
|
16
|
+
path: string;
|
|
17
|
+
hasOverride: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type SeoListDeps = {
|
|
21
|
+
rows: readonly SeoListRow[];
|
|
22
|
+
/** Navigate to the SEO editor for one page key. */
|
|
23
|
+
openEditor: (key: string) => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function SeoListView({ deps }: { deps: SeoListDeps }): ReactNode {
|
|
27
|
+
const { rows, openEditor } = deps;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="p-8 md:p-12 w-full">
|
|
31
|
+
<div className="text-[0.7rem] uppercase tracking-[0.22em] text-ink/55">SEO</div>
|
|
32
|
+
<h1 className="mt-2 font-serif text-3xl md:text-4xl text-ink">SEO & slugs</h1>
|
|
33
|
+
|
|
34
|
+
<p className="mt-4 text-ink/65 max-w-xl">
|
|
35
|
+
Edit title, description, social previews and URL slug per language. Changes override the
|
|
36
|
+
defaults from the codebase and are used directly by canonical, hreflang and sitemap.
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
<ul className="mt-10 divide-y divide-ink/10 border-y border-ink/10">
|
|
40
|
+
{rows.map((row) => (
|
|
41
|
+
<li key={row.key}>
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
onClick={() => openEditor(row.key)}
|
|
45
|
+
className="flex w-full items-center gap-4 py-5 hover:bg-white transition-colors px-4 -mx-4 text-left"
|
|
46
|
+
>
|
|
47
|
+
<span className="flex-1 min-w-0">
|
|
48
|
+
<span className="flex items-center gap-3">
|
|
49
|
+
<span className="font-serif text-lg text-ink">{row.title}</span>
|
|
50
|
+
{row.hasOverride && (
|
|
51
|
+
<span className="inline-flex items-center gap-1 text-[0.65rem] uppercase tracking-[0.18em] text-sea">
|
|
52
|
+
<Check className="size-3" />
|
|
53
|
+
edited
|
|
54
|
+
</span>
|
|
55
|
+
)}
|
|
56
|
+
</span>
|
|
57
|
+
<span className="mt-1 block text-sm text-ink/60">
|
|
58
|
+
<span className="font-mono">{row.key}</span> · {row.path}
|
|
59
|
+
</span>
|
|
60
|
+
</span>
|
|
61
|
+
<ChevronRight className="size-5 text-ink/40" />
|
|
62
|
+
</button>
|
|
63
|
+
</li>
|
|
64
|
+
))}
|
|
65
|
+
</ul>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type SeoDetailDeps = {
|
|
71
|
+
pageKey: string;
|
|
72
|
+
/** Navigate back to the SEO index. */
|
|
73
|
+
onBack: () => void;
|
|
74
|
+
/** The host's page SEO panel for this page key. */
|
|
75
|
+
children: ReactNode;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export function SeoDetailView({ deps }: { deps: SeoDetailDeps }): ReactNode {
|
|
79
|
+
const { pageKey, onBack, children } = deps;
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="p-8 md:p-12 w-full">
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
onClick={onBack}
|
|
86
|
+
className="inline-flex items-center gap-2 text-xs uppercase tracking-[0.22em] text-ink/60 hover:text-ink"
|
|
87
|
+
>
|
|
88
|
+
<ChevronLeft className="size-4" />
|
|
89
|
+
All SEO pages
|
|
90
|
+
</button>
|
|
91
|
+
|
|
92
|
+
<div className="mt-6 text-[0.7rem] uppercase tracking-[0.22em] text-ink/55">
|
|
93
|
+
Page · {pageKey}
|
|
94
|
+
</div>
|
|
95
|
+
<h1 className="mt-2 font-serif text-3xl md:text-4xl text-ink">Page SEO</h1>
|
|
96
|
+
<p className="mt-3 text-ink/65 max-w-xl">
|
|
97
|
+
Defaults come from the codebase. Changes are saved per language and used by canonical,
|
|
98
|
+
hreflang, Open Graph and Twitter cards.
|
|
99
|
+
</p>
|
|
100
|
+
|
|
101
|
+
<div className="mt-8">{children}</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|