@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.30.1",
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 &amp; 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
+ }