@pramen/cms-editor 0.0.33 → 0.0.35
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/dist/index.html +1 -1
- package/dist/{main.3svjn3hn.js → main.22t99khc.js} +73 -73
- package/package.json +1 -1
- package/src/components.tsx +18 -5
- package/src/fields.tsx +28 -6
- package/src/routes/_layout.tsx +5 -0
- package/src/types.ts +4 -0
package/package.json
CHANGED
package/src/components.tsx
CHANGED
|
@@ -139,14 +139,27 @@ function CreatePage({ api, onClose, onCreated, onError }: { api: Api; onClose: (
|
|
|
139
139
|
<Modal onClose={onClose}>
|
|
140
140
|
<ModalTitle>Create a <Dim>new page</Dim> and define the essentials<Dim>.</Dim></ModalTitle>
|
|
141
141
|
<div className="flex flex-col gap-4">
|
|
142
|
-
<
|
|
142
|
+
<div className="flex w-full flex-col gap-2">
|
|
143
143
|
<span className="text-sm font-medium text-fg">Content type</span>
|
|
144
|
-
|
|
144
|
+
{/* Visible cards, not a dropdown: the type is an easy-to-miss choice, and picking the
|
|
145
|
+
wrong one puts the entry under a different route. Cards make the selection deliberate. */}
|
|
146
|
+
<div className="flex flex-wrap gap-2">
|
|
145
147
|
{cts.map((c) => (
|
|
146
|
-
<
|
|
148
|
+
<button
|
|
149
|
+
key={c.id}
|
|
150
|
+
type="button"
|
|
151
|
+
onClick={() => setTypeId(c.id)}
|
|
152
|
+
className={`rounded-lg border px-4 py-2.5 text-sm font-medium transition-colors ${
|
|
153
|
+
typeId === c.id
|
|
154
|
+
? "border-brand-green bg-surface-muted text-fg"
|
|
155
|
+
: "border-border bg-surface-card text-fg-muted hover:border-fg-subtle hover:text-fg"
|
|
156
|
+
}`}
|
|
157
|
+
>
|
|
158
|
+
{c.name}
|
|
159
|
+
</button>
|
|
147
160
|
))}
|
|
148
|
-
</
|
|
149
|
-
</
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
150
163
|
<Input label="Title" value={title} onChange={setTitle} />
|
|
151
164
|
<Input label="Slug" value={slug} onChange={setSlug} placeholder={slugify(title)} />
|
|
152
165
|
<div className="mt-2 flex justify-end gap-2">
|
package/src/fields.tsx
CHANGED
|
@@ -73,12 +73,7 @@ function FieldInput({ def, value, onChange, api }: { def: FieldDefinition; value
|
|
|
73
73
|
case "select":
|
|
74
74
|
return (
|
|
75
75
|
<FieldShell label={label}>
|
|
76
|
-
<
|
|
77
|
-
<option value="">—</option>
|
|
78
|
-
{(def.options ?? []).map((o) => (
|
|
79
|
-
<option key={o} value={o}>{o}</option>
|
|
80
|
-
))}
|
|
81
|
-
</select>
|
|
76
|
+
<SelectField def={def} value={value as string | null} onChange={onChange} api={api} />
|
|
82
77
|
</FieldShell>
|
|
83
78
|
);
|
|
84
79
|
case "media":
|
|
@@ -235,6 +230,33 @@ function Repeater({ def, value, onChange, api, label }: { def: FieldDefinition;
|
|
|
235
230
|
);
|
|
236
231
|
}
|
|
237
232
|
|
|
233
|
+
// A `select` field. Static `options` render as-is; when `optionsFrom` is set, the options are
|
|
234
|
+
// fetched once from that query handler (returns `{ value, label }[]`) — e.g. a live list of
|
|
235
|
+
// campaigns — so the editor never has to hardcode or copy identifiers by hand.
|
|
236
|
+
function SelectField({ def, value, onChange, api }: { def: FieldDefinition; value: string | null; onChange: (v: unknown) => void; api: Api }) {
|
|
237
|
+
const [dyn, setDyn] = useState<{ value: string; label: string }[] | null>(null);
|
|
238
|
+
const from = def.optionsFrom;
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
if (!from) return;
|
|
241
|
+
let alive = true;
|
|
242
|
+
api
|
|
243
|
+
.call<{ value: string; label: string }[]>(from)
|
|
244
|
+
.then((r) => { if (alive) setDyn(Array.isArray(r) ? r : []); })
|
|
245
|
+
.catch(() => { if (alive) setDyn([]); });
|
|
246
|
+
return () => { alive = false; };
|
|
247
|
+
}, [from, api]);
|
|
248
|
+
const loading = Boolean(from) && dyn === null;
|
|
249
|
+
const opts = from ? dyn ?? [] : (def.options ?? []).map((o) => ({ value: o, label: o }));
|
|
250
|
+
return (
|
|
251
|
+
<select className={CONTROL} value={value ?? ""} onChange={(e) => onChange(e.target.value || null)}>
|
|
252
|
+
<option value="">{loading ? "Načítám…" : "—"}</option>
|
|
253
|
+
{opts.map((o) => (
|
|
254
|
+
<option key={o.value} value={o.value}>{o.label}</option>
|
|
255
|
+
))}
|
|
256
|
+
</select>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
238
260
|
function MediaField({ value, onChange, api }: { value: string | null; onChange: (v: string | null) => void; api: Api }) {
|
|
239
261
|
const [open, setOpen] = useState(false);
|
|
240
262
|
const [media, setMedia] = useState<Media | null>(null);
|
package/src/routes/_layout.tsx
CHANGED
|
@@ -39,9 +39,14 @@ export default function RootLayout() {
|
|
|
39
39
|
) : null}
|
|
40
40
|
<Button variant="ghost" size="sm" className={tabCls("settings")} onPress={() => navigate("settings")}>Settings</Button>
|
|
41
41
|
{extraNav.map((l) => (
|
|
42
|
+
// Companion tools live OUTSIDE this SPA (a separate static page/worker route), so
|
|
43
|
+
// open them in a new tab. A same-tab click would be caught by the client router
|
|
44
|
+
// (Navigation API) and fall to the in-app 404, since the path isn't an SPA route.
|
|
42
45
|
<a
|
|
43
46
|
key={l.href}
|
|
44
47
|
href={l.href}
|
|
48
|
+
target="_blank"
|
|
49
|
+
rel="noopener noreferrer"
|
|
45
50
|
className="rounded-md px-2.5 py-1.5 text-sm text-fg-muted transition-colors hover:bg-surface-muted hover:text-fg"
|
|
46
51
|
>
|
|
47
52
|
{l.label}
|
package/src/types.ts
CHANGED
|
@@ -26,6 +26,10 @@ export interface FieldDefinition {
|
|
|
26
26
|
min?: number;
|
|
27
27
|
max?: number;
|
|
28
28
|
options?: string[];
|
|
29
|
+
/** For `select`: fetch options at edit time from a query handler of this name, which must
|
|
30
|
+
* return `{ value, label }[]`. Lets a select offer live data (e.g. existing campaigns)
|
|
31
|
+
* instead of a static list. Takes precedence over `options`. */
|
|
32
|
+
optionsFrom?: string;
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
export interface RegionDefinition {
|