@maykonpaulo/maestro-admin 0.2.0 → 0.3.0-next.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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/components/MaestroAdmin.tsx
2
- import { useMemo as useMemo3, useState as useState9 } from "react";
2
+ import { useMemo as useMemo3, useState as useState10 } from "react";
3
3
 
4
4
  // src/client/MaestroClient.ts
5
5
  var MaestroApiError = class extends Error {
@@ -294,6 +294,12 @@ var EN = {
294
294
  entitiesFooter: (count) => `${count} entities \xB7 Maestro`,
295
295
  yes: "true",
296
296
  no: "false",
297
+ jsonFormMode: "Fields",
298
+ jsonRawMode: "JSON",
299
+ jsonInvalid: "Invalid JSON \u2014 fix it to apply this change.",
300
+ emptyObject: "(empty)",
301
+ addItem: "Add item",
302
+ removeItem: "Remove item",
297
303
  searchMenu: "Search menu\u2026",
298
304
  recents: "Recents",
299
305
  menus: "Menus",
@@ -334,6 +340,12 @@ var PT_BR = {
334
340
  entitiesFooter: (count) => `${count} entidades \xB7 Maestro`,
335
341
  yes: "sim",
336
342
  no: "n\xE3o",
343
+ jsonFormMode: "Campos",
344
+ jsonRawMode: "JSON",
345
+ jsonInvalid: "JSON inv\xE1lido \u2014 corrija para aplicar esta altera\xE7\xE3o.",
346
+ emptyObject: "(vazio)",
347
+ addItem: "Adicionar item",
348
+ removeItem: "Remover item",
337
349
  searchMenu: "Buscar no menu\u2026",
338
350
  recents: "Recentes",
339
351
  menus: "Menus",
@@ -595,8 +607,58 @@ function RelationValue({ field, value }) {
595
607
  );
596
608
  }
597
609
 
610
+ // src/components/ObjectValue.tsx
611
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
612
+ var ISO_DATETIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
613
+ var MAX_DEPTH = 4;
614
+ function isPlainObject(value) {
615
+ return typeof value === "object" && value !== null && !Array.isArray(value);
616
+ }
617
+ function Primitive({ value }) {
618
+ if (value === null || value === void 0) {
619
+ return /* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: "\u2014" });
620
+ }
621
+ if (typeof value === "boolean" || typeof value === "number") {
622
+ return /* @__PURE__ */ jsx4("code", { className: "text-xs font-medium text-indigo-700", children: String(value) });
623
+ }
624
+ const text = String(value);
625
+ if (ISO_DATETIME.test(text)) {
626
+ const d = new Date(text);
627
+ if (!Number.isNaN(d.getTime())) {
628
+ return /* @__PURE__ */ jsx4("span", { className: "whitespace-nowrap text-slate-700", children: d.toLocaleString() });
629
+ }
630
+ }
631
+ return /* @__PURE__ */ jsx4("span", { className: "break-all text-slate-700", children: text });
632
+ }
633
+ function ObjectValue({ value, depth = 0 }) {
634
+ const t = useT();
635
+ if (!isPlainObject(value) && !Array.isArray(value)) {
636
+ return /* @__PURE__ */ jsx4(Primitive, { value });
637
+ }
638
+ if (depth >= MAX_DEPTH) {
639
+ return /* @__PURE__ */ jsx4("code", { className: "block max-w-md overflow-x-auto rounded bg-slate-50 px-1.5 py-0.5 text-xs text-slate-700", children: JSON.stringify(value) });
640
+ }
641
+ const entries = Array.isArray(value) ? value.map((v, i) => [String(i), v]) : Object.entries(value);
642
+ if (entries.length === 0) {
643
+ return /* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: t.emptyObject });
644
+ }
645
+ return /* @__PURE__ */ jsx4(
646
+ "dl",
647
+ {
648
+ className: depth === 0 ? "w-full divide-y divide-slate-100 rounded-md border border-slate-200 bg-slate-50/50" : "w-full divide-y divide-slate-100 border-l-2 border-slate-200 pl-2",
649
+ children: entries.map(([key, entry]) => (
650
+ // flex-wrap + basis: deeply nested values drop to their own full-width line instead of squeezing.
651
+ /* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-start gap-x-3 gap-y-1 px-3 py-1.5", children: [
652
+ /* @__PURE__ */ jsx4("dt", { className: "w-36 shrink-0 truncate pt-px text-xs font-medium text-slate-500", title: key, children: key }),
653
+ /* @__PURE__ */ jsx4("dd", { className: "min-w-0 flex-1 basis-56 text-sm", children: isPlainObject(entry) || Array.isArray(entry) ? /* @__PURE__ */ jsx4(ObjectValue, { value: entry, depth: depth + 1 }) : /* @__PURE__ */ jsx4(Primitive, { value: entry }) })
654
+ ] }, key)
655
+ ))
656
+ }
657
+ );
658
+ }
659
+
598
660
  // src/components/FieldValue.tsx
599
- import { jsx as jsx4 } from "react/jsx-runtime";
661
+ import { jsx as jsx5 } from "react/jsx-runtime";
600
662
  function enumLabel(field, value) {
601
663
  const opt = field.enumOptions?.find((o) => o.value === value);
602
664
  return opt ? opt.label : String(value);
@@ -604,34 +666,40 @@ function enumLabel(field, value) {
604
666
  function FieldValue({ field, value, compact = false }) {
605
667
  const t = useT();
606
668
  if (value === null || value === void 0 || value === "") {
607
- return /* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: "\u2014" });
669
+ return /* @__PURE__ */ jsx5("span", { className: "text-slate-400", children: "\u2014" });
608
670
  }
609
671
  if (field.sensitive) {
610
- return /* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: "\u2022\u2022\u2022\u2022\u2022\u2022" });
672
+ return /* @__PURE__ */ jsx5("span", { className: "text-slate-400", children: "\u2022\u2022\u2022\u2022\u2022\u2022" });
611
673
  }
612
674
  switch (field.type) {
613
675
  case "boolean":
614
- return value ? /* @__PURE__ */ jsx4(Badge, { tone: "green", children: t.yes }) : /* @__PURE__ */ jsx4(Badge, { tone: "slate", children: t.no });
676
+ return value ? /* @__PURE__ */ jsx5(Badge, { tone: "green", children: t.yes }) : /* @__PURE__ */ jsx5(Badge, { tone: "slate", children: t.no });
615
677
  case "enum":
616
- return /* @__PURE__ */ jsx4(Badge, { tone: "indigo", children: enumLabel(field, value) });
678
+ return /* @__PURE__ */ jsx5(Badge, { tone: "indigo", children: enumLabel(field, value) });
617
679
  case "relation":
618
- return field.relationEntity ? /* @__PURE__ */ jsx4(RelationValue, { field, value }) : /* @__PURE__ */ jsx4("span", { className: "text-slate-700", children: String(value) });
680
+ return field.relationEntity ? /* @__PURE__ */ jsx5(RelationValue, { field, value }) : /* @__PURE__ */ jsx5("span", { className: "text-slate-700", children: String(value) });
619
681
  case "date":
620
682
  case "datetime": {
621
683
  const d = new Date(String(value));
622
- return /* @__PURE__ */ jsx4("span", { className: "whitespace-nowrap", children: Number.isNaN(d.getTime()) ? String(value) : d.toLocaleString() });
684
+ return /* @__PURE__ */ jsx5("span", { className: "whitespace-nowrap", children: Number.isNaN(d.getTime()) ? String(value) : d.toLocaleString() });
623
685
  }
624
686
  case "json":
625
- return /* @__PURE__ */ jsx4("code", { className: `block overflow-x-auto rounded bg-slate-50 px-1.5 py-0.5 text-xs text-slate-700 ${compact ? "max-w-xs truncate" : "max-w-md"}`, children: JSON.stringify(value) });
687
+ if (compact) {
688
+ return /* @__PURE__ */ jsx5("code", { className: "block max-w-xs truncate overflow-x-auto rounded bg-slate-50 px-1.5 py-0.5 text-xs text-slate-700", children: JSON.stringify(value) });
689
+ }
690
+ if (typeof value === "object") {
691
+ return /* @__PURE__ */ jsx5(ObjectValue, { value });
692
+ }
693
+ return /* @__PURE__ */ jsx5("code", { className: "block max-w-md overflow-x-auto rounded bg-slate-50 px-1.5 py-0.5 text-xs text-slate-700", children: JSON.stringify(value) });
626
694
  default:
627
- return /* @__PURE__ */ jsx4("span", { className: `text-slate-700 ${compact ? "block max-w-64 truncate" : ""}`, children: String(value) });
695
+ return /* @__PURE__ */ jsx5("span", { className: `text-slate-700 ${compact ? "block max-w-64 truncate" : ""}`, children: String(value) });
628
696
  }
629
697
  }
630
698
 
631
699
  // src/components/menu.tsx
632
700
  import { useEffect as useEffect4, useLayoutEffect, useRef as useRef2, useState as useState4 } from "react";
633
701
  import { createPortal } from "react-dom";
634
- import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
702
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
635
703
  function PopMenu({ anchor, items, onClose }) {
636
704
  const ref = useRef2(null);
637
705
  const [pos, setPos] = useState4(anchor);
@@ -663,14 +731,14 @@ function PopMenu({ anchor, items, onClose }) {
663
731
  };
664
732
  }, [onClose]);
665
733
  return createPortal(
666
- /* @__PURE__ */ jsx5(
734
+ /* @__PURE__ */ jsx6(
667
735
  "div",
668
736
  {
669
737
  ref,
670
738
  role: "menu",
671
739
  style: { position: "fixed", left: pos.x, top: pos.y, zIndex: 50 },
672
740
  className: "min-w-44 rounded-md border border-slate-200 bg-white py-1 shadow-lg",
673
- children: items.map((item) => /* @__PURE__ */ jsx5(
741
+ children: items.map((item) => /* @__PURE__ */ jsx6(
674
742
  "button",
675
743
  {
676
744
  role: "menuitem",
@@ -690,8 +758,8 @@ function PopMenu({ anchor, items, onClose }) {
690
758
  }
691
759
  function MenuButton({ items, label }) {
692
760
  const [anchor, setAnchor] = useState4(void 0);
693
- return /* @__PURE__ */ jsxs4(Fragment2, { children: [
694
- /* @__PURE__ */ jsx5(
761
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
762
+ /* @__PURE__ */ jsx6(
695
763
  "button",
696
764
  {
697
765
  type: "button",
@@ -705,7 +773,7 @@ function MenuButton({ items, label }) {
705
773
  children: "\u22EE"
706
774
  }
707
775
  ),
708
- anchor && /* @__PURE__ */ jsx5(PopMenu, { anchor, items, onClose: () => setAnchor(void 0) })
776
+ anchor && /* @__PURE__ */ jsx6(PopMenu, { anchor, items, onClose: () => setAnchor(void 0) })
709
777
  ] });
710
778
  }
711
779
 
@@ -729,7 +797,7 @@ async function copyText(text) {
729
797
  }
730
798
 
731
799
  // src/components/EntityList.tsx
732
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
800
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
733
801
  var PAGE_SIZE = 20;
734
802
  function filterFor(field, value) {
735
803
  if (value === null || value === void 0) return { field: field.name, operator: "isNull" };
@@ -843,14 +911,14 @@ function EntityList({
843
911
  return items;
844
912
  };
845
913
  const totalPages = data?.totalPages ?? (data ? Math.max(1, Math.ceil(data.total / PAGE_SIZE)) : 1);
846
- return /* @__PURE__ */ jsxs5("section", { className: "space-y-4", children: [
847
- /* @__PURE__ */ jsxs5("header", { className: "flex flex-wrap items-center justify-between gap-3", children: [
848
- /* @__PURE__ */ jsxs5("div", { children: [
849
- /* @__PURE__ */ jsx6("h1", { className: "text-lg font-semibold text-slate-900", children: entity.label.plural }),
850
- data && /* @__PURE__ */ jsx6("p", { className: "text-sm text-slate-500", children: t.records(data.total) })
914
+ return /* @__PURE__ */ jsxs6("section", { className: "space-y-4", children: [
915
+ /* @__PURE__ */ jsxs6("header", { className: "flex flex-wrap items-center justify-between gap-3", children: [
916
+ /* @__PURE__ */ jsxs6("div", { children: [
917
+ /* @__PURE__ */ jsx7("h1", { className: "text-lg font-semibold text-slate-900", children: entity.label.plural }),
918
+ data && /* @__PURE__ */ jsx7("p", { className: "text-sm text-slate-500", children: t.records(data.total) })
851
919
  ] }),
852
- /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2", children: [
853
- searchFields.length > 0 && /* @__PURE__ */ jsx6(
920
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", children: [
921
+ searchFields.length > 0 && /* @__PURE__ */ jsx7(
854
922
  "input",
855
923
  {
856
924
  type: "search",
@@ -863,15 +931,15 @@ function EntityList({
863
931
  className: "rounded-md border border-slate-300 px-3 py-1.5 text-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
864
932
  }
865
933
  ),
866
- entity.capabilities.create && /* @__PURE__ */ jsx6(Button, { variant: "primary", onClick: onCreate, children: t.newRecord })
934
+ entity.capabilities.create && /* @__PURE__ */ jsx7(Button, { variant: "primary", onClick: onCreate, children: t.newRecord })
867
935
  ] })
868
936
  ] }),
869
- filters.length > 0 && /* @__PURE__ */ jsxs5("div", { className: "flex flex-wrap items-center gap-2 text-sm", children: [
870
- /* @__PURE__ */ jsxs5("span", { className: "text-slate-500", children: [
937
+ filters.length > 0 && /* @__PURE__ */ jsxs6("div", { className: "flex flex-wrap items-center gap-2 text-sm", children: [
938
+ /* @__PURE__ */ jsxs6("span", { className: "text-slate-500", children: [
871
939
  t.filters,
872
940
  ":"
873
941
  ] }),
874
- filters.map((f) => /* @__PURE__ */ jsxs5(
942
+ filters.map((f) => /* @__PURE__ */ jsxs6(
875
943
  "button",
876
944
  {
877
945
  onClick: () => {
@@ -882,12 +950,12 @@ function EntityList({
882
950
  className: "inline-flex items-center gap-1 rounded-full bg-indigo-50 px-2.5 py-0.5 text-xs font-medium text-indigo-700 hover:bg-indigo-100",
883
951
  children: [
884
952
  filterChipText(entity, f),
885
- /* @__PURE__ */ jsx6("span", { "aria-hidden": true, children: "\xD7" })
953
+ /* @__PURE__ */ jsx7("span", { "aria-hidden": true, children: "\xD7" })
886
954
  ]
887
955
  },
888
956
  f.field
889
957
  )),
890
- /* @__PURE__ */ jsx6(
958
+ /* @__PURE__ */ jsx7(
891
959
  "button",
892
960
  {
893
961
  onClick: () => {
@@ -899,10 +967,10 @@ function EntityList({
899
967
  }
900
968
  )
901
969
  ] }),
902
- error && /* @__PURE__ */ jsx6(ErrorBanner, { error }),
903
- loading && !data ? /* @__PURE__ */ jsx6(Spinner, { label: t.loading }) : data && data.records.length === 0 ? /* @__PURE__ */ jsx6(EmptyState, { title: t.noRecords, hint: search || filters.length ? t.tryDifferentSearch : void 0 }) : /* @__PURE__ */ jsx6("div", { className: "overflow-x-auto rounded-lg border border-slate-200", children: /* @__PURE__ */ jsxs5("table", { className: "min-w-full divide-y divide-slate-200 text-sm", children: [
904
- /* @__PURE__ */ jsx6("thead", { className: "bg-slate-50", children: /* @__PURE__ */ jsxs5("tr", { children: [
905
- columns.map((f) => /* @__PURE__ */ jsxs5(
970
+ error && /* @__PURE__ */ jsx7(ErrorBanner, { error }),
971
+ loading && !data ? /* @__PURE__ */ jsx7(Spinner, { label: t.loading }) : data && data.records.length === 0 ? /* @__PURE__ */ jsx7(EmptyState, { title: t.noRecords, hint: search || filters.length ? t.tryDifferentSearch : void 0 }) : /* @__PURE__ */ jsx7("div", { className: "overflow-x-auto rounded-lg border border-slate-200", children: /* @__PURE__ */ jsxs6("table", { className: "min-w-full divide-y divide-slate-200 text-sm", children: [
972
+ /* @__PURE__ */ jsx7("thead", { className: "bg-slate-50", children: /* @__PURE__ */ jsxs6("tr", { children: [
973
+ columns.map((f) => /* @__PURE__ */ jsxs6(
906
974
  "th",
907
975
  {
908
976
  onClick: () => f.sortable && toggleSort(f.name),
@@ -914,38 +982,38 @@ function EntityList({
914
982
  },
915
983
  f.name
916
984
  )),
917
- /* @__PURE__ */ jsx6("th", { className: "px-3 py-2 text-right font-medium text-slate-600", children: t.actions })
985
+ /* @__PURE__ */ jsx7("th", { className: "px-3 py-2 text-right font-medium text-slate-600", children: t.actions })
918
986
  ] }) }),
919
- /* @__PURE__ */ jsx6("tbody", { className: "divide-y divide-slate-100 bg-white", children: data?.records.map((record, i) => {
987
+ /* @__PURE__ */ jsx7("tbody", { className: "divide-y divide-slate-100 bg-white", children: data?.records.map((record, i) => {
920
988
  const id = String(record[entity.primaryKey]);
921
- return /* @__PURE__ */ jsxs5("tr", { className: "hover:bg-slate-50", children: [
922
- columns.map((f) => /* @__PURE__ */ jsx6(
989
+ return /* @__PURE__ */ jsxs6("tr", { className: "hover:bg-slate-50", children: [
990
+ columns.map((f) => /* @__PURE__ */ jsx7(
923
991
  "td",
924
992
  {
925
993
  className: "cursor-pointer px-3 py-2 align-top",
926
994
  onClick: (e) => setCellMenu({ anchor: { x: e.clientX, y: e.clientY }, field: f, record }),
927
995
  onDoubleClick: () => onOpen(id),
928
- children: /* @__PURE__ */ jsx6(FieldValue, { field: f, value: record[f.name], compact: true })
996
+ children: /* @__PURE__ */ jsx7(FieldValue, { field: f, value: record[f.name], compact: true })
929
997
  },
930
998
  f.name
931
999
  )),
932
- /* @__PURE__ */ jsx6("td", { className: "whitespace-nowrap px-3 py-2 text-right", children: /* @__PURE__ */ jsx6(MenuButton, { label: t.actions, items: rowMenuItems(record) }) })
1000
+ /* @__PURE__ */ jsx7("td", { className: "whitespace-nowrap px-3 py-2 text-right", children: /* @__PURE__ */ jsx7(MenuButton, { label: t.actions, items: rowMenuItems(record) }) })
933
1001
  ] }, id || i);
934
1002
  }) })
935
1003
  ] }) }),
936
- cellMenu && /* @__PURE__ */ jsx6(PopMenu, { anchor: cellMenu.anchor, items: cellMenuItems(cellMenu), onClose: () => setCellMenu(void 0) }),
937
- copied && /* @__PURE__ */ jsx6("div", { className: "fixed bottom-4 right-4 z-50 rounded-md bg-slate-900 px-3 py-1.5 text-sm text-white shadow-lg", children: t.copied }),
938
- data && totalPages > 1 && /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-end gap-2 text-sm", children: [
939
- /* @__PURE__ */ jsx6(Button, { variant: "secondary", disabled: page <= 1, onClick: () => setPage((p) => p - 1), children: t.previous }),
940
- /* @__PURE__ */ jsx6("span", { className: "text-slate-500", children: t.page(page, totalPages) }),
941
- /* @__PURE__ */ jsx6(Button, { variant: "secondary", disabled: page >= totalPages, onClick: () => setPage((p) => p + 1), children: t.next })
1004
+ cellMenu && /* @__PURE__ */ jsx7(PopMenu, { anchor: cellMenu.anchor, items: cellMenuItems(cellMenu), onClose: () => setCellMenu(void 0) }),
1005
+ copied && /* @__PURE__ */ jsx7("div", { className: "fixed bottom-4 right-4 z-50 rounded-md bg-slate-900 px-3 py-1.5 text-sm text-white shadow-lg", children: t.copied }),
1006
+ data && totalPages > 1 && /* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-end gap-2 text-sm", children: [
1007
+ /* @__PURE__ */ jsx7(Button, { variant: "secondary", disabled: page <= 1, onClick: () => setPage((p) => p - 1), children: t.previous }),
1008
+ /* @__PURE__ */ jsx7("span", { className: "text-slate-500", children: t.page(page, totalPages) }),
1009
+ /* @__PURE__ */ jsx7(Button, { variant: "secondary", disabled: page >= totalPages, onClick: () => setPage((p) => p + 1), children: t.next })
942
1010
  ] })
943
1011
  ] });
944
1012
  }
945
1013
 
946
1014
  // src/components/EntityDetail.tsx
947
1015
  import { useState as useState6 } from "react";
948
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1016
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
949
1017
  function EntityDetail({
950
1018
  entity,
951
1019
  id,
@@ -986,34 +1054,34 @@ function EntityDetail({
986
1054
  if (entity.capabilities.delete) {
987
1055
  menuItems.push({ id: "remove", label: `\u{1F5D1} ${t.remove}`, danger: true, onSelect: () => void onDelete() });
988
1056
  }
989
- return /* @__PURE__ */ jsxs6("section", { className: "space-y-4", children: [
990
- /* @__PURE__ */ jsxs6("header", { className: "flex items-center justify-between gap-3", children: [
991
- /* @__PURE__ */ jsxs6("div", { children: [
992
- /* @__PURE__ */ jsxs6("button", { onClick: onBack, className: "text-sm text-indigo-600 hover:underline", children: [
1057
+ return /* @__PURE__ */ jsxs7("section", { className: "space-y-4", children: [
1058
+ /* @__PURE__ */ jsxs7("header", { className: "flex items-center justify-between gap-3", children: [
1059
+ /* @__PURE__ */ jsxs7("div", { children: [
1060
+ /* @__PURE__ */ jsxs7("button", { onClick: onBack, className: "text-sm text-indigo-600 hover:underline", children: [
993
1061
  "\u2190 ",
994
1062
  entity.label.plural
995
1063
  ] }),
996
- /* @__PURE__ */ jsxs6("h1", { className: "text-lg font-semibold text-slate-900", children: [
1064
+ /* @__PURE__ */ jsxs7("h1", { className: "text-lg font-semibold text-slate-900", children: [
997
1065
  entity.label.singular,
998
1066
  " ",
999
1067
  data ? String(data[entity.displayField] ?? id) : ""
1000
1068
  ] })
1001
1069
  ] }),
1002
- /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1", children: [
1003
- entity.capabilities.update && /* @__PURE__ */ jsx7(Button, { variant: "primary", onClick: onEdit, children: t.edit }),
1004
- /* @__PURE__ */ jsx7(MenuButton, { label: t.actions, items: menuItems })
1070
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-1", children: [
1071
+ entity.capabilities.update && /* @__PURE__ */ jsx8(Button, { variant: "primary", onClick: onEdit, children: t.edit }),
1072
+ /* @__PURE__ */ jsx8(MenuButton, { label: t.actions, items: menuItems })
1005
1073
  ] })
1006
1074
  ] }),
1007
- error && /* @__PURE__ */ jsx7(ErrorBanner, { error }),
1008
- loading && !data ? /* @__PURE__ */ jsx7(Spinner, {}) : data && /* @__PURE__ */ jsx7("dl", { className: "divide-y divide-slate-100 rounded-lg border border-slate-200 bg-white", children: fields.map((f) => {
1075
+ error && /* @__PURE__ */ jsx8(ErrorBanner, { error }),
1076
+ loading && !data ? /* @__PURE__ */ jsx8(Spinner, {}) : data && /* @__PURE__ */ jsx8("dl", { className: "divide-y divide-slate-100 rounded-lg border border-slate-200 bg-white", children: fields.map((f) => {
1009
1077
  const value = data[f.name];
1010
1078
  const isRelation = f.type === "relation" && f.relationEntity && value !== null && value !== void 0;
1011
1079
  const target = isRelation ? entities.find((e) => e.id === f.relationEntity) : void 0;
1012
- return /* @__PURE__ */ jsxs6("div", { className: "grid grid-cols-3 gap-4 px-4 py-3", children: [
1013
- /* @__PURE__ */ jsx7("dt", { className: "text-sm font-medium text-slate-500", children: f.label }),
1014
- /* @__PURE__ */ jsxs6("dd", { className: "col-span-2 flex items-center gap-2 text-sm", children: [
1015
- /* @__PURE__ */ jsx7(FieldValue, { field: f, value }),
1016
- isRelation && /* @__PURE__ */ jsxs6(
1080
+ return /* @__PURE__ */ jsxs7("div", { className: "grid grid-cols-3 gap-4 px-4 py-3", children: [
1081
+ /* @__PURE__ */ jsx8("dt", { className: "text-sm font-medium text-slate-500", children: f.label }),
1082
+ /* @__PURE__ */ jsxs7("dd", { className: "col-span-2 flex items-center gap-2 text-sm", children: [
1083
+ /* @__PURE__ */ jsx8(FieldValue, { field: f, value }),
1084
+ isRelation && /* @__PURE__ */ jsxs7(
1017
1085
  "button",
1018
1086
  {
1019
1087
  onClick: () => onNavigate(f.relationEntity, String(value)),
@@ -1027,15 +1095,270 @@ function EntityDetail({
1027
1095
  ] })
1028
1096
  ] }, f.name);
1029
1097
  }) }),
1030
- copied && /* @__PURE__ */ jsx7("div", { className: "fixed bottom-4 right-4 z-50 rounded-md bg-slate-900 px-3 py-1.5 text-sm text-white shadow-lg", children: t.copied })
1098
+ copied && /* @__PURE__ */ jsx8("div", { className: "fixed bottom-4 right-4 z-50 rounded-md bg-slate-900 px-3 py-1.5 text-sm text-white shadow-lg", children: t.copied })
1031
1099
  ] });
1032
1100
  }
1033
1101
 
1034
1102
  // src/components/EntityForm.tsx
1035
- import { useEffect as useEffect5, useState as useState7 } from "react";
1103
+ import { useEffect as useEffect6, useState as useState8 } from "react";
1104
+
1105
+ // src/components/JsonInput.tsx
1106
+ import { useEffect as useEffect5, useRef as useRef3, useState as useState7 } from "react";
1107
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1108
+ var leafInputClass = "w-full rounded-md border border-slate-300 px-2.5 py-1 text-sm text-slate-800 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 disabled:bg-slate-100";
1109
+ var MAX_DEPTH2 = 3;
1110
+ function RawJsonArea({
1111
+ id,
1112
+ value,
1113
+ onChange,
1114
+ disabled,
1115
+ rows = 8,
1116
+ placeholder
1117
+ }) {
1118
+ const t = useT();
1119
+ const [text, setText] = useState7(() => value === null || value === void 0 ? "" : JSON.stringify(value, null, 2));
1120
+ const [invalid, setInvalid] = useState7(false);
1121
+ const lastEmitted = useRef3(value);
1122
+ useEffect5(() => {
1123
+ if (value !== lastEmitted.current) {
1124
+ lastEmitted.current = value;
1125
+ setText(value === null || value === void 0 ? "" : JSON.stringify(value, null, 2));
1126
+ setInvalid(false);
1127
+ }
1128
+ }, [value]);
1129
+ const onText = (raw) => {
1130
+ setText(raw);
1131
+ if (raw.trim() === "") {
1132
+ lastEmitted.current = null;
1133
+ setInvalid(false);
1134
+ onChange(null);
1135
+ return;
1136
+ }
1137
+ try {
1138
+ const parsed = JSON.parse(raw);
1139
+ lastEmitted.current = parsed;
1140
+ setInvalid(false);
1141
+ onChange(parsed);
1142
+ } catch {
1143
+ setInvalid(true);
1144
+ }
1145
+ };
1146
+ return /* @__PURE__ */ jsxs8("div", { className: "space-y-1", children: [
1147
+ /* @__PURE__ */ jsx9(
1148
+ "textarea",
1149
+ {
1150
+ id,
1151
+ rows,
1152
+ value: text,
1153
+ placeholder,
1154
+ disabled,
1155
+ onChange: (e) => onText(e.target.value),
1156
+ "aria-invalid": invalid || void 0,
1157
+ className: `${leafInputClass} font-mono ${invalid ? "border-amber-400 focus:border-amber-500 focus:ring-amber-500" : ""}`
1158
+ }
1159
+ ),
1160
+ invalid && /* @__PURE__ */ jsx9("p", { role: "alert", className: "text-xs text-amber-600", children: t.jsonInvalid })
1161
+ ] });
1162
+ }
1163
+ function LeafEditor({
1164
+ id,
1165
+ value,
1166
+ onChange,
1167
+ disabled,
1168
+ depth
1169
+ }) {
1170
+ if (typeof value === "boolean") {
1171
+ return /* @__PURE__ */ jsx9(
1172
+ "input",
1173
+ {
1174
+ id,
1175
+ type: "checkbox",
1176
+ checked: value,
1177
+ disabled,
1178
+ onChange: (e) => onChange(e.target.checked),
1179
+ className: "mt-1 h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500"
1180
+ }
1181
+ );
1182
+ }
1183
+ if (typeof value === "number") {
1184
+ return /* @__PURE__ */ jsx9(
1185
+ "input",
1186
+ {
1187
+ id,
1188
+ type: "number",
1189
+ value: String(value),
1190
+ disabled,
1191
+ step: "any",
1192
+ onChange: (e) => onChange(e.target.value === "" ? null : Number(e.target.value)),
1193
+ className: leafInputClass
1194
+ }
1195
+ );
1196
+ }
1197
+ if (typeof value === "string") {
1198
+ return /* @__PURE__ */ jsx9(
1199
+ "input",
1200
+ {
1201
+ id,
1202
+ type: "text",
1203
+ value,
1204
+ disabled,
1205
+ onChange: (e) => onChange(e.target.value),
1206
+ className: leafInputClass
1207
+ }
1208
+ );
1209
+ }
1210
+ if (value === null || value === void 0) {
1211
+ return /* @__PURE__ */ jsx9(
1212
+ "input",
1213
+ {
1214
+ id,
1215
+ type: "text",
1216
+ value: "",
1217
+ placeholder: "null",
1218
+ disabled,
1219
+ onChange: (e) => onChange(e.target.value === "" ? null : e.target.value),
1220
+ className: leafInputClass
1221
+ }
1222
+ );
1223
+ }
1224
+ if (isPlainObject(value) && depth < MAX_DEPTH2) {
1225
+ return /* @__PURE__ */ jsx9("div", { className: "rounded-md border border-slate-200", children: /* @__PURE__ */ jsx9(ObjectEditor, { idPrefix: id, value, onChange, disabled, depth: depth + 1 }) });
1226
+ }
1227
+ if (Array.isArray(value) && depth < MAX_DEPTH2) {
1228
+ return /* @__PURE__ */ jsx9("div", { className: "rounded-md border border-slate-200", children: /* @__PURE__ */ jsx9(ArrayEditor, { idPrefix: id, value, onChange, disabled, depth: depth + 1 }) });
1229
+ }
1230
+ return /* @__PURE__ */ jsx9(RawJsonArea, { id, value, onChange, disabled, rows: 3 });
1231
+ }
1232
+ function ArrayEditor({
1233
+ idPrefix,
1234
+ value,
1235
+ onChange,
1236
+ disabled,
1237
+ depth = 0
1238
+ }) {
1239
+ const t = useT();
1240
+ const setAt = (i, next) => onChange(value.map((cur, idx) => idx === i ? next : cur));
1241
+ const removeAt = (i) => onChange(value.filter((_, idx) => idx !== i));
1242
+ const append = () => {
1243
+ const last = value[value.length - 1];
1244
+ const template = last === void 0 || last === null ? null : JSON.parse(JSON.stringify(last));
1245
+ onChange([...value, template]);
1246
+ };
1247
+ return /* @__PURE__ */ jsxs8("div", { className: "divide-y divide-slate-100", children: [
1248
+ value.map((item, i) => {
1249
+ const id = `${idPrefix}-${i}`;
1250
+ return /* @__PURE__ */ jsxs8("div", { className: "flex flex-wrap items-start gap-x-3 gap-y-1.5 px-3 py-2", children: [
1251
+ /* @__PURE__ */ jsx9("label", { htmlFor: id, className: "w-8 shrink-0 pt-1.5 text-right text-xs font-medium text-slate-400", children: i }),
1252
+ !disabled && /* @__PURE__ */ jsx9(
1253
+ "button",
1254
+ {
1255
+ type: "button",
1256
+ title: t.removeItem,
1257
+ "aria-label": `${t.removeItem} ${i}`,
1258
+ onClick: () => removeAt(i),
1259
+ className: "shrink-0 pt-1.5 text-xs text-slate-400 transition-colors hover:text-red-600",
1260
+ children: "\u2715"
1261
+ }
1262
+ ),
1263
+ /* @__PURE__ */ jsx9("div", { className: "min-w-0 flex-1 basis-64", children: /* @__PURE__ */ jsx9(LeafEditor, { id, value: item, onChange: (next) => setAt(i, next), disabled, depth }) })
1264
+ ] }, i);
1265
+ }),
1266
+ value.length === 0 && /* @__PURE__ */ jsx9("p", { className: "px-3 py-2 text-xs text-slate-400", children: t.emptyObject }),
1267
+ !disabled && /* @__PURE__ */ jsx9("div", { className: "px-3 py-1.5", children: /* @__PURE__ */ jsxs8("button", { type: "button", onClick: append, className: "text-xs font-medium text-indigo-600 hover:underline", children: [
1268
+ "+ ",
1269
+ t.addItem
1270
+ ] }) })
1271
+ ] });
1272
+ }
1273
+ function ObjectEditor({
1274
+ idPrefix,
1275
+ value,
1276
+ onChange,
1277
+ disabled,
1278
+ depth = 0
1279
+ }) {
1280
+ const t = useT();
1281
+ const entries = Object.entries(value);
1282
+ if (entries.length === 0) {
1283
+ return /* @__PURE__ */ jsx9("p", { className: "px-3 py-2 text-xs text-slate-400", children: t.emptyObject });
1284
+ }
1285
+ return /* @__PURE__ */ jsx9("div", { className: "divide-y divide-slate-100", children: entries.map(([key, entry]) => {
1286
+ const id = `${idPrefix}-${key}`;
1287
+ return (
1288
+ // flex-wrap + basis: when the label column leaves less than ~16rem for the control (deeply
1289
+ // nested groups), the control wraps to its own full-width line instead of squeezing.
1290
+ /* @__PURE__ */ jsxs8("div", { className: "flex flex-wrap items-start gap-x-3 gap-y-1.5 px-3 py-2", children: [
1291
+ /* @__PURE__ */ jsx9("label", { htmlFor: id, className: "w-36 shrink-0 truncate pt-1.5 text-xs font-medium text-slate-500", title: key, children: key }),
1292
+ /* @__PURE__ */ jsx9("div", { className: "min-w-0 flex-1 basis-64", children: /* @__PURE__ */ jsx9(
1293
+ LeafEditor,
1294
+ {
1295
+ id,
1296
+ value: entry,
1297
+ onChange: (next) => onChange({ ...value, [key]: next }),
1298
+ disabled,
1299
+ depth
1300
+ }
1301
+ ) })
1302
+ ] }, key)
1303
+ );
1304
+ }) });
1305
+ }
1306
+ function JsonInput({
1307
+ field,
1308
+ value,
1309
+ onChange,
1310
+ disabled
1311
+ }) {
1312
+ const t = useT();
1313
+ const [mode, setMode] = useState7(() => isPlainObject(value) ? "form" : "raw");
1314
+ const [modeTouched, setModeTouched] = useState7(false);
1315
+ const structured = isPlainObject(value) || Array.isArray(value);
1316
+ useEffect5(() => {
1317
+ if (!modeTouched && structured) setMode("form");
1318
+ }, [structured, modeTouched]);
1319
+ const pick = (next) => {
1320
+ setModeTouched(true);
1321
+ setMode(next);
1322
+ };
1323
+ const showForm = mode === "form" && structured;
1324
+ const toggleClass = (active) => `rounded px-2 py-0.5 text-xs font-medium transition-colors ${active ? "bg-white text-slate-800 shadow-sm ring-1 ring-slate-300" : "text-slate-500 hover:text-slate-700"}`;
1325
+ return /* @__PURE__ */ jsxs8("div", { className: "rounded-md border border-slate-300 shadow-sm", children: [
1326
+ /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-end gap-1 rounded-t-md border-b border-slate-200 bg-slate-50 px-2 py-1", children: [
1327
+ /* @__PURE__ */ jsx9(
1328
+ "button",
1329
+ {
1330
+ type: "button",
1331
+ className: toggleClass(showForm),
1332
+ disabled: !structured,
1333
+ onClick: () => pick("form"),
1334
+ children: t.jsonFormMode
1335
+ }
1336
+ ),
1337
+ /* @__PURE__ */ jsx9("button", { type: "button", className: toggleClass(!showForm), onClick: () => pick("raw"), children: t.jsonRawMode })
1338
+ ] }),
1339
+ /* @__PURE__ */ jsx9("div", { className: "p-1", children: showForm ? Array.isArray(value) ? /* @__PURE__ */ jsx9(ArrayEditor, { idPrefix: `field-${field.name}`, value, onChange, disabled }) : /* @__PURE__ */ jsx9(
1340
+ ObjectEditor,
1341
+ {
1342
+ idPrefix: `field-${field.name}`,
1343
+ value,
1344
+ onChange,
1345
+ disabled
1346
+ }
1347
+ ) : /* @__PURE__ */ jsx9(
1348
+ RawJsonArea,
1349
+ {
1350
+ id: `field-${field.name}`,
1351
+ value,
1352
+ onChange,
1353
+ disabled,
1354
+ placeholder: field.form.placeholder
1355
+ }
1356
+ ) })
1357
+ ] });
1358
+ }
1036
1359
 
1037
1360
  // src/components/FieldInput.tsx
1038
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1361
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1039
1362
  var inputClass = "w-full rounded-md border border-slate-300 px-3 py-1.5 text-sm text-slate-800 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 disabled:bg-slate-100";
1040
1363
  function FieldInput({
1041
1364
  field,
@@ -1046,7 +1369,7 @@ function FieldInput({
1046
1369
  const id = `field-${field.name}`;
1047
1370
  switch (field.type) {
1048
1371
  case "boolean":
1049
- return /* @__PURE__ */ jsx8(
1372
+ return /* @__PURE__ */ jsx10(
1050
1373
  "input",
1051
1374
  {
1052
1375
  id,
@@ -1062,7 +1385,7 @@ function FieldInput({
1062
1385
  case "float":
1063
1386
  case "decimal":
1064
1387
  case "currency":
1065
- return /* @__PURE__ */ jsx8(
1388
+ return /* @__PURE__ */ jsx10(
1066
1389
  "input",
1067
1390
  {
1068
1391
  id,
@@ -1076,7 +1399,7 @@ function FieldInput({
1076
1399
  }
1077
1400
  );
1078
1401
  case "enum":
1079
- return /* @__PURE__ */ jsxs7(
1402
+ return /* @__PURE__ */ jsxs9(
1080
1403
  "select",
1081
1404
  {
1082
1405
  id,
@@ -1085,13 +1408,13 @@ function FieldInput({
1085
1408
  onChange: (e) => onChange(e.target.value),
1086
1409
  className: inputClass,
1087
1410
  children: [
1088
- /* @__PURE__ */ jsx8("option", { value: "", children: "\u2014" }),
1089
- field.enumOptions?.map((o) => /* @__PURE__ */ jsx8("option", { value: String(o.value), children: o.label }, String(o.value)))
1411
+ /* @__PURE__ */ jsx10("option", { value: "", children: "\u2014" }),
1412
+ field.enumOptions?.map((o) => /* @__PURE__ */ jsx10("option", { value: String(o.value), children: o.label }, String(o.value)))
1090
1413
  ]
1091
1414
  }
1092
1415
  );
1093
1416
  case "date":
1094
- return /* @__PURE__ */ jsx8(
1417
+ return /* @__PURE__ */ jsx10(
1095
1418
  "input",
1096
1419
  {
1097
1420
  id,
@@ -1102,32 +1425,23 @@ function FieldInput({
1102
1425
  className: inputClass
1103
1426
  }
1104
1427
  );
1105
- case "text":
1106
1428
  case "json":
1107
- return /* @__PURE__ */ jsx8(
1429
+ return /* @__PURE__ */ jsx10(JsonInput, { field, value, onChange, disabled });
1430
+ case "text":
1431
+ return /* @__PURE__ */ jsx10(
1108
1432
  "textarea",
1109
1433
  {
1110
1434
  id,
1111
- rows: field.type === "json" ? 5 : 3,
1112
- value: field.type === "json" && value !== null && typeof value === "object" ? JSON.stringify(value, null, 2) : value === null || value === void 0 ? "" : String(value),
1435
+ rows: 3,
1436
+ value: value === null || value === void 0 ? "" : String(value),
1113
1437
  placeholder: field.form.placeholder,
1114
1438
  disabled,
1115
- onChange: (e) => {
1116
- if (field.type === "json") {
1117
- try {
1118
- onChange(e.target.value ? JSON.parse(e.target.value) : null);
1119
- } catch {
1120
- onChange(e.target.value);
1121
- }
1122
- } else {
1123
- onChange(e.target.value);
1124
- }
1125
- },
1126
- className: `${inputClass} font-mono`
1439
+ onChange: (e) => onChange(e.target.value),
1440
+ className: inputClass
1127
1441
  }
1128
1442
  );
1129
1443
  default:
1130
- return /* @__PURE__ */ jsx8(
1444
+ return /* @__PURE__ */ jsx10(
1131
1445
  "input",
1132
1446
  {
1133
1447
  id,
@@ -1143,7 +1457,7 @@ function FieldInput({
1143
1457
  }
1144
1458
 
1145
1459
  // src/components/EntityForm.tsx
1146
- import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1460
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1147
1461
  function EntityForm({
1148
1462
  entity,
1149
1463
  mode,
@@ -1157,10 +1471,10 @@ function EntityForm({
1157
1471
  const fields = formFields(entity, mode);
1158
1472
  const sourceId = mode === "edit" ? id : cloneFromId;
1159
1473
  const existing = useEntityRecord(entity.id, sourceId);
1160
- const [values, setValues] = useState7({});
1161
- const [saving, setSaving] = useState7(false);
1162
- const [error, setError] = useState7(void 0);
1163
- useEffect5(() => {
1474
+ const [values, setValues] = useState8({});
1475
+ const [saving, setSaving] = useState8(false);
1476
+ const [error, setError] = useState8(void 0);
1477
+ useEffect6(() => {
1164
1478
  if (!existing.data || !sourceId) return;
1165
1479
  if (mode === "edit") {
1166
1480
  setValues(existing.data);
@@ -1192,25 +1506,25 @@ function EntityForm({
1192
1506
  }
1193
1507
  };
1194
1508
  if (sourceId && existing.loading && !existing.data) {
1195
- return /* @__PURE__ */ jsx9(Spinner, {});
1509
+ return /* @__PURE__ */ jsx11(Spinner, {});
1196
1510
  }
1197
1511
  const title = mode === "edit" ? t.editTitle(entity.label.singular) : cloneFromId ? t.cloneTitle(entity.label.singular) : t.newTitle(entity.label.singular);
1198
- return /* @__PURE__ */ jsxs8("section", { className: "max-w-2xl space-y-4", children: [
1199
- /* @__PURE__ */ jsxs8("header", { children: [
1200
- /* @__PURE__ */ jsxs8("button", { onClick: onCancel, className: "text-sm text-indigo-600 hover:underline", children: [
1512
+ return /* @__PURE__ */ jsxs10("section", { className: "space-y-4", children: [
1513
+ /* @__PURE__ */ jsxs10("header", { children: [
1514
+ /* @__PURE__ */ jsxs10("button", { onClick: onCancel, className: "text-sm text-indigo-600 hover:underline", children: [
1201
1515
  "\u2190 ",
1202
1516
  entity.label.plural
1203
1517
  ] }),
1204
- /* @__PURE__ */ jsx9("h1", { className: "text-lg font-semibold text-slate-900", children: title })
1518
+ /* @__PURE__ */ jsx11("h1", { className: "text-lg font-semibold text-slate-900", children: title })
1205
1519
  ] }),
1206
- error && /* @__PURE__ */ jsx9(ErrorBanner, { error }),
1207
- /* @__PURE__ */ jsxs8("form", { onSubmit: submit, className: "space-y-4 rounded-lg border border-slate-200 bg-white p-5", children: [
1208
- fields.map((f) => /* @__PURE__ */ jsxs8("div", { className: "space-y-1", children: [
1209
- /* @__PURE__ */ jsxs8("label", { htmlFor: `field-${f.name}`, className: "block text-sm font-medium text-slate-700", children: [
1520
+ error && /* @__PURE__ */ jsx11(ErrorBanner, { error }),
1521
+ /* @__PURE__ */ jsxs10("form", { onSubmit: submit, className: "space-y-4 rounded-lg border border-slate-200 bg-white p-5", children: [
1522
+ fields.map((f) => /* @__PURE__ */ jsxs10("div", { className: "space-y-1", children: [
1523
+ /* @__PURE__ */ jsxs10("label", { htmlFor: `field-${f.name}`, className: "block text-sm font-medium text-slate-700", children: [
1210
1524
  f.label,
1211
- f.required && /* @__PURE__ */ jsx9("span", { className: "ml-0.5 text-red-500", children: "*" })
1525
+ f.required && /* @__PURE__ */ jsx11("span", { className: "ml-0.5 text-red-500", children: "*" })
1212
1526
  ] }),
1213
- /* @__PURE__ */ jsx9(
1527
+ /* @__PURE__ */ jsx11(
1214
1528
  FieldInput,
1215
1529
  {
1216
1530
  field: f,
@@ -1219,11 +1533,11 @@ function EntityForm({
1219
1533
  onChange: (next) => setValues((v) => ({ ...v, [f.name]: next }))
1220
1534
  }
1221
1535
  ),
1222
- f.form.helpText && /* @__PURE__ */ jsx9("p", { className: "text-xs text-slate-500", children: f.form.helpText })
1536
+ f.form.helpText && /* @__PURE__ */ jsx11("p", { className: "text-xs text-slate-500", children: f.form.helpText })
1223
1537
  ] }, f.name)),
1224
- /* @__PURE__ */ jsxs8("div", { className: "flex justify-end gap-2 border-t border-slate-100 pt-4", children: [
1225
- /* @__PURE__ */ jsx9(Button, { type: "button", variant: "secondary", onClick: onCancel, disabled: saving, children: t.cancel }),
1226
- /* @__PURE__ */ jsx9(Button, { type: "submit", variant: "primary", disabled: saving, children: saving ? t.saving : mode === "create" ? t.create : t.save })
1538
+ /* @__PURE__ */ jsxs10("div", { className: "flex justify-end gap-2 border-t border-slate-100 pt-4", children: [
1539
+ /* @__PURE__ */ jsx11(Button, { type: "button", variant: "secondary", onClick: onCancel, disabled: saving, children: t.cancel }),
1540
+ /* @__PURE__ */ jsx11(Button, { type: "submit", variant: "primary", disabled: saving, children: saving ? t.saving : mode === "create" ? t.create : t.save })
1227
1541
  ] })
1228
1542
  ] })
1229
1543
  ] });
@@ -1252,7 +1566,7 @@ function applyLabels(metadata, labels) {
1252
1566
  }
1253
1567
 
1254
1568
  // src/recents.ts
1255
- import { useCallback as useCallback2, useState as useState8 } from "react";
1569
+ import { useCallback as useCallback2, useState as useState9 } from "react";
1256
1570
  var STORAGE_KEY = "maestro-admin:recents";
1257
1571
  var MAX_RECENTS = 8;
1258
1572
  function read(key) {
@@ -1272,7 +1586,7 @@ function write(key, ids) {
1272
1586
  }
1273
1587
  function useRecents(scope) {
1274
1588
  const key = scope ? `${STORAGE_KEY}:${scope}` : STORAGE_KEY;
1275
- const [recents, setRecents] = useState8(() => read(key));
1589
+ const [recents, setRecents] = useState9(() => read(key));
1276
1590
  const push = useCallback2(
1277
1591
  (entityId) => {
1278
1592
  setRecents((prev) => {
@@ -1287,11 +1601,11 @@ function useRecents(scope) {
1287
1601
  }
1288
1602
 
1289
1603
  // src/components/MaestroAdmin.tsx
1290
- import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1604
+ import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
1291
1605
  function AdminShell({ title, labels }) {
1292
1606
  const { data, error, loading } = useMetadata();
1293
1607
  const t = useT();
1294
- const [view, setView] = useState9(void 0);
1608
+ const [view, setView] = useState10(void 0);
1295
1609
  const { recents, push: pushRecent } = useRecents();
1296
1610
  const metadata = useMemo3(() => data ? applyLabels(data, labels) : void 0, [data, labels]);
1297
1611
  const entities = metadata?.entities ?? [];
@@ -1307,10 +1621,10 @@ function AdminShell({ title, labels }) {
1307
1621
  setView({ kind: "detail", entityId, id });
1308
1622
  };
1309
1623
  if (loading && !data) {
1310
- return /* @__PURE__ */ jsx10("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ jsx10(Spinner, { label: t.loadingMetadata }) });
1624
+ return /* @__PURE__ */ jsx12("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ jsx12(Spinner, { label: t.loadingMetadata }) });
1311
1625
  }
1312
- return /* @__PURE__ */ jsx10(AdminEntitiesProvider, { value: entities, children: /* @__PURE__ */ jsxs9("div", { className: "flex h-screen overflow-hidden bg-white text-slate-800", children: [
1313
- /* @__PURE__ */ jsx10(
1626
+ return /* @__PURE__ */ jsx12(AdminEntitiesProvider, { value: entities, children: /* @__PURE__ */ jsxs11("div", { className: "flex h-screen overflow-hidden bg-white text-slate-800", children: [
1627
+ /* @__PURE__ */ jsx12(
1314
1628
  Sidebar,
1315
1629
  {
1316
1630
  entities,
@@ -1321,9 +1635,9 @@ function AdminShell({ title, labels }) {
1321
1635
  onSelect: openEntity
1322
1636
  }
1323
1637
  ),
1324
- /* @__PURE__ */ jsxs9("main", { className: "flex-1 overflow-y-auto p-6", children: [
1325
- error && /* @__PURE__ */ jsx10(ErrorBanner, { error }),
1326
- activeEntity && effectiveView && effectiveView.kind === "list" && /* @__PURE__ */ jsx10(
1638
+ /* @__PURE__ */ jsxs11("main", { className: "flex-1 overflow-y-auto p-6", children: [
1639
+ error && /* @__PURE__ */ jsx12(ErrorBanner, { error }),
1640
+ activeEntity && effectiveView && effectiveView.kind === "list" && /* @__PURE__ */ jsx12(
1327
1641
  EntityList,
1328
1642
  {
1329
1643
  entity: activeEntity,
@@ -1334,7 +1648,7 @@ function AdminShell({ title, labels }) {
1334
1648
  onNavigate: navigateToRecord
1335
1649
  }
1336
1650
  ),
1337
- activeEntity && effectiveView?.kind === "detail" && /* @__PURE__ */ jsx10(
1651
+ activeEntity && effectiveView?.kind === "detail" && /* @__PURE__ */ jsx12(
1338
1652
  EntityDetail,
1339
1653
  {
1340
1654
  entity: activeEntity,
@@ -1346,7 +1660,7 @@ function AdminShell({ title, labels }) {
1346
1660
  onNavigate: navigateToRecord
1347
1661
  }
1348
1662
  ),
1349
- activeEntity && (effectiveView?.kind === "create" || effectiveView?.kind === "edit") && /* @__PURE__ */ jsx10(
1663
+ activeEntity && (effectiveView?.kind === "create" || effectiveView?.kind === "edit") && /* @__PURE__ */ jsx12(
1350
1664
  EntityForm,
1351
1665
  {
1352
1666
  entity: activeEntity,
@@ -1366,7 +1680,7 @@ function MaestroAdmin(props) {
1366
1680
  [props.client, props.apiUrl, props.headers]
1367
1681
  );
1368
1682
  const strings = useMemo3(() => stringsFor(props.locale ?? "en"), [props.locale]);
1369
- return /* @__PURE__ */ jsx10(AdminClientProvider, { value: client, children: /* @__PURE__ */ jsx10(I18nProvider, { value: strings, children: /* @__PURE__ */ jsx10(AdminShell, { title: props.title ?? "Maestro Admin", labels: props.labels }) }) });
1683
+ return /* @__PURE__ */ jsx12(AdminClientProvider, { value: client, children: /* @__PURE__ */ jsx12(I18nProvider, { value: strings, children: /* @__PURE__ */ jsx12(AdminShell, { title: props.title ?? "Maestro Admin", labels: props.labels }) }) });
1370
1684
  }
1371
1685
  export {
1372
1686
  AdminClientProvider,
@@ -1376,9 +1690,11 @@ export {
1376
1690
  FieldInput,
1377
1691
  FieldValue,
1378
1692
  I18nProvider,
1693
+ JsonInput,
1379
1694
  MaestroAdmin,
1380
1695
  MaestroApiError,
1381
1696
  MaestroClient,
1697
+ ObjectValue,
1382
1698
  Sidebar,
1383
1699
  applyLabels,
1384
1700
  buildMenu,