@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bygghammar/cms-platform",
3
- "version": "0.6.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
+ }