@bygghammar/cms-platform 0.6.0 → 0.8.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/package.json +10 -2
- package/src/backoffice/SaveGlobalDialog.tsx +221 -0
- package/src/backoffice/forms/FieldEditor.tsx +611 -0
- package/src/backoffice/forms/FormPreview.tsx +108 -0
- package/src/backoffice/forms/TestDeliveryPanel.tsx +201 -0
- package/src/backoffice/forms/VersionHistory.tsx +142 -0
- package/src/backoffice/media/MediaCleanup.tsx +186 -0
- package/src/backoffice/media/MediaReplaceDialog.tsx +158 -0
- package/src/backoffice/media/ReuploadOverwrite.tsx +226 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bygghammar/cms-platform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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,
|
|
@@ -76,7 +76,15 @@
|
|
|
76
76
|
"./backoffice/ProgressBar": "./src/backoffice/ProgressBar.tsx",
|
|
77
77
|
"./backoffice/PreviewLightbox": "./src/backoffice/PreviewLightbox.tsx",
|
|
78
78
|
"./backoffice/RichTextEditor": "./src/backoffice/RichTextEditor.tsx",
|
|
79
|
-
"./backoffice/media/ConfirmDialog": "./src/backoffice/media/ConfirmDialog.tsx"
|
|
79
|
+
"./backoffice/media/ConfirmDialog": "./src/backoffice/media/ConfirmDialog.tsx",
|
|
80
|
+
"./backoffice/media/MediaCleanup": "./src/backoffice/media/MediaCleanup.tsx",
|
|
81
|
+
"./backoffice/media/MediaReplaceDialog": "./src/backoffice/media/MediaReplaceDialog.tsx",
|
|
82
|
+
"./backoffice/media/ReuploadOverwrite": "./src/backoffice/media/ReuploadOverwrite.tsx",
|
|
83
|
+
"./backoffice/forms/FieldEditor": "./src/backoffice/forms/FieldEditor.tsx",
|
|
84
|
+
"./backoffice/forms/FormPreview": "./src/backoffice/forms/FormPreview.tsx",
|
|
85
|
+
"./backoffice/forms/VersionHistory": "./src/backoffice/forms/VersionHistory.tsx",
|
|
86
|
+
"./backoffice/forms/TestDeliveryPanel": "./src/backoffice/forms/TestDeliveryPanel.tsx",
|
|
87
|
+
"./backoffice/SaveGlobalDialog": "./src/backoffice/SaveGlobalDialog.tsx"
|
|
80
88
|
},
|
|
81
89
|
"dependencies": {
|
|
82
90
|
"fflate": "^0.8.3"
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { X, Globe, Layers, BookmarkPlus } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
export type SaveGlobalMode = "global" | "template";
|
|
5
|
+
export type SaveGlobalResult = {
|
|
6
|
+
name: string;
|
|
7
|
+
key: string;
|
|
8
|
+
kind: "block" | "section";
|
|
9
|
+
description: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function slugifyKey(value: string) {
|
|
13
|
+
return (
|
|
14
|
+
value
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.normalize("NFD")
|
|
17
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
18
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
19
|
+
.replace(/^-+|-+$/g, "")
|
|
20
|
+
.slice(0, 48) || "section"
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function SaveGlobalDialog({
|
|
25
|
+
open,
|
|
26
|
+
mode,
|
|
27
|
+
defaultName,
|
|
28
|
+
error,
|
|
29
|
+
busy = false,
|
|
30
|
+
onCancel,
|
|
31
|
+
onSubmit,
|
|
32
|
+
}: {
|
|
33
|
+
open: boolean;
|
|
34
|
+
mode: SaveGlobalMode;
|
|
35
|
+
defaultName: string;
|
|
36
|
+
error?: string | null;
|
|
37
|
+
busy?: boolean;
|
|
38
|
+
onCancel: () => void;
|
|
39
|
+
onSubmit: (result: SaveGlobalResult) => void;
|
|
40
|
+
}) {
|
|
41
|
+
const [name, setName] = useState(defaultName);
|
|
42
|
+
const [key, setKey] = useState(slugifyKey(defaultName));
|
|
43
|
+
const [keyTouched, setKeyTouched] = useState(false);
|
|
44
|
+
const [kind, setKind] = useState<"block" | "section">("section");
|
|
45
|
+
const [description, setDescription] = useState("");
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (open) {
|
|
49
|
+
setName(defaultName);
|
|
50
|
+
setKey(slugifyKey(defaultName));
|
|
51
|
+
setKeyTouched(false);
|
|
52
|
+
setKind("section");
|
|
53
|
+
setDescription("");
|
|
54
|
+
}
|
|
55
|
+
}, [open, defaultName]);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!open) return;
|
|
59
|
+
const onKey = (e: KeyboardEvent) => {
|
|
60
|
+
if (e.key === "Escape") onCancel();
|
|
61
|
+
};
|
|
62
|
+
document.addEventListener("keydown", onKey);
|
|
63
|
+
const prev = document.body.style.overflow;
|
|
64
|
+
document.body.style.overflow = "hidden";
|
|
65
|
+
return () => {
|
|
66
|
+
document.removeEventListener("keydown", onKey);
|
|
67
|
+
document.body.style.overflow = prev;
|
|
68
|
+
};
|
|
69
|
+
}, [open, onCancel]);
|
|
70
|
+
|
|
71
|
+
if (!open) return null;
|
|
72
|
+
|
|
73
|
+
const isGlobal = mode === "global";
|
|
74
|
+
const keyInvalid = isGlobal && !/^[a-z0-9_-]+$/.test(key);
|
|
75
|
+
const canSave = name.trim().length > 0 && (!isGlobal || (!keyInvalid && key.length > 0));
|
|
76
|
+
|
|
77
|
+
const submit = () => {
|
|
78
|
+
if (!canSave || busy) return;
|
|
79
|
+
onSubmit({ name: name.trim(), key, kind, description: description.trim() });
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
|
|
84
|
+
<button aria-label="Close" onClick={onCancel} className="absolute inset-0 bg-ink/50" />
|
|
85
|
+
<div className="relative w-full max-w-lg bg-white border border-ink/10 shadow-xl">
|
|
86
|
+
<div className="flex items-start justify-between border-b border-ink/10 px-5 py-4">
|
|
87
|
+
<div>
|
|
88
|
+
<div className="text-[0.65rem] uppercase tracking-[0.22em] text-ink/50">
|
|
89
|
+
{isGlobal ? "Reusable" : "Blocks library"}
|
|
90
|
+
</div>
|
|
91
|
+
<h2 className="text-lg font-medium text-ink flex items-center gap-2">
|
|
92
|
+
{isGlobal ? <Globe className="size-4" /> : <BookmarkPlus className="size-4" />}
|
|
93
|
+
{isGlobal ? "Save as reusable" : "Save one-off copy"}
|
|
94
|
+
</h2>
|
|
95
|
+
</div>
|
|
96
|
+
<button onClick={onCancel} className="text-ink/40 hover:text-ink" title="Close">
|
|
97
|
+
<X className="size-4" />
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div className="px-5 py-4 space-y-4">
|
|
102
|
+
<p className="text-xs text-ink/60 leading-relaxed">
|
|
103
|
+
{isGlobal
|
|
104
|
+
? "This page will switch to a live reference. Edit it once and every page or section using it updates."
|
|
105
|
+
: "Stores an independent duplicate in the Blocks library. Later edits only affect this page."}
|
|
106
|
+
</p>
|
|
107
|
+
|
|
108
|
+
<label className="block">
|
|
109
|
+
<span className="text-[0.65rem] uppercase tracking-[0.18em] text-ink/55">Name</span>
|
|
110
|
+
<input
|
|
111
|
+
autoFocus
|
|
112
|
+
value={name}
|
|
113
|
+
onChange={(e) => {
|
|
114
|
+
setName(e.target.value);
|
|
115
|
+
if (!keyTouched) setKey(slugifyKey(e.target.value));
|
|
116
|
+
}}
|
|
117
|
+
onKeyDown={(e) => e.key === "Enter" && submit()}
|
|
118
|
+
className="mt-1 w-full border border-ink/15 px-3 py-2 text-sm focus:border-sea outline-none"
|
|
119
|
+
/>
|
|
120
|
+
</label>
|
|
121
|
+
|
|
122
|
+
{isGlobal ? (
|
|
123
|
+
<>
|
|
124
|
+
<label className="block">
|
|
125
|
+
<span className="text-[0.65rem] uppercase tracking-[0.18em] text-ink/55">
|
|
126
|
+
Key (url-safe, unique)
|
|
127
|
+
</span>
|
|
128
|
+
<input
|
|
129
|
+
value={key}
|
|
130
|
+
onChange={(e) => {
|
|
131
|
+
setKeyTouched(true);
|
|
132
|
+
setKey(e.target.value.trim());
|
|
133
|
+
}}
|
|
134
|
+
onKeyDown={(e) => e.key === "Enter" && submit()}
|
|
135
|
+
className="mt-1 w-full border border-ink/15 px-3 py-2 text-sm font-mono focus:border-sea outline-none"
|
|
136
|
+
/>
|
|
137
|
+
{keyInvalid && (
|
|
138
|
+
<span className="mt-1 block text-[0.7rem] text-red-600">
|
|
139
|
+
Use lowercase letters, numbers, hyphen or underscore only.
|
|
140
|
+
</span>
|
|
141
|
+
)}
|
|
142
|
+
</label>
|
|
143
|
+
|
|
144
|
+
<div>
|
|
145
|
+
<span className="text-[0.65rem] uppercase tracking-[0.18em] text-ink/55">
|
|
146
|
+
Save to
|
|
147
|
+
</span>
|
|
148
|
+
<div className="mt-1 grid grid-cols-2 gap-2">
|
|
149
|
+
{[
|
|
150
|
+
{
|
|
151
|
+
v: "section" as const,
|
|
152
|
+
icon: Layers,
|
|
153
|
+
title: "Global sections",
|
|
154
|
+
hint: "Shows in Global Sections",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
v: "block" as const,
|
|
158
|
+
icon: Globe,
|
|
159
|
+
title: "Global block",
|
|
160
|
+
hint: "Shows in Block library",
|
|
161
|
+
},
|
|
162
|
+
].map((o) => {
|
|
163
|
+
const Icon = o.icon;
|
|
164
|
+
const active = kind === o.v;
|
|
165
|
+
return (
|
|
166
|
+
<button
|
|
167
|
+
key={o.v}
|
|
168
|
+
type="button"
|
|
169
|
+
onClick={() => setKind(o.v)}
|
|
170
|
+
className={`text-left border px-3 py-2 ${active ? "border-sea bg-ice/40" : "border-ink/15 hover:border-ink/30"}`}
|
|
171
|
+
>
|
|
172
|
+
<span className="flex items-center gap-1.5 text-sm text-ink">
|
|
173
|
+
<Icon className="size-3.5" /> {o.title}
|
|
174
|
+
</span>
|
|
175
|
+
<span className="block text-[0.7rem] text-ink/55 mt-0.5">{o.hint}</span>
|
|
176
|
+
</button>
|
|
177
|
+
);
|
|
178
|
+
})}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
</>
|
|
182
|
+
) : (
|
|
183
|
+
<label className="block">
|
|
184
|
+
<span className="text-[0.65rem] uppercase tracking-[0.18em] text-ink/55">
|
|
185
|
+
Description (optional)
|
|
186
|
+
</span>
|
|
187
|
+
<input
|
|
188
|
+
value={description}
|
|
189
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
190
|
+
onKeyDown={(e) => e.key === "Enter" && submit()}
|
|
191
|
+
className="mt-1 w-full border border-ink/15 px-3 py-2 text-sm focus:border-sea outline-none"
|
|
192
|
+
/>
|
|
193
|
+
</label>
|
|
194
|
+
)}
|
|
195
|
+
|
|
196
|
+
{error && (
|
|
197
|
+
<div className="border border-red-300 bg-red-50 px-3 py-2 text-xs text-red-700">
|
|
198
|
+
{error}
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<div className="flex justify-end gap-2 border-t border-ink/10 px-5 py-3">
|
|
204
|
+
<button
|
|
205
|
+
onClick={onCancel}
|
|
206
|
+
className="border border-ink/15 px-3 py-1.5 text-sm hover:border-ink/40"
|
|
207
|
+
>
|
|
208
|
+
Cancel
|
|
209
|
+
</button>
|
|
210
|
+
<button
|
|
211
|
+
onClick={submit}
|
|
212
|
+
disabled={!canSave || busy}
|
|
213
|
+
className="bg-ink text-white px-4 py-1.5 text-sm disabled:opacity-40"
|
|
214
|
+
>
|
|
215
|
+
{busy ? "Saving…" : "Save"}
|
|
216
|
+
</button>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
}
|