@realiizlabs/admin 0.8.0 → 0.8.1

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.
@@ -143,12 +143,26 @@ function readMeta(schema) {
143
143
  const s = schema;
144
144
  const raw = typeof s.meta === "function" ? s.meta() ?? {} : {};
145
145
  const pick = (k) => typeof raw[k] === "string" ? raw[k] : void 0;
146
+ const rec = raw.recommended;
147
+ const recommended = Array.isArray(rec) && rec.length === 2 && rec.every((n) => typeof n === "number") ? [rec[0], rec[1]] : void 0;
148
+ const INPUTS = ["text", "textarea", "date", "number", "boolean", "select", "tags", "image", "url"];
149
+ const input = typeof raw.input === "string" && INPUTS.includes(raw.input) ? raw.input : void 0;
146
150
  return {
147
151
  label: pick("label"),
148
152
  placeholder: pick("placeholder"),
149
- description: pick("description") ?? (typeof s.description === "string" ? s.description : void 0)
153
+ description: pick("description") ?? (typeof s.description === "string" ? s.description : void 0),
154
+ recommended,
155
+ suffix: pick("suffix"),
156
+ help: pick("help"),
157
+ input
150
158
  };
151
159
  }
160
+ function inputOverrideFor(name, schema, entry) {
161
+ return entry?.inputs?.[name] ?? readMeta(schema).input;
162
+ }
163
+ function recommendedFor(name, schema, entry) {
164
+ return entry?.recommended?.[name] ?? readMeta(schema).recommended;
165
+ }
152
166
  function titlecase(name) {
153
167
  const words = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").toLowerCase().trim();
154
168
  return words.charAt(0).toUpperCase() + words.slice(1);
@@ -162,6 +176,9 @@ function labelFor(name, schema, entry) {
162
176
  function placeholderFor(name, schema, entry) {
163
177
  return entry?.placeholders?.[name] ?? readMeta(schema).placeholder;
164
178
  }
179
+ function helpFor(name, schema, entry) {
180
+ return entry?.help?.[name] ?? readMeta(schema).help;
181
+ }
165
182
 
166
183
  // src/forms/walk.ts
167
184
  function walkSchema(schema, entry = {}) {
@@ -176,11 +193,14 @@ function walkSchema(schema, entry = {}) {
176
193
  return {
177
194
  name,
178
195
  label: labelFor(name, raw, entry),
179
- input: role === "image" ? "image" : info.input,
196
+ input: role === "image" ? "image" : inputOverrideFor(name, raw, entry) ?? info.input,
180
197
  required,
181
198
  constraints: info.constraints,
182
199
  messages: info.messages,
183
200
  placeholder: placeholderFor(name, raw, entry),
201
+ recommended: recommendedFor(name, raw, entry),
202
+ suffix: readMeta(raw).suffix,
203
+ help: helpFor(name, raw, entry),
184
204
  order: 0,
185
205
  role,
186
206
  options: entry.options?.[name] ?? info.options
@@ -212,6 +232,50 @@ function applyOrder(specs, order) {
212
232
  const rest = specs.filter((s) => !rank.has(s.name));
213
233
  return [...named, ...rest].map((s, i) => ({ ...s, order: i }));
214
234
  }
235
+ function ChipsInput({ id, name, value, onChange, onBlur, placeholder, invalid, max }) {
236
+ const [draft, setDraft] = react.useState("");
237
+ const commit = () => {
238
+ const parts = draft.split(",").map((s) => s.trim()).filter(Boolean);
239
+ if (parts.length) onChange([...value, ...parts.filter((p) => !value.includes(p))]);
240
+ setDraft("");
241
+ };
242
+ const onKey = (e) => {
243
+ if (e.key === "Enter" || e.key === ",") {
244
+ e.preventDefault();
245
+ commit();
246
+ } else if (e.key === "Backspace" && draft === "" && value.length) {
247
+ onChange(value.slice(0, -1));
248
+ }
249
+ };
250
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ["realiiz-chips", invalid && "realiiz-form__control--invalid"].filter(Boolean).join(" "), "data-name": name, children: [
251
+ value.map((tag) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "realiiz-chip", children: [
252
+ tag,
253
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "aria-label": `Remove ${tag}`, onClick: () => onChange(value.filter((t) => t !== tag)), children: "\xD7" })
254
+ ] }, tag)),
255
+ /* @__PURE__ */ jsxRuntime.jsx(
256
+ "input",
257
+ {
258
+ id,
259
+ type: "text",
260
+ className: "realiiz-chips__input",
261
+ value: draft,
262
+ placeholder: value.length ? "" : placeholder,
263
+ onChange: (e) => setDraft(e.target.value),
264
+ onKeyDown: onKey,
265
+ onBlur: () => {
266
+ commit();
267
+ onBlur();
268
+ },
269
+ "aria-invalid": invalid || void 0
270
+ }
271
+ ),
272
+ typeof max === "number" && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "realiiz-chips__count", children: [
273
+ value.length,
274
+ " of ",
275
+ max
276
+ ] })
277
+ ] });
278
+ }
215
279
  function Control(p) {
216
280
  const cls = ["realiiz-form__control", p.invalid && "realiiz-form__control--invalid"].filter(Boolean).join(" ");
217
281
  const common = {
@@ -286,18 +350,6 @@ function Control(p) {
286
350
  onChange: (e) => p.onChange(e.target.value)
287
351
  }
288
352
  );
289
- case "tags":
290
- return /* @__PURE__ */ jsxRuntime.jsx(
291
- "input",
292
- {
293
- ...common,
294
- type: "text",
295
- className: cls,
296
- value: str,
297
- placeholder: p.spec.placeholder ?? "one, two, three",
298
- onChange: (e) => p.onChange(e.target.value)
299
- }
300
- );
301
353
  default:
302
354
  return /* @__PURE__ */ jsxRuntime.jsx(
303
355
  "input",
@@ -313,24 +365,61 @@ function Control(p) {
313
365
  );
314
366
  }
315
367
  }
368
+ var defaultLabel = (label, help) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-form__help", title: help, children: label });
369
+ function LengthGauge({ length: raw, range, suffix }) {
370
+ const [min, max] = range;
371
+ const length = raw === 0 ? 0 : raw + (suffix?.length ?? 0);
372
+ const state = length === 0 ? "empty" : length < min ? "short" : length <= max ? "ok" : "over";
373
+ const scale = Math.round(max * 1.3);
374
+ const pct = (n) => `${Math.min(100, n / scale * 100)}%`;
375
+ const verdict = state === "ok" ? "good length" : state === "over" ? `over ${max} \u2014 may be cut off` : `aim for ${min}\u2013${max}`;
376
+ const incl = suffix && length > 0 ? ` incl. \u201C${suffix.trim()}\u201D` : "";
377
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: `realiiz-gauge realiiz-gauge--${state}`, title: `Recommended ${min}\u2013${max} characters${suffix ? ` including \u201C${suffix.trim()}\u201D` : ""}`, children: [
378
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "realiiz-gauge__bar", "aria-hidden": "true", children: [
379
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-gauge__zone", style: { left: pct(min), width: `calc(${pct(max)} - ${pct(min)})` } }),
380
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-gauge__fill", style: { width: pct(length) } })
381
+ ] }),
382
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "realiiz-gauge__text", children: [
383
+ /* @__PURE__ */ jsxRuntime.jsx("b", { children: length }),
384
+ " characters",
385
+ incl,
386
+ " \xB7 ",
387
+ verdict
388
+ ] })
389
+ ] });
390
+ }
316
391
  function Field(p) {
317
392
  const id = `${p.formId}-${p.spec.name}`;
318
393
  const invalid = p.issues.length > 0;
319
394
  const max = p.spec.constraints.max;
320
- const showCount = typeof max === "number" && (p.spec.input === "text" || p.spec.input === "textarea");
395
+ const isText = p.spec.input === "text" || p.spec.input === "textarea";
321
396
  const len = typeof p.value === "string" ? p.value.length : 0;
322
397
  const isCheck = p.spec.input === "boolean";
323
- const control = /* @__PURE__ */ jsxRuntime.jsx(Control, { id, spec: p.spec, value: p.value, invalid, options: p.options, onChange: p.onChange, onBlur: p.onBlur });
398
+ const isTags = p.spec.input === "tags";
399
+ const labelText = p.spec.help ? (p.renderLabel ?? defaultLabel)(p.spec.label, p.spec.help) : p.spec.label;
400
+ const control = isTags ? /* @__PURE__ */ jsxRuntime.jsx(
401
+ ChipsInput,
402
+ {
403
+ id,
404
+ name: p.spec.name,
405
+ value: Array.isArray(p.value) ? p.value : [],
406
+ onChange: (v) => p.onChange(v),
407
+ onBlur: p.onBlur,
408
+ placeholder: p.spec.placeholder,
409
+ invalid,
410
+ max: p.spec.constraints.exact ?? p.spec.constraints.max
411
+ }
412
+ ) : /* @__PURE__ */ jsxRuntime.jsx(Control, { id, spec: p.spec, value: p.value, invalid, options: p.options, onChange: p.onChange, onBlur: p.onBlur });
324
413
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `realiiz-form__field realiiz-form__field--${p.spec.input}`, "data-field": p.spec.name, children: [
325
414
  isCheck ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "realiiz-form__check", htmlFor: id, children: [
326
415
  control,
327
416
  /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
328
- p.spec.label,
417
+ labelText,
329
418
  p.spec.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-form__required", "aria-hidden": "true", children: "*" })
330
419
  ] })
331
420
  ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
332
421
  /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "realiiz-form__label", htmlFor: id, children: [
333
- p.spec.label,
422
+ labelText,
334
423
  p.spec.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-form__required", "aria-hidden": "true", children: "*" })
335
424
  ] }),
336
425
  control
@@ -339,15 +428,529 @@ function Field(p) {
339
428
  "Current: ",
340
429
  p.current
341
430
  ] }),
342
- showCount && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "realiiz-form__helper", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "realiiz-form__count", children: [
343
- len,
344
- " / ",
345
- max
346
- ] }) }),
431
+ isText && (p.spec.recommended || typeof max === "number") && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-form__helper", children: [
432
+ p.spec.recommended ? /* @__PURE__ */ jsxRuntime.jsx(LengthGauge, { length: len, range: p.spec.recommended, suffix: p.spec.suffix }) : /* @__PURE__ */ jsxRuntime.jsx("span", {}),
433
+ typeof max === "number" && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "realiiz-form__count", children: [
434
+ len,
435
+ " / ",
436
+ max
437
+ ] })
438
+ ] }),
347
439
  invalid && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "realiiz-form__error", id: `${id}-error`, children: p.issues.map((i, n) => /* @__PURE__ */ jsxRuntime.jsx("div", { children: i.message }, n)) })
348
440
  ] });
349
441
  }
350
442
 
443
+ // src/forms-ui/md/table.ts
444
+ var ROW = /^\s*\|.*\|\s*$/;
445
+ var SEP = /^\s*\|[\s:|-]+\|\s*$/;
446
+ function tableCells(line) {
447
+ return line.trim().replace(/^\|/, "").replace(/\|$/, "").split(/(?<!\\)\|/).map((c) => c.trim());
448
+ }
449
+ function tableAt(v, pos) {
450
+ const lines = v.split("\n");
451
+ let i = 0, off = 0;
452
+ while (i < lines.length && pos > off + lines[i].length) {
453
+ off += lines[i].length + 1;
454
+ i++;
455
+ }
456
+ if (i >= lines.length || !ROW.test(lines[i])) return null;
457
+ let s = i, e = i;
458
+ while (s > 0 && ROW.test(lines[s - 1])) s--;
459
+ while (e < lines.length - 1 && ROW.test(lines[e + 1])) e++;
460
+ if (e - s < 1 || !SEP.test(lines[s + 1])) return null;
461
+ const start = lines.slice(0, s).reduce((n, l) => n + l.length + 1, 0);
462
+ const end = start + lines.slice(s, e + 1).join("\n").length;
463
+ const align = tableCells(lines[s + 1]).map((c) => c.startsWith(":") && c.endsWith(":") ? "c" : c.endsWith(":") ? "r" : c.startsWith(":") ? "L" : "l");
464
+ const rows = lines.slice(s, e + 1).filter((_, n) => n !== 1).map(tableCells);
465
+ const col = Math.max(0, (lines[i].slice(0, pos - off).match(/(?<!\\)\|/g) || []).length - 1);
466
+ const row = i - s <= 1 ? 0 : i - s - 1;
467
+ return { start, end, rows, align, row, col };
468
+ }
469
+ function renderTable(rows, align) {
470
+ const cols = Math.max(...rows.map((r) => r.length));
471
+ const w = [];
472
+ for (let c = 0; c < cols; c++) w[c] = Math.max(3, ...rows.map((r) => (r[c] || "").length));
473
+ const pad = (t, c) => (t || "").padEnd(w[c]);
474
+ const line = (r) => "| " + Array.from({ length: cols }, (_, c) => pad(r[c], c)).join(" | ") + " |";
475
+ const sep = "|" + Array.from({ length: cols }, (_, c) => align[c] === "c" ? ":" + "-".repeat(w[c]) + ":" : align[c] === "r" ? " " + "-".repeat(w[c] - 1) + ": " : align[c] === "L" ? ":" + "-".repeat(w[c] - 1) + " " : " " + "-".repeat(w[c]) + " ").join("|") + "|";
476
+ return [line(rows[0]), sep, ...rows.slice(1).map(line)].join("\n");
477
+ }
478
+ function newTable(rows, cols) {
479
+ const head = Array.from({ length: cols }, (_, i) => `Column ${i + 1}`);
480
+ const grid = [head, ...Array.from({ length: rows }, () => Array(cols).fill(""))];
481
+ return { text: renderTable(grid, Array(cols).fill("l")), firstHeader: head[0] };
482
+ }
483
+ function applyTableOp(v, pos, op) {
484
+ const t = tableAt(v, pos);
485
+ if (!t) return null;
486
+ const rows = t.rows.map((r) => r.slice());
487
+ const cols = Math.max(...rows.map((r) => r.length));
488
+ const align = t.align.slice();
489
+ if (op === "rowAfter") {
490
+ rows.splice(Math.max(1, t.row + 1), 0, Array(cols).fill(""));
491
+ } else if (op === "rowDel") {
492
+ if (t.row === 0 || rows.length <= 2) return { error: "A table needs its header row and at least one row below it." };
493
+ rows.splice(t.row, 1);
494
+ } else if (op === "colAfter") {
495
+ rows.forEach((r) => {
496
+ while (r.length < cols) r.push("");
497
+ r.splice(t.col + 1, 0, "");
498
+ });
499
+ align.splice(t.col + 1, 0, "l");
500
+ } else if (op === "colDel") {
501
+ if (cols <= 1) return { error: "That's the last column \u2014 delete the table instead." };
502
+ rows.forEach((r) => r.splice(t.col, 1));
503
+ align.splice(t.col, 1);
504
+ }
505
+ const out = renderTable(rows, align);
506
+ return { text: v.slice(0, t.start) + out + v.slice(t.end), a: t.start, b: t.start + out.length };
507
+ }
508
+
509
+ // src/forms-ui/md/edits.ts
510
+ var toggleWrap = (mark, placeholder = "text") => (s, a, b) => {
511
+ const sel = s.slice(a, b);
512
+ const n = mark.length;
513
+ if (sel.length >= 2 * n && sel.startsWith(mark) && sel.endsWith(mark)) {
514
+ const inner = sel.slice(n, sel.length - n);
515
+ return { text: s.slice(0, a) + inner + s.slice(b), a, b: a + inner.length };
516
+ }
517
+ if (s.slice(a - n, a) === mark && s.slice(b, b + n) === mark) {
518
+ return { text: s.slice(0, a - n) + sel + s.slice(b + n), a: a - n, b: a - n + sel.length };
519
+ }
520
+ const body = sel || placeholder;
521
+ return { text: s.slice(0, a) + mark + body + mark + s.slice(b), a: a + n, b: a + n + body.length };
522
+ };
523
+ var PREFIX_RE = /^(#{1,6}\s+|[-*+]\s+|\d+[.)]\s+|>\s?)/;
524
+ var lineSpan = (s, a, b) => {
525
+ const start = s.lastIndexOf("\n", a - 1) + 1;
526
+ const endIdx = s.indexOf("\n", Math.max(b, a));
527
+ return { start, end: endIdx === -1 ? s.length : endIdx };
528
+ };
529
+ var toggleLines = (prefix, matches) => (s, a, b) => {
530
+ const { start, end } = lineSpan(s, a, b);
531
+ const lines = s.slice(start, end).split("\n");
532
+ const allOn = lines.every((l) => l.trim() === "" || matches(l));
533
+ const out = lines.map((l, i) => {
534
+ const bare = l.replace(PREFIX_RE, "");
535
+ return allOn ? bare : (typeof prefix === "string" ? prefix : prefix(i)) + bare;
536
+ });
537
+ const replaced = out.join("\n");
538
+ return { text: s.slice(0, start) + replaced + s.slice(end), a: start, b: start + replaced.length };
539
+ };
540
+ var LINK_RE = /\[([^\]\n]*)\]\(([^)\n]*)\)/g;
541
+ function linkAt(s, a, b) {
542
+ for (const m of s.matchAll(LINK_RE)) {
543
+ if (s[m.index - 1] === "!") continue;
544
+ const from = m.index, to = from + m[0].length;
545
+ if (a >= from && b <= to) return { from, to, label: m[1] };
546
+ if (from > b) break;
547
+ }
548
+ return null;
549
+ }
550
+ var toggleLink = (s, a, b) => {
551
+ const hit = linkAt(s, a, b);
552
+ if (hit) return { text: s.slice(0, hit.from) + hit.label + s.slice(hit.to), a: hit.from, b: hit.from + hit.label.length };
553
+ const label = s.slice(a, b) || "link text";
554
+ const urlStart = a + label.length + 3;
555
+ return { text: s.slice(0, a) + `[${label}](https://)` + s.slice(b), a: urlStart, b: urlStart + 8 };
556
+ };
557
+ var insertImage = (s, a, b) => {
558
+ const alt = s.slice(a, b) || "description";
559
+ const urlStart = a + alt.length + 4;
560
+ return { text: s.slice(0, a) + `![${alt}](https://)` + s.slice(b), a: urlStart, b: urlStart + 8 };
561
+ };
562
+ var insertDot = (s, a, b) => {
563
+ const t = " \xB7 ";
564
+ return { text: s.slice(0, a) + t + s.slice(b), a: a + t.length, b: a + t.length };
565
+ };
566
+ var blockBefore = (s, a) => a === 0 || s.slice(0, a).endsWith("\n\n") ? "" : s.slice(0, a).endsWith("\n") ? "\n" : "\n\n";
567
+ var insertRule = (s, a, b) => {
568
+ const before = blockBefore(s, a);
569
+ const t = before + "---\n\n";
570
+ return { text: s.slice(0, a) + t + s.slice(b), a: a + t.length, b: a + t.length };
571
+ };
572
+ var insertTable = (rows, cols) => (s, a, b) => {
573
+ const { text, firstHeader } = newTable(rows, cols);
574
+ const before = blockBefore(s, a);
575
+ const t = before + text + "\n";
576
+ const at = a + before.length + text.indexOf(firstHeader);
577
+ return { text: s.slice(0, a) + t + s.slice(b), a: at, b: at + firstHeader.length };
578
+ };
579
+ var toggleTerm = (component = "Term", attr = "definition") => (s, a, b) => {
580
+ const re = new RegExp(`<${component}\\s+${attr}="([^"]*)"\\s*>([\\s\\S]*?)</${component}>`, "g");
581
+ for (const m of s.matchAll(re)) {
582
+ const from = m.index, to = from + m[0].length;
583
+ if (a >= from && b <= to) return { text: s.slice(0, from) + m[2] + s.slice(to), a: from, b: from + m[2].length };
584
+ if (from > b) break;
585
+ }
586
+ const word = s.slice(a, b) || "term";
587
+ const open = `<${component} ${attr}="`;
588
+ const text = s.slice(0, a) + open + `">${word}</${component}>` + s.slice(b);
589
+ return { text, a: a + open.length, b: a + open.length };
590
+ };
591
+ var TOOLS = [
592
+ { id: "bold", title: "Bold \u2014 \u2318B", apply: toggleWrap("**") },
593
+ { id: "italic", title: "Italic \u2014 \u2318I", apply: toggleWrap("_") },
594
+ { id: "h2", title: "Section heading", apply: toggleLines("## ", (l) => /^##\s/.test(l)) },
595
+ { id: "h3", title: "Sub-heading", apply: toggleLines("### ", (l) => /^###\s/.test(l)) },
596
+ { id: "ul", title: "Bulleted list", apply: toggleLines("- ", (l) => /^[-*+]\s/.test(l)) },
597
+ { id: "ol", title: "Numbered list", apply: toggleLines((i) => `${i + 1}. `, (l) => /^\d+[.)]\s/.test(l)) },
598
+ { id: "quote", title: "Quote", apply: toggleLines("> ", (l) => /^>/.test(l)) },
599
+ { id: "link", title: "Link", apply: toggleLink },
600
+ { id: "code", title: "Inline code", apply: toggleWrap("`", "code") },
601
+ { id: "image", title: "Image", apply: insertImage },
602
+ { id: "dot", title: "Separator between details ( \xB7 )", apply: insertDot },
603
+ { id: "rule", title: "Divider line across the page", apply: insertRule }
604
+ ];
605
+ var toolById = (id) => TOOLS.find((t) => t.id === id);
606
+ function activeTools(s, a, b) {
607
+ const on = /* @__PURE__ */ new Set();
608
+ const sel = s.slice(a, b);
609
+ const wrapped = (m) => s.slice(a - m.length, a) === m && s.slice(b, b + m.length) === m || sel.length >= 2 * m.length && sel.startsWith(m) && sel.endsWith(m);
610
+ if (wrapped("**") || spanAt(s, a, b, /\*\*[^*\n]+\*\*/g, 2)) on.add("bold");
611
+ if (wrapped("_") || spanAt(s, a, b, /(?<![\w_])_[^_\n]+_(?![\w_])/g, 1)) on.add("italic");
612
+ if (wrapped("`") || spanAt(s, a, b, /`[^`\n]+`/g, 1)) on.add("code");
613
+ if (linkAt(s, a, b)) on.add("link");
614
+ if (spanAt(s, a, b, /<([A-Z][\w]*)\s[^>]*>[\s\S]*?<\/\1>/g, 0)) on.add("term");
615
+ const { start, end } = lineSpan(s, a, a);
616
+ const line = s.slice(start, end);
617
+ if (/^##\s/.test(line)) on.add("h2");
618
+ else if (/^###\s/.test(line)) on.add("h3");
619
+ else if (/^[-*+]\s/.test(line)) on.add("ul");
620
+ else if (/^\d+[.)]\s/.test(line)) on.add("ol");
621
+ else if (/^>/.test(line)) on.add("quote");
622
+ return on;
623
+ }
624
+ function spanAt(s, a, b, re, pad) {
625
+ for (const m of s.matchAll(re)) {
626
+ const from = m.index, to = from + m[0].length;
627
+ if (a >= from + pad && b <= to - pad) return true;
628
+ if (from > b) break;
629
+ }
630
+ return false;
631
+ }
632
+ var Svg = ({ children, fill = "none" }) => /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "realiiz-md__ico", viewBox: "0 0 24 24", fill, stroke: "currentColor", strokeWidth: "1.9", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children });
633
+ var MD_ICONS = {
634
+ bold: /* @__PURE__ */ jsxRuntime.jsx("b", { children: "B" }),
635
+ italic: /* @__PURE__ */ jsxRuntime.jsx("i", { children: "I" }),
636
+ h2: /* @__PURE__ */ jsxRuntime.jsxs(Svg, { children: [
637
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 5v14" }),
638
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 5v14" }),
639
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 12h8" }),
640
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M17.5 19v-6.5l-1.6 1.2" })
641
+ ] }),
642
+ h3: /* @__PURE__ */ jsxRuntime.jsxs(Svg, { children: [
643
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 5v14" }),
644
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 5v14" }),
645
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 12h7" }),
646
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16 12.5h3.5l-2 2.2a1.9 1.9 0 1 1-1.5 3.2" })
647
+ ] }),
648
+ ul: /* @__PURE__ */ jsxRuntime.jsxs(Svg, { children: [
649
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "9", y1: "6.5", x2: "20", y2: "6.5" }),
650
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "9", y1: "12", x2: "20", y2: "12" }),
651
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "9", y1: "17.5", x2: "20", y2: "17.5" }),
652
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.6", cy: "6.5", r: "1.3", fill: "currentColor", stroke: "none" }),
653
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.6", cy: "12", r: "1.3", fill: "currentColor", stroke: "none" }),
654
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.6", cy: "17.5", r: "1.3", fill: "currentColor", stroke: "none" })
655
+ ] }),
656
+ ol: /* @__PURE__ */ jsxRuntime.jsxs(Svg, { children: [
657
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "10", y1: "6.5", x2: "20", y2: "6.5" }),
658
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "10", y1: "12", x2: "20", y2: "12" }),
659
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "10", y1: "17.5", x2: "20", y2: "17.5" }),
660
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 5.6 5.2 5v3.4" }),
661
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.6 11.2h1.8L3.6 13.7h1.9" }),
662
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.7 16.3h1.7v1.2H4.2h1.2v1.2H3.7" })
663
+ ] }),
664
+ quote: /* @__PURE__ */ jsxRuntime.jsxs(Svg, { children: [
665
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 7c-2.4 0-4 1.7-4 4 0 1.9 1.3 3.2 3 3.2.4 0 .8-.1 1-.2-.3 1.4-1.5 2.6-3 3l.7 1.5c2.8-.8 4.8-3.3 4.8-6.6C11.5 8.7 10.5 7 9 7z", fill: "currentColor", stroke: "none" }),
666
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M18 7c-2.4 0-4 1.7-4 4 0 1.9 1.3 3.2 3 3.2.4 0 .8-.1 1-.2-.3 1.4-1.5 2.6-3 3l.7 1.5c2.8-.8 4.8-3.3 4.8-6.6C20.5 8.7 19.5 7 18 7z", fill: "currentColor", stroke: "none" })
667
+ ] }),
668
+ link: /* @__PURE__ */ jsxRuntime.jsxs(Svg, { children: [
669
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M10 13a5 5 0 0 0 7.5.5l2.5-2.5a5 5 0 0 0-7-7l-1.5 1.4" }),
670
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 11a5 5 0 0 0-7.5-.5L4 13a5 5 0 0 0 7 7l1.4-1.4" })
671
+ ] }),
672
+ code: /* @__PURE__ */ jsxRuntime.jsxs(Svg, { children: [
673
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m8 7-5 5 5 5" }),
674
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m16 7 5 5-5 5" }),
675
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m14 4-4 16" })
676
+ ] }),
677
+ // A tooltip: a tall speech bubble with an "i" — what the reader will actually see on the page.
678
+ term: /* @__PURE__ */ jsxRuntime.jsxs(Svg, { children: [
679
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 2.5h14a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2h-4.5L12 21.5 9.5 17.5H5a2 2 0 0 1-2-2v-11a2 2 0 0 1 2-2z" }),
680
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 9v4.5" }),
681
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "6.2", r: ".9", fill: "currentColor", stroke: "none" })
682
+ ] }),
683
+ image: /* @__PURE__ */ jsxRuntime.jsxs(Svg, { children: [
684
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
685
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "8.5", cy: "8.5", r: "1.6" }),
686
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m21 15-5-5L5 21" })
687
+ ] }),
688
+ dot: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-md__dot", children: "\xB7" }),
689
+ rule: /* @__PURE__ */ jsxRuntime.jsx(Svg, { children: /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "12", x2: "21", y2: "12" }) }),
690
+ table: /* @__PURE__ */ jsxRuntime.jsxs(Svg, { children: [
691
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "4", width: "18", height: "16", rx: "2" }),
692
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "9.5", x2: "21", y2: "9.5" }),
693
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "15", x2: "21", y2: "15" }),
694
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "9.5", x2: "12", y2: "20" })
695
+ ] }),
696
+ expand: /* @__PURE__ */ jsxRuntime.jsxs(Svg, { children: [
697
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 3H3v6" }),
698
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M15 21h6v-6" }),
699
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 3l7 7" }),
700
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 21l-7-7" })
701
+ ] }),
702
+ contract: /* @__PURE__ */ jsxRuntime.jsxs(Svg, { children: [
703
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 10h6V4" }),
704
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M20 14h-6v6" }),
705
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 3l7 7" }),
706
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 21l-7-7" })
707
+ ] })
708
+ };
709
+ var MAX = 6;
710
+ function TableGrid({ onPick }) {
711
+ const [open, setOpen] = react.useState(false);
712
+ const [hot, setHot] = react.useState([0, 0]);
713
+ const wrap = react.useRef(null);
714
+ react.useEffect(() => {
715
+ if (!open) return;
716
+ const away = (e) => {
717
+ if (!wrap.current?.contains(e.target)) setOpen(false);
718
+ };
719
+ const esc = (e) => {
720
+ if (e.key === "Escape") setOpen(false);
721
+ };
722
+ document.addEventListener("mousedown", away);
723
+ document.addEventListener("keydown", esc);
724
+ return () => {
725
+ document.removeEventListener("mousedown", away);
726
+ document.removeEventListener("keydown", esc);
727
+ };
728
+ }, [open]);
729
+ const [r, c] = hot;
730
+ const label = r && c ? `${c} column${c === 1 ? "" : "s"} \xD7 ${r} row${r === 1 ? "" : "s"}` : "Pick a size";
731
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: ["realiiz-md__tgwrap", open && "is-open"].filter(Boolean).join(" "), ref: wrap, children: [
732
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-tool": "table", "data-tip": "New table \u2014 pick a size", "aria-label": "Insert a table", "aria-expanded": open, onClick: () => setOpen((o) => !o), children: MD_ICONS.table }),
733
+ open && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "realiiz-md__tgrid", role: "dialog", "aria-label": "Table size", onMouseLeave: () => setHot([0, 0]), children: [
734
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-md__tgcells", children: Array.from({ length: MAX * MAX }, (_, n) => {
735
+ const rr = Math.floor(n / MAX) + 1, cc = n % MAX + 1;
736
+ return /* @__PURE__ */ jsxRuntime.jsx(
737
+ "i",
738
+ {
739
+ className: rr <= r && cc <= c ? "hot" : void 0,
740
+ onMouseEnter: () => setHot([rr, cc]),
741
+ onClick: () => {
742
+ setOpen(false);
743
+ onPick(rr, cc);
744
+ },
745
+ "aria-label": `${cc} \xD7 ${rr}`,
746
+ role: "button"
747
+ },
748
+ n
749
+ );
750
+ }) }),
751
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-md__tglab", children: label })
752
+ ] })
753
+ ] });
754
+ }
755
+ var FORMAT = ["bold", "italic"];
756
+ var BLOCK = ["h2", "h3", "ul", "ol", "quote"];
757
+ var INLINE = ["link", "code"];
758
+ var INSERT = ["image", "dot", "rule"];
759
+ var TABLE_OPS = [
760
+ { op: "rowAfter", label: "+ Row", tip: "Insert a row below this one" },
761
+ { op: "rowDel", label: "\u2212 Row", tip: "Delete this row" },
762
+ { op: "colAfter", label: "+ Col", tip: "Insert a column to the right" },
763
+ { op: "colDel", label: "\u2212 Col", tip: "Delete this column" }
764
+ ];
765
+ function MarkdownEditor({ id, name = "body", value, onChange, placeholder, renderPreview, previewClassName, minHeight = 320, termComponent }) {
766
+ const [mode, setMode] = react.useState(() => renderPreview && value.trim() ? "preview" : "edit");
767
+ const [preview, setPreview] = react.useState(null);
768
+ const [loading, setLoading] = react.useState(false);
769
+ const [expanded, setExpanded] = react.useState(false);
770
+ const [on, setOn] = react.useState(() => /* @__PURE__ */ new Set());
771
+ const [inTable, setInTable] = react.useState(false);
772
+ const [note, setNote] = react.useState(null);
773
+ const ta = react.useRef(null);
774
+ const sync = react.useCallback(() => {
775
+ const el = ta.current;
776
+ if (!el) return;
777
+ setOn(activeTools(el.value, el.selectionStart, el.selectionEnd));
778
+ setInTable(Boolean(tableAt(el.value, el.selectionStart)));
779
+ }, []);
780
+ const commit = (edit) => {
781
+ const el = ta.current;
782
+ if (!el) return;
783
+ const { text, a, b } = edit;
784
+ const from = commonPrefix(el.value, text);
785
+ const oldTail = el.value.length - from, newTail = text.length - from;
786
+ const suffix = commonSuffix(el.value.slice(from), text.slice(from));
787
+ const oldEnd = from + oldTail - suffix, newEnd = from + newTail - suffix;
788
+ let ok = false;
789
+ el.focus();
790
+ el.setSelectionRange(from, oldEnd);
791
+ try {
792
+ ok = document.execCommand("insertText", false, text.slice(from, newEnd));
793
+ } catch {
794
+ ok = false;
795
+ }
796
+ if (!ok || el.value !== text) onChange(text);
797
+ requestAnimationFrame(() => {
798
+ el.setSelectionRange(a, b);
799
+ sync();
800
+ });
801
+ };
802
+ const run = (apply) => {
803
+ const el = ta.current;
804
+ if (!el) return;
805
+ commit(apply(value, el.selectionStart, el.selectionEnd));
806
+ };
807
+ const runTable = (op) => {
808
+ const el = ta.current;
809
+ if (!el) return;
810
+ const r = applyTableOp(value, el.selectionStart, op);
811
+ if (!r) return;
812
+ if ("error" in r) {
813
+ setNote(r.error);
814
+ return;
815
+ }
816
+ setNote(null);
817
+ commit(r);
818
+ };
819
+ const onKey = (e) => {
820
+ if (e.key === "Escape" && expanded) {
821
+ e.preventDefault();
822
+ setExpanded(false);
823
+ return;
824
+ }
825
+ if (!(e.metaKey || e.ctrlKey)) return;
826
+ if (e.key === "b") {
827
+ e.preventDefault();
828
+ run(toolById("bold").apply);
829
+ }
830
+ if (e.key === "i") {
831
+ e.preventDefault();
832
+ run(toolById("italic").apply);
833
+ }
834
+ if (e.key === "k") {
835
+ e.preventDefault();
836
+ run(toolById("link").apply);
837
+ }
838
+ };
839
+ react.useEffect(() => {
840
+ if (!expanded) return;
841
+ const prev = document.body.style.overflow;
842
+ document.body.style.overflow = "hidden";
843
+ const esc = (e) => {
844
+ if (e.key === "Escape") setExpanded(false);
845
+ };
846
+ document.addEventListener("keydown", esc);
847
+ return () => {
848
+ document.body.style.overflow = prev;
849
+ document.removeEventListener("keydown", esc);
850
+ };
851
+ }, [expanded]);
852
+ react.useEffect(() => {
853
+ if (mode !== "preview" || !renderPreview) return;
854
+ let live = true;
855
+ setLoading(true);
856
+ renderPreview(value).then((r) => {
857
+ if (live) setPreview(r);
858
+ }).catch((err) => {
859
+ if (live) setPreview({ error: err instanceof Error ? err.message : String(err) });
860
+ }).finally(() => {
861
+ if (live) setLoading(false);
862
+ });
863
+ return () => {
864
+ live = false;
865
+ };
866
+ }, [mode, value, renderPreview]);
867
+ const termTool = termComponent ? { id: "term", title: `Tooltip \u2014 wraps the selection in <${termComponent}>`, apply: toggleTerm(termComponent) } : null;
868
+ const btn = (toolId) => {
869
+ const t = toolId === "term" && termTool ? termTool : toolById(toolId);
870
+ return /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-tool": t.id, "data-tip": t.title, "aria-label": t.title, "aria-pressed": on.has(t.id) || void 0, className: on.has(t.id) ? "on" : void 0, onMouseDown: (e) => e.preventDefault(), onClick: () => run(t.apply), children: MD_ICONS[t.id] }, t.id);
871
+ };
872
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ["realiiz-md", expanded && "realiiz-md--fs"].filter(Boolean).join(" "), children: [
873
+ renderPreview && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "realiiz-md__mode", role: "tablist", "aria-label": "Editor mode", children: [
874
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", role: "tab", "aria-selected": mode === "edit", onClick: () => setMode("edit"), children: "Edit Markdown" }),
875
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", role: "tab", "aria-selected": mode === "preview", onClick: () => setMode("preview"), children: "Preview" })
876
+ ] }),
877
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-md__panel", children: [
878
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-md__head", children: [
879
+ mode === "edit" && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "realiiz-md__tools", role: "toolbar", "aria-label": "Formatting", children: [
880
+ FORMAT.map(btn),
881
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-md__sep" }),
882
+ BLOCK.map(btn),
883
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-md__sep" }),
884
+ INLINE.map(btn),
885
+ termTool && btn("term"),
886
+ inTable && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
887
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-md__sep" }),
888
+ TABLE_OPS.map((t) => /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-tbl-op": t.op, "data-tip": t.tip, "aria-label": t.tip, onMouseDown: (e) => e.preventDefault(), onClick: () => runTable(t.op), children: t.label }, t.op))
889
+ ] }),
890
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "realiiz-md__insert", children: [
891
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-md__lab", children: "Insert" }),
892
+ INSERT.map(btn),
893
+ /* @__PURE__ */ jsxRuntime.jsx(TableGrid, { onPick: (r, c) => run(insertTable(r, c)) })
894
+ ] })
895
+ ] }),
896
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-md__spacer" }),
897
+ /* @__PURE__ */ jsxRuntime.jsx(
898
+ "button",
899
+ {
900
+ type: "button",
901
+ className: "realiiz-md__expand",
902
+ "data-tip": expanded ? "Back to the normal layout \u2014 or press Esc" : "Fill the window \u2014 Esc to come back",
903
+ "aria-label": expanded ? "Exit" : "Expand",
904
+ onClick: () => setExpanded((x) => !x),
905
+ children: expanded ? MD_ICONS.contract : MD_ICONS.expand
906
+ }
907
+ )
908
+ ] }),
909
+ mode === "edit" ? /* @__PURE__ */ jsxRuntime.jsx(
910
+ "textarea",
911
+ {
912
+ ref: ta,
913
+ id,
914
+ name,
915
+ className: "realiiz-form__control realiiz-form__control--body realiiz-md__ta",
916
+ style: expanded ? void 0 : { minHeight },
917
+ value,
918
+ placeholder,
919
+ onChange: (e) => {
920
+ onChange(e.target.value);
921
+ sync();
922
+ },
923
+ onSelect: sync,
924
+ onKeyUp: sync,
925
+ onClick: sync,
926
+ onKeyDown: onKey,
927
+ spellCheck: true
928
+ }
929
+ ) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-md__preview", style: expanded ? void 0 : { minHeight }, "aria-live": "polite", "aria-busy": loading, children: [
930
+ loading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "realiiz-md__loading", children: "Rendering\u2026" }),
931
+ !loading && preview && "error" in preview && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-form__alert", role: "alert", children: [
932
+ "Couldn't render: ",
933
+ preview.error
934
+ ] }),
935
+ !loading && preview && "html" in preview && /* @__PURE__ */ jsxRuntime.jsx("div", { className: ["realiiz-prose", previewClassName].filter(Boolean).join(" "), dangerouslySetInnerHTML: { __html: preview.html } })
936
+ ] })
937
+ ] }),
938
+ note && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "realiiz-form__helper realiiz-md__note", role: "status", children: note })
939
+ ] });
940
+ }
941
+ function commonPrefix(a, b) {
942
+ const n = Math.min(a.length, b.length);
943
+ let i = 0;
944
+ while (i < n && a[i] === b[i]) i++;
945
+ return i;
946
+ }
947
+ function commonSuffix(a, b) {
948
+ const n = Math.min(a.length, b.length);
949
+ let i = 0;
950
+ while (i < n && a[a.length - 1 - i] === b[b.length - 1 - i]) i++;
951
+ return i;
952
+ }
953
+
351
954
  // src/forms-ui/issues.ts
352
955
  function mapIssues(issues, specs) {
353
956
  const byName = new Map(specs.map((s) => [s.name, s]));
@@ -424,9 +1027,65 @@ var FORM_CSS = `
424
1027
  .realiiz-form__alert{border:1px solid var(--_danger);border-radius:var(--_radius);padding:12px 14px;margin:0 0 20px;color:var(--_danger)}
425
1028
  .realiiz-form__alert ul{margin:6px 0 0;padding-left:18px}
426
1029
  .realiiz-form__actions{display:flex;gap:10px;justify-content:flex-end;flex-wrap:wrap;margin-top:8px}
1030
+ .realiiz-form__help{text-decoration:underline dotted;text-decoration-color:color-mix(in srgb,currentColor 45%,transparent);text-underline-offset:4px;cursor:help}
1031
+ .realiiz-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
1032
+ .realiiz-form__footer{max-width:920px}
1033
+ .realiiz-form__bar{display:flex;align-items:center;gap:10px;margin-top:16px;padding:12px 0;border-top:1px solid var(--_border);background:#fff}
1034
+ .realiiz-form__status{margin-right:auto;font-size:.85rem;color:var(--_muted)}
1035
+ .realiiz-form__bar[data-dirty] .realiiz-form__status{color:var(--_fg)}
427
1036
  .realiiz-form__button{font:inherit;font-weight:600;border-radius:var(--_radius);padding:10px 16px;border:1px solid var(--_border);background:#fff;color:var(--_fg);cursor:pointer;min-height:40px}
428
1037
  .realiiz-form__button--primary{background:var(--_accent);color:var(--_accent-fg);border-color:var(--_accent)}
429
1038
  .realiiz-form__button:disabled{opacity:.6;cursor:default}
1039
+ .realiiz-md{--_md-head:#fafbfd;--_md-hover:#f4f8fd;--_md-hover-line:#bcd0ea;--_md-on:#eef4fb;--_md-tip:#1f2933;position:relative}
1040
+ .realiiz-md__mode{display:inline-flex;align-self:flex-start;width:max-content;gap:2px;padding:3px;background:color-mix(in srgb,var(--_border) 40%,transparent);border-radius:8px;margin-bottom:8px}
1041
+ .realiiz-md__mode button{font:inherit;font-size:.8rem;padding:5px 13px;border:0;border-radius:6px;background:transparent;color:var(--_muted);cursor:pointer}
1042
+ .realiiz-md__mode button[aria-selected="true"]{background:#fff;color:var(--_fg);box-shadow:0 1px 2px rgba(0,0,0,.12)}
1043
+ .realiiz-md__panel{position:relative;z-index:0;display:flex;flex-direction:column;border:1px solid var(--_border);border-radius:10px;background:#fff;overflow:hidden}
1044
+ .realiiz-md__panel:focus-within{border-color:var(--_md-hover-line)}
1045
+ .realiiz-md__head{position:relative;z-index:5;display:flex;align-items:center;gap:2px;flex-wrap:wrap;padding:5px 7px;background:var(--_md-head);border-bottom:1px solid var(--_border);flex-shrink:0}
1046
+ .realiiz-md__tools{display:flex;align-items:center;gap:3px;flex-wrap:wrap}
1047
+ .realiiz-md__spacer{flex:1}
1048
+ .realiiz-md__head button{position:relative;min-width:31px;height:28px;padding:0 9px;display:inline-flex;align-items:center;justify-content:center;background:#fff;border:1px solid var(--_border);border-radius:6px;font-family:inherit;font-size:.82rem;color:var(--_fg);cursor:pointer;line-height:1}
1049
+ .realiiz-md__head button:hover:not(:disabled){border-color:var(--_md-hover-line);background:var(--_md-hover)}
1050
+ .realiiz-md__head button:active{background:var(--_md-on)}
1051
+ .realiiz-md__head button.on{background:var(--_md-on);border-color:var(--_accent);color:var(--_accent);font-weight:600}
1052
+ .realiiz-md__ico{width:15px;height:15px;display:block}
1053
+ .realiiz-md__dot{font-size:1.05rem;line-height:1}
1054
+ .realiiz-md__sep{width:1px;height:17px;background:var(--_border);margin:0 5px}
1055
+ .realiiz-md__insert{display:inline-flex;align-items:center;gap:3px;border-left:1px solid var(--_border);padding-left:8px;margin-left:6px}
1056
+ .realiiz-md__insert button{background:#f6f8fa;color:var(--_muted);padding:0;width:31px}
1057
+ .realiiz-md__insert button:hover{color:var(--_accent)}
1058
+ .realiiz-md__lab{font-size:.74rem;color:var(--_muted);margin-right:3px}
1059
+ .realiiz-md__head button[data-tip]::after{content:attr(data-tip);position:absolute;top:calc(100% + 6px);left:0;background:var(--_md-tip);color:#fff;font-size:.72rem;padding:4px 8px;border-radius:5px;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .12s;z-index:20}
1060
+ .realiiz-md__head button[data-tip]:hover::after,.realiiz-md__head button[data-tip]:focus-visible::after{opacity:1}
1061
+ .realiiz-md__expand[data-tip]::after{left:auto;right:0}
1062
+ .realiiz-md__tgwrap{position:relative;display:inline-flex}
1063
+ .realiiz-md__tgrid{position:absolute;top:calc(100% + 6px);left:0;background:#fff;border:1px solid var(--_border);border-radius:9px;padding:9px;box-shadow:0 10px 28px rgba(0,0,0,.14);z-index:7;display:block}
1064
+ .realiiz-md__tgcells{display:grid;grid-template-columns:repeat(6,17px);gap:3px}
1065
+ .realiiz-md__tgcells i{width:17px;height:15px;border:1px solid var(--_border);border-radius:2px;background:#fff;cursor:pointer;display:block}
1066
+ .realiiz-md__tgcells i.hot{background:#dbe9fb;border-color:var(--_accent)}
1067
+ .realiiz-md__tglab{display:block;margin-top:8px;font-size:.74rem;color:var(--_muted);text-align:center;white-space:nowrap}
1068
+ .realiiz-md .realiiz-md__ta{flex:1 1 auto;border:0;border-radius:0;resize:vertical;background:#fff;max-width:none;padding:16px 18px;line-height:1.65}
1069
+ .realiiz-md .realiiz-md__ta:focus{outline:none;box-shadow:none}
1070
+ .realiiz-md__preview{padding:20px 24px;background:#fff}
1071
+ .realiiz-md__note{padding:4px 2px}
1072
+ .realiiz-md--fs{position:fixed;inset:0;z-index:60;background:#fff;padding:14px 22px 22px;overflow:auto;display:flex;flex-direction:column}
1073
+ .realiiz-md--fs .realiiz-md__panel{flex:1;min-height:0}
1074
+ .realiiz-md--fs .realiiz-md__ta,.realiiz-md--fs .realiiz-md__preview{flex:1;min-height:0;overflow:auto;resize:none}
1075
+ .realiiz-prose{max-width:68ch;line-height:1.65}
1076
+ .realiiz-chips{display:flex;flex-wrap:wrap;gap:6px;align-items:center;min-height:40px;padding:5px 8px;background:#fff;border:1px solid var(--_border);border-radius:var(--_radius)}
1077
+ .realiiz-chips:focus-within{border-color:var(--_accent);box-shadow:var(--_ring)}
1078
+ .realiiz-chip{display:inline-flex;align-items:center;gap:4px;padding:2px 4px 2px 9px;border:1px solid var(--_border);border-radius:999px;font-size:.8rem}
1079
+ .realiiz-chip button{font:inherit;line-height:1;width:18px;height:18px;border-radius:50%;border:0;background:transparent;color:var(--_muted);cursor:pointer}
1080
+ .realiiz-chips__input{flex:1;min-width:120px;border:0;background:transparent;font:inherit;color:inherit;outline:none;padding:4px}
1081
+ .realiiz-chips__count{margin-left:auto;color:var(--_muted);font-size:.75rem}
1082
+ .realiiz-gauge{display:inline-flex;align-items:center;gap:10px;font-size:.75rem;color:var(--_muted)}
1083
+ .realiiz-gauge__bar{position:relative;width:140px;height:6px;border-radius:3px;background:var(--_border);overflow:hidden}
1084
+ .realiiz-gauge__zone{position:absolute;top:0;bottom:0;background:rgba(21,128,61,.28)}
1085
+ .realiiz-gauge__fill{position:absolute;top:0;bottom:0;left:0;background:var(--_muted);border-radius:3px;transition:width .12s}
1086
+ .realiiz-gauge__text b{font-weight:600;color:inherit}
1087
+ .realiiz-gauge--ok{color:#15803d}.realiiz-gauge--ok .realiiz-gauge__fill{background:#15803d}
1088
+ .realiiz-gauge--over{color:var(--_danger)}.realiiz-gauge--over .realiiz-gauge__fill{background:var(--_danger)}
430
1089
  `;
431
1090
 
432
1091
  // src/forms-ui/values.ts
@@ -436,10 +1095,13 @@ function toControl(spec, raw) {
436
1095
  switch (spec.input) {
437
1096
  case "boolean":
438
1097
  return Boolean(raw);
439
- case "date":
440
- return raw instanceof Date ? raw.toISOString().slice(0, 10) : String(raw);
1098
+ case "date": {
1099
+ if (raw instanceof Date) return raw.toISOString().slice(0, 10);
1100
+ const s = String(raw);
1101
+ return /^\d{4}-\d{2}-\d{2}/.test(s) ? s.slice(0, 10) : s;
1102
+ }
441
1103
  case "tags":
442
- return Array.isArray(raw) ? raw.join(", ") : String(raw);
1104
+ return Array.isArray(raw) ? raw.map(String) : String(raw).split(",").map((t) => t.trim()).filter(Boolean);
443
1105
  default:
444
1106
  return String(raw);
445
1107
  }
@@ -460,6 +1122,10 @@ function toFrontmatter(specs, state, hiddenValues) {
460
1122
  frontmatter[spec.name] = PENDING_UPLOAD_PREFIX + v.name;
461
1123
  continue;
462
1124
  }
1125
+ if (Array.isArray(v)) {
1126
+ if (v.length || spec.required) frontmatter[spec.name] = v;
1127
+ continue;
1128
+ }
463
1129
  const s = typeof v === "string" ? v.trim() : "";
464
1130
  if (s === "") {
465
1131
  if (spec.required) frontmatter[spec.name] = spec.input === "tags" ? [] : "";
@@ -480,13 +1146,39 @@ function toFrontmatter(specs, state, hiddenValues) {
480
1146
  }
481
1147
  return { frontmatter, files };
482
1148
  }
483
- function ContentForm({ entry, initialValues = {}, initialBody = "", onSubmit, onCancel, submitLabel = "Save", className, showBody = true, bodyLabel = "Content" }) {
1149
+ var LEAVE_MESSAGE = "You have unsaved changes. Leave this page and lose them?";
1150
+ function slugify(s) {
1151
+ return s.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1152
+ }
1153
+ function ContentForm({ entry, initialValues = {}, initialBody = "", onSubmit, onCancel, submitLabel = "Save", className, showBody = true, bodyLabel = "Content", renderPreview, previewClassName, termComponent, stickyBar = false, status, barExtra, footer, renderLabel, derive, guardUnsaved = true }) {
484
1154
  const formId = react.useId();
485
1155
  const specs = react.useMemo(() => walkSchema(entry.schema, entry), [entry]);
486
1156
  const visible = react.useMemo(() => specs.filter((s) => s.role !== "hidden" && s.role !== "body"), [specs]);
487
1157
  const bodySpec = specs.find((s) => s.role === "body");
488
- const [state, setState] = react.useState(() => Object.fromEntries(specs.map((s) => [s.name, toControl(s, initialValues[s.name])])));
1158
+ const sections = react.useMemo(() => {
1159
+ const groups = entry.groups ?? [];
1160
+ const grouped = new Set(groups.flatMap((g) => g.fields));
1161
+ const byName = new Map(visible.map((s) => [s.name, s]));
1162
+ const main = { label: entry.singular ?? entry.label, fields: visible.filter((s) => !grouped.has(s.name)) };
1163
+ const rest = groups.map((g) => ({ label: g.label, fields: g.fields.map((n) => byName.get(n)).filter((s) => Boolean(s)) })).filter((g) => g.fields.length > 0);
1164
+ return [main, ...rest];
1165
+ }, [visible, entry.groups, entry.label, entry.singular]);
1166
+ const initialState = react.useMemo(() => Object.fromEntries(specs.map((s) => [s.name, toControl(s, initialValues[s.name])])), [specs, initialValues]);
1167
+ const [state, setState] = react.useState(initialState);
1168
+ const [deriving, setDeriving] = react.useState(() => Boolean(derive && !initialState[derive.target]));
1169
+ const update = (name, v) => setState((st) => {
1170
+ const next = { ...st, [name]: v };
1171
+ if (derive && deriving && name === derive.source && typeof v === "string") {
1172
+ next[derive.target] = (derive.transform ?? slugify)(v);
1173
+ }
1174
+ return next;
1175
+ });
1176
+ const onFieldChange = (name, v) => {
1177
+ if (derive && deriving && name === derive.target) setDeriving(false);
1178
+ update(name, v);
1179
+ };
489
1180
  const [body, setBody] = react.useState(initialBody);
1181
+ const dirty = body !== initialBody || specs.some((s) => !sameValue(state[s.name], initialState[s.name]));
490
1182
  const [issues, setIssues] = react.useState([]);
491
1183
  const [attempted, setAttempted] = react.useState(false);
492
1184
  const [submitting, setSubmitting] = react.useState(false);
@@ -509,6 +1201,31 @@ function ContentForm({ entry, initialValues = {}, initialBody = "", onSubmit, on
509
1201
  live = false;
510
1202
  };
511
1203
  }, [visible]);
1204
+ const leaving = react.useRef(false);
1205
+ react.useEffect(() => {
1206
+ if (!guardUnsaved || !dirty || submitting) return;
1207
+ const onUnload = (e) => {
1208
+ if (!leaving.current) {
1209
+ e.preventDefault();
1210
+ e.returnValue = LEAVE_MESSAGE;
1211
+ }
1212
+ };
1213
+ const onClick = (e) => {
1214
+ const a = e.target?.closest?.("a[href]");
1215
+ if (!a || a.target === "_blank" || e.defaultPrevented || e.metaKey || e.ctrlKey) return;
1216
+ if (a.origin !== window.location.origin) return;
1217
+ if (!window.confirm(LEAVE_MESSAGE)) {
1218
+ e.preventDefault();
1219
+ e.stopPropagation();
1220
+ } else leaving.current = true;
1221
+ };
1222
+ window.addEventListener("beforeunload", onUnload);
1223
+ document.addEventListener("click", onClick, true);
1224
+ return () => {
1225
+ window.removeEventListener("beforeunload", onUnload);
1226
+ document.removeEventListener("click", onClick, true);
1227
+ };
1228
+ }, [guardUnsaved, dirty, submitting]);
512
1229
  const validate = react.useCallback(() => {
513
1230
  const { frontmatter } = toFrontmatter(specs, state, hiddenValues);
514
1231
  const result = entry.schema.safeParse(frontmatter);
@@ -537,9 +1254,9 @@ function ContentForm({ entry, initialValues = {}, initialBody = "", onSubmit, on
537
1254
  /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "This can't be saved yet." }),
538
1255
  /* @__PURE__ */ jsxRuntime.jsx("ul", { children: formLevel.map((i, n) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: i.message }, n)) })
539
1256
  ] }),
540
- /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { children: [
541
- /* @__PURE__ */ jsxRuntime.jsx("legend", { children: entry.label }),
542
- visible.map((s) => /* @__PURE__ */ jsxRuntime.jsx(
1257
+ sections.map((sec) => /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { children: [
1258
+ /* @__PURE__ */ jsxRuntime.jsx("legend", { children: sec.label }),
1259
+ sec.fields.map((s) => /* @__PURE__ */ jsxRuntime.jsx(
543
1260
  Field,
544
1261
  {
545
1262
  formId,
@@ -548,37 +1265,51 @@ function ContentForm({ entry, initialValues = {}, initialBody = "", onSubmit, on
548
1265
  issues: byField.get(s.name) ?? [],
549
1266
  current: typeof initialValues[s.name] === "string" ? initialValues[s.name] : void 0,
550
1267
  options: optionsFor(s),
551
- onChange: (v) => setState((st) => ({ ...st, [s.name]: v })),
1268
+ renderLabel,
1269
+ onChange: (v) => onFieldChange(s.name, v),
552
1270
  onBlur: () => {
553
1271
  if (attempted) validate();
554
1272
  }
555
1273
  },
556
1274
  s.name
557
1275
  ))
558
- ] }),
1276
+ ] }, sec.label)),
559
1277
  (showBody || bodySpec) && /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { children: [
560
1278
  /* @__PURE__ */ jsxRuntime.jsx("legend", { children: bodySpec?.label ?? bodyLabel }),
561
- /* @__PURE__ */ jsxRuntime.jsx("label", { className: "realiiz-form__label", htmlFor: `${formId}-body`, children: bodySpec?.label ?? bodyLabel }),
1279
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "realiiz-form__label realiiz-sr-only", htmlFor: `${formId}-body`, children: bodySpec?.label ?? bodyLabel }),
562
1280
  /* @__PURE__ */ jsxRuntime.jsx(
563
- "textarea",
1281
+ MarkdownEditor,
564
1282
  {
565
1283
  id: `${formId}-body`,
566
- name: "body",
567
- className: "realiiz-form__control realiiz-form__control--body",
568
1284
  value: body,
1285
+ onChange: setBody,
569
1286
  placeholder: bodySpec?.placeholder ?? "Write in Markdown. Headings with ##, links as [text](url).",
570
- onChange: (e) => setBody(e.target.value)
1287
+ renderPreview,
1288
+ previewClassName,
1289
+ termComponent
571
1290
  }
572
1291
  )
573
1292
  ] }),
574
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-form__actions", children: [
1293
+ footer && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "realiiz-form__footer", children: footer }),
1294
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: stickyBar ? "realiiz-form__bar" : "realiiz-form__actions", "data-dirty": dirty || void 0, children: [
1295
+ stickyBar && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-form__status", role: "status", children: status ?? (dirty ? "Unsaved changes" : "No changes yet") }),
1296
+ stickyBar && barExtra,
575
1297
  onCancel && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "realiiz-form__button", onClick: onCancel, children: "Cancel" }),
576
1298
  /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", className: "realiiz-form__button realiiz-form__button--primary", disabled: submitting, children: submitting ? `${submitLabel}\u2026` : submitLabel })
577
1299
  ] })
578
1300
  ] });
579
1301
  }
1302
+ function sameValue(a, b) {
1303
+ if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => v === b[i]);
1304
+ return a === b;
1305
+ }
580
1306
 
1307
+ exports.ChipsInput = ChipsInput;
581
1308
  exports.ContentForm = ContentForm;
1309
+ exports.LengthGauge = LengthGauge;
1310
+ exports.MarkdownEditor = MarkdownEditor;
582
1311
  exports.PENDING_UPLOAD_PREFIX = PENDING_UPLOAD_PREFIX;
1312
+ exports.TOOLS = TOOLS;
1313
+ exports.slugify = slugify;
583
1314
  //# sourceMappingURL=index.cjs.map
584
1315
  //# sourceMappingURL=index.cjs.map