@almadar/std 4.0.0 → 5.1.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.
@@ -728,19 +728,14 @@ function buildTrait(c) {
728
728
  const actionEvents = /* @__PURE__ */ new Set();
729
729
  for (const a of c.headerActions) actionEvents.add(a.event);
730
730
  for (const a of c.itemActions) actionEvents.add(a.event);
731
- for (const re of c.refreshEvents) actionEvents.add(re);
732
731
  const events = [
733
732
  { key: "INIT", name: "Initialize" },
734
733
  ...Array.from(actionEvents).map((e) => {
735
734
  const needsId = c.itemActions.some((a) => a.event === e);
736
- const isRefresh = c.refreshEvents.includes(e);
737
- if (isRefresh) {
738
- return { key: e, name: e, payload: [{ name: "data", type: "object", required: true }] };
739
- }
740
735
  return needsId ? { key: e, name: e, payload: [{ name: "id", type: "string", required: true }, { name: "row", type: "object" }] } : { key: e, name: e };
741
736
  })
742
737
  ];
743
- const listensDecl = c.refreshEvents.length > 0 ? c.refreshEvents.map((evt) => ({ event: evt, triggers: evt })) : void 0;
738
+ const listensDecl = c.refreshEvents.length > 0 ? c.refreshEvents.map((evt) => ({ event: evt, triggers: "INIT" })) : void 0;
744
739
  return {
745
740
  name: c.traitName,
746
741
  linkedEntity: entityName,
@@ -777,14 +772,7 @@ function buildTrait(c) {
777
772
  ]
778
773
  }]
779
774
  ]
780
- },
781
- // Refresh self-loops: when modal atoms fire SAVE etc., re-fetch data
782
- ...c.refreshEvents.map((evt) => ({
783
- from: "browsing",
784
- to: "browsing",
785
- event: evt,
786
- effects: [["ref", entityName]]
787
- }))
775
+ }
788
776
  ]
789
777
  }
790
778
  };
@@ -842,6 +830,8 @@ function resolve2(params) {
842
830
  }
843
831
  ]
844
832
  };
833
+ const saveEvent = params.saveEvent ?? "SAVE";
834
+ const emitOnSave = params.emitOnSave ?? saveEvent;
845
835
  return {
846
836
  entityName,
847
837
  fields,
@@ -855,9 +845,9 @@ function resolve2(params) {
855
845
  openPayload: params.openPayload ?? [],
856
846
  closeEvent: params.closeEvent ?? "CLOSE",
857
847
  openEffects: params.openEffects ?? [],
858
- saveEvent: params.saveEvent ?? null,
848
+ saveEvent,
859
849
  saveEffects: params.saveEffects ?? [],
860
- emitOnSave: params.emitOnSave ?? null,
850
+ emitOnSave,
861
851
  standalone: params.standalone ?? true,
862
852
  pageName: params.pageName ?? `${entityName}ModalPage`,
863
853
  pagePath: params.pagePath ?? `/${p.toLowerCase()}/modal`,
@@ -871,11 +861,9 @@ function buildTrait2(c) {
871
861
  const events = [
872
862
  { key: "INIT", name: "Initialize" },
873
863
  { key: c.openEvent, name: "Open", ...c.openPayload.length > 0 ? { payload: c.openPayload } : {} },
874
- { key: c.closeEvent, name: "Close" }
864
+ { key: c.closeEvent, name: "Close" },
865
+ { key: c.saveEvent, name: "Save", payload: [{ name: "data", type: "object", required: true }] }
875
866
  ];
876
- if (c.saveEvent) {
877
- events.push({ key: c.saveEvent, name: "Save", payload: [{ name: "data", type: "object", required: true }] });
878
- }
879
867
  const transitions = [
880
868
  // INIT: closed → closed
881
869
  {
@@ -928,41 +916,44 @@ function buildTrait2(c) {
928
916
  }]] : []
929
917
  ] }
930
918
  ];
931
- if (c.saveEvent) {
932
- const mainRefresh = c.standalone ? [["ref", c.entityName], ["render-ui", "main", {
933
- type: "stack",
934
- direction: "vertical",
935
- gap: "lg",
936
- children: [
937
- { type: "stack", direction: "horizontal", gap: "md", justify: "space-between", children: [
938
- { type: "stack", direction: "horizontal", gap: "md", children: [
939
- { type: "icon", name: c.headerIcon, size: "lg" },
940
- { type: "typography", content: c.modalTitle, variant: "h2" }
941
- ] },
942
- { type: "button", label: "Open", event: c.openEvent, variant: "primary", icon: c.headerIcon }
919
+ const mainRefresh = c.standalone ? [["ref", c.entityName], ["render-ui", "main", {
920
+ type: "stack",
921
+ direction: "vertical",
922
+ gap: "lg",
923
+ children: [
924
+ { type: "stack", direction: "horizontal", gap: "md", justify: "space-between", children: [
925
+ { type: "stack", direction: "horizontal", gap: "md", children: [
926
+ { type: "icon", name: c.headerIcon, size: "lg" },
927
+ { type: "typography", content: c.modalTitle, variant: "h2" }
943
928
  ] },
944
- { type: "divider" },
945
- { type: "empty-state", icon: c.headerIcon, title: "Nothing open", description: "Click Open to view details in a modal overlay." }
946
- ]
947
- }]] : [];
948
- transitions.push({
949
- from: "open",
950
- to: "closed",
951
- event: c.saveEvent,
952
- effects: [
953
- ...c.saveEffects,
954
- ["render-ui", "modal", null],
955
- // Emit after persist succeeds so browse traits can fetch fresh data
956
- ...c.emitOnSave ? [["emit", c.emitOnSave]] : [],
957
- ...mainRefresh
958
- ]
959
- });
960
- }
929
+ { type: "button", label: "Open", event: c.openEvent, variant: "primary", icon: c.headerIcon }
930
+ ] },
931
+ { type: "divider" },
932
+ { type: "empty-state", icon: c.headerIcon, title: "Nothing open", description: "Click Open to view details in a modal overlay." }
933
+ ]
934
+ }]] : [];
935
+ transitions.push({
936
+ from: "open",
937
+ to: "closed",
938
+ event: c.saveEvent,
939
+ effects: [
940
+ ...c.saveEffects,
941
+ ["render-ui", "modal", null],
942
+ // Emit after persist succeeds so browse traits can fetch fresh data.
943
+ // Skip the emit when emitOnSave equals saveEvent — that's a self-emit
944
+ // that the runtime would short-circuit anyway, and avoids double
945
+ // dispatch on every save.
946
+ ...c.emitOnSave !== c.saveEvent ? [["emit", c.emitOnSave]] : [],
947
+ ...mainRefresh
948
+ ]
949
+ });
961
950
  return {
962
951
  name: c.traitName,
963
952
  linkedEntity: c.entityName,
964
953
  category: "interaction",
965
- ...c.emitOnSave ? { emits: [{ event: c.emitOnSave }] } : {},
954
+ // Phase F.10: emits[] is always populated (default emitOnSave = saveEvent).
955
+ // If a molecule supplies a distinct emitOnSave, declare both events.
956
+ emits: c.emitOnSave === c.saveEvent ? [{ event: c.saveEvent }] : [{ event: c.saveEvent }, { event: c.emitOnSave }],
966
957
  stateMachine: {
967
958
  states: [{ name: "closed", isInitial: true }, { name: "open" }],
968
959
  events,
@@ -991,6 +982,8 @@ function resolve3(params) {
991
982
  const fields = ensureIdField(params.fields);
992
983
  const nonIdFields = fields.filter((f) => f.name !== "id");
993
984
  const p = plural(entityName);
985
+ const confirmEvent = params.confirmEvent ?? "CONFIRM";
986
+ const emitOnConfirm = params.emitOnConfirm ?? confirmEvent;
994
987
  return {
995
988
  entityName,
996
989
  fields,
@@ -1004,9 +997,9 @@ function resolve3(params) {
1004
997
  cancelLabel: params.cancelLabel ?? "Cancel",
1005
998
  headerIcon: params.headerIcon ?? "shield-check",
1006
999
  requestEvent: params.requestEvent ?? "REQUEST",
1007
- confirmEvent: params.confirmEvent ?? "CONFIRM",
1000
+ confirmEvent,
1008
1001
  confirmEffects: params.confirmEffects ?? [],
1009
- emitOnConfirm: params.emitOnConfirm ?? null,
1002
+ emitOnConfirm,
1010
1003
  standalone: params.standalone ?? true,
1011
1004
  pageName: params.pageName ?? `${entityName}ConfirmPage`,
1012
1005
  pagePath: params.pagePath ?? `/${p.toLowerCase()}/confirm`,
@@ -1086,7 +1079,9 @@ function buildTrait3(c) {
1086
1079
  name: c.traitName,
1087
1080
  linkedEntity: entityName,
1088
1081
  category: "interaction",
1089
- ...c.emitOnConfirm ? { emits: [{ event: c.emitOnConfirm }] } : {},
1082
+ // Phase F.10: emits[] always populated. When emitOnConfirm equals
1083
+ // confirmEvent, declare just the one. When they differ, declare both.
1084
+ emits: c.emitOnConfirm === c.confirmEvent ? [{ event: c.confirmEvent }] : [{ event: c.confirmEvent }, { event: c.emitOnConfirm }],
1090
1085
  stateMachine: {
1091
1086
  states: [
1092
1087
  { name: "idle", isInitial: true },
@@ -1123,7 +1118,9 @@ function buildTrait3(c) {
1123
1118
  effects: [
1124
1119
  ...c.confirmEffects,
1125
1120
  ...dismissAndRefresh,
1126
- ...c.emitOnConfirm ? [["emit", c.emitOnConfirm]] : []
1121
+ // Skip self-emit when emitOnConfirm == confirmEvent (the runtime
1122
+ // would short-circuit anyway).
1123
+ ...c.emitOnConfirm !== c.confirmEvent ? [["emit", c.emitOnConfirm]] : []
1127
1124
  ]
1128
1125
  },
1129
1126
  {
@@ -8982,6 +8979,7 @@ function stdList(params) {
8982
8979
  const UPPER = entityName.replace(/([a-z])([A-Z])/g, "$1_$2").toUpperCase();
8983
8980
  const CREATED = `${UPPER}_CREATED`;
8984
8981
  const UPDATED = `${UPPER}_UPDATED`;
8982
+ const DELETED = `${UPPER}_DELETED`;
8985
8983
  const browseTrait = extractTrait(stdBrowse({
8986
8984
  entityName,
8987
8985
  fields,
@@ -9002,7 +9000,7 @@ function stdList(params) {
9002
9000
  { label: "Edit", event: "EDIT" },
9003
9001
  { label: "Delete", event: "DELETE", variant: "danger" }
9004
9002
  ],
9005
- refreshEvents: [CREATED, UPDATED]
9003
+ refreshEvents: [CREATED, UPDATED, DELETED]
9006
9004
  }));
9007
9005
  const createTrait = extractTrait(stdModal({
9008
9006
  standalone: false,
@@ -9048,62 +9046,20 @@ function stdList(params) {
9048
9046
  closeEvent: "CLOSE",
9049
9047
  openEffects: [["fetch", entityName, { id: "@payload.id" }]]
9050
9048
  }));
9051
- const sm = browseTrait.stateMachine;
9052
- sm.states.push({ name: "deleting" });
9053
- const deleteEvent = sm.events.find((e) => e.key === "DELETE");
9054
- if (deleteEvent && !deleteEvent.payload) {
9055
- deleteEvent.payload = [{ name: "id", type: "string" }, { name: "row", type: "object" }];
9056
- }
9057
- const existingKeys = new Set(sm.events.map((e) => e.key));
9058
- if (!existingKeys.has("CONFIRM_DELETE")) sm.events.push({ key: "CONFIRM_DELETE", name: "Confirm Delete" });
9059
- if (!existingKeys.has("CANCEL")) sm.events.push({ key: "CANCEL", name: "Cancel" });
9060
- if (!existingKeys.has("CLOSE")) sm.events.push({ key: "CLOSE", name: "Close" });
9061
- const initTransition = sm.transitions[0];
9062
- const initRenderEffect = initTransition.effects.find(
9063
- (e) => Array.isArray(e) && e[0] === "render-ui" && e[1] === "main"
9064
- );
9065
- const browseMainView = initRenderEffect ? initRenderEffect[2] : null;
9066
- sm.transitions.push(
9067
- // DELETE: browsing → deleting (fetch entity by ID, show confirmation modal)
9068
- { from: "browsing", to: "deleting", event: "DELETE", effects: [
9069
- ["fetch", entityName, { id: "@payload.id" }],
9070
- ["render-ui", "modal", {
9071
- type: "stack",
9072
- direction: "vertical",
9073
- gap: "md",
9074
- children: [
9075
- { type: "stack", direction: "horizontal", gap: "sm", children: [
9076
- { type: "icon", name: "trash-2", size: "md" },
9077
- { type: "typography", content: `Delete ${entityName}`, variant: "h3" }
9078
- ] },
9079
- { type: "divider" },
9080
- { type: "typography", content: `@entity.${c.nonIdFields[0]?.name ?? "name"}`, variant: "h4" },
9081
- { type: "typography", content: c.deleteMessage, variant: "body" },
9082
- { type: "stack", direction: "horizontal", gap: "sm", justify: "end", children: [
9083
- { type: "button", label: "Cancel", event: "CANCEL", variant: "ghost" },
9084
- { type: "button", label: "Delete", event: "CONFIRM_DELETE", variant: "danger", icon: "trash" }
9085
- ] }
9086
- ]
9087
- }]
9088
- ] },
9089
- // CONFIRM_DELETE: deleting → browsing (persist delete, dismiss modal, re-render main)
9090
- { from: "deleting", to: "browsing", event: "CONFIRM_DELETE", effects: [
9091
- ["persist", "delete", entityName, "@entity.id"],
9092
- ["render-ui", "modal", null],
9093
- ["render-ui", "main", browseMainView]
9094
- ] },
9095
- // CANCEL/CLOSE from deleting (dismiss modal, re-render main)
9096
- { from: "deleting", to: "browsing", event: "CANCEL", effects: [
9097
- ["render-ui", "modal", null],
9098
- ["fetch", entityName],
9099
- ["render-ui", "main", browseMainView]
9100
- ] },
9101
- { from: "deleting", to: "browsing", event: "CLOSE", effects: [
9102
- ["render-ui", "modal", null],
9103
- ["fetch", entityName],
9104
- ["render-ui", "main", browseMainView]
9105
- ] }
9106
- );
9049
+ const deleteTrait = extractTrait(stdConfirmation({
9050
+ standalone: false,
9051
+ entityName,
9052
+ fields,
9053
+ traitName: `${entityName}Delete`,
9054
+ confirmTitle: `Delete ${entityName}`,
9055
+ confirmMessage: c.deleteMessage,
9056
+ confirmLabel: "Delete",
9057
+ headerIcon: "trash-2",
9058
+ requestEvent: "DELETE",
9059
+ confirmEvent: "CONFIRM_DELETE",
9060
+ confirmEffects: [["persist", "delete", entityName, "@entity.pendingId"]],
9061
+ emitOnConfirm: DELETED
9062
+ }));
9107
9063
  const entity = makeEntity({ name: entityName, fields, persistence: c.persistence, collection: c.collection });
9108
9064
  const page = {
9109
9065
  name: c.pageName,
@@ -9113,13 +9069,14 @@ function stdList(params) {
9113
9069
  { ref: browseTrait.name },
9114
9070
  { ref: createTrait.name },
9115
9071
  { ref: editTrait.name },
9116
- { ref: viewTrait.name }
9072
+ { ref: viewTrait.name },
9073
+ { ref: deleteTrait.name }
9117
9074
  ]
9118
9075
  };
9119
9076
  return {
9120
9077
  name: `${entityName}Orbital`,
9121
9078
  entity,
9122
- traits: [browseTrait, createTrait, editTrait, viewTrait],
9079
+ traits: [browseTrait, createTrait, editTrait, viewTrait, deleteTrait],
9123
9080
  pages: [page]
9124
9081
  };
9125
9082
  }
@@ -9607,17 +9564,9 @@ function stdInventory(params) {
9607
9564
  headerIcon: "trash-2",
9608
9565
  requestEvent: "DROP",
9609
9566
  confirmEvent: "CONFIRM_DROP",
9610
- confirmEffects: [["persist", "delete", entityName, "@payload.id"]],
9567
+ confirmEffects: [["persist", "delete", entityName, "@entity.pendingId"]],
9611
9568
  emitOnConfirm: "CONFIRM_DROP"
9612
9569
  }));
9613
- const dropSm = dropTrait.stateMachine;
9614
- if (dropSm && "events" in dropSm) {
9615
- const events = dropSm.events;
9616
- const confirmDropEvt = events.find((e) => e.key === "CONFIRM_DROP");
9617
- if (confirmDropEvt && !confirmDropEvt.payload) {
9618
- confirmDropEvt.payload = [{ name: "id", type: "string", required: true }];
9619
- }
9620
- }
9621
9570
  const instances = [
9622
9571
  { id: "item-1", name: "Health Potion", description: "Restores 50 HP", status: "active", pendingId: "" },
9623
9572
  { id: "item-2", name: "Iron Sword", description: "A sturdy blade", status: "active", pendingId: "" },