@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pramen/cms-editor",
3
- "version": "0.0.33",
3
+ "version": "0.0.35",
4
4
  "description": "Visual block/page editor for @pramen/cms — a standalone React SPA that talks to the CMS handlers over HTTP.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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
- <label className="flex w-full flex-col gap-2">
142
+ <div className="flex w-full flex-col gap-2">
143
143
  <span className="text-sm font-medium text-fg">Content type</span>
144
- <select className="h-10 w-full rounded-lg border border-border bg-surface-card px-4 text-sm text-fg outline-none focus:border-brand-green" value={typeId} onChange={(e) => setTypeId(e.target.value)}>
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
- <option key={c.id} value={c.id}>{c.name}</option>
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
- </select>
149
- </label>
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
- <select className={CONTROL} value={(value as string) ?? ""} onChange={(e) => onChange(e.target.value || null)}>
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);
@@ -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 {