@almadar/std 16.190.0 → 16.192.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.
Files changed (107) hide show
  1. package/behaviors/lolo/infra/atoms/std-approval-gate.lolo +184 -18
  2. package/behaviors/lolo/infra/atoms/std-status-lifecycle.lolo +16 -3
  3. package/behaviors/lolo/ui/core/atoms/std-billable-hour.lolo +1 -0
  4. package/behaviors/lolo/ui/core/atoms/std-browse.lolo +3 -0
  5. package/behaviors/lolo/ui/core/atoms/std-image-upload-multi.lolo +37 -4
  6. package/behaviors/lolo/ui/core/atoms/std-modal.lolo +20 -6
  7. package/behaviors/lolo/ui/core/atoms/std-record-detail.lolo +40 -3
  8. package/behaviors/lolo/ui/core/atoms/std-tag-taxonomy.lolo +176 -4
  9. package/behaviors/lolo/ui/core/atoms/std-version-history.lolo +3 -1
  10. package/behaviors/lolo/ui/core/molecules/ui-calendar-grid.lolo +25 -1
  11. package/behaviors/lolo/ui/core/molecules/ui-data-grid.lolo +37 -1
  12. package/behaviors/lolo/ui/core/molecules/ui-data-list.lolo +46 -1
  13. package/behaviors/lolo/ui/core/molecules/ui-document-details.lolo +30 -2
  14. package/behaviors/lolo/ui/core/molecules/ui-document-panel.lolo +14 -29
  15. package/behaviors/lolo/ui/core/molecules/ui-positioned-canvas.lolo +19 -1
  16. package/behaviors/lolo/ui/core/molecules/ui-reply-tree.lolo +21 -1
  17. package/behaviors/lolo/ui/core/molecules/ui-sortable-list.lolo +15 -1
  18. package/behaviors/lolo/ui/core/molecules/ui-table-view.lolo +36 -1
  19. package/behaviors/lolo/ui/core/organisms/ui-detail-panel.lolo +41 -1
  20. package/behaviors/lolo/ui/core/organisms/ui-entity-cards.lolo +34 -1
  21. package/behaviors/lolo/ui/core/organisms/ui-entity-list.lolo +30 -1
  22. package/behaviors/lolo/ui/core/organisms/ui-entity-table.lolo +36 -1
  23. package/behaviors/lolo/ui/core/organisms/ui-form-section.lolo +39 -1
  24. package/behaviors/lolo/ui/core/organisms/ui-form.lolo +39 -1
  25. package/behaviors/lolo/ui/core/organisms/ui-master-detail.lolo +24 -1
  26. package/behaviors/lolo/ui/core/organisms/ui-media-gallery.lolo +30 -1
  27. package/behaviors/lolo/ui/core/organisms/ui-stat-card.lolo +37 -1
  28. package/behaviors/lolo/ui/core/organisms/ui-timeline.lolo +18 -1
  29. package/behaviors/lolo/ui/core/templates/ui-counter-template.lolo +21 -1
  30. package/behaviors/lolo/ui/core/templates/ui-generic-app-template.lolo +16 -1
  31. package/behaviors/lolo/ui/game/atoms/std-platformer-body.lolo +7 -2
  32. package/behaviors/lolo/ui/marketing/organisms/ui-book-chapter-view.lolo +12 -1
  33. package/behaviors/lolo/ui/marketing/organisms/ui-book-table-of-contents.lolo +13 -1
  34. package/behaviors/lolo/ui/marketing/organisms/ui-book-viewer.lolo +23 -1
  35. package/behaviors/lolo/ui/marketing/organisms/ui-case-study-organism.lolo +23 -1
  36. package/behaviors/lolo/ui/marketing/organisms/ui-feature-grid-organism.lolo +24 -1
  37. package/behaviors/lolo/ui/marketing/organisms/ui-hero-organism.lolo +22 -1
  38. package/behaviors/lolo/ui/marketing/organisms/ui-pricing-organism.lolo +23 -1
  39. package/behaviors/lolo/ui/marketing/organisms/ui-showcase-organism.lolo +24 -1
  40. package/behaviors/lolo/ui/marketing/organisms/ui-stats-organism.lolo +22 -1
  41. package/behaviors/lolo/ui/marketing/organisms/ui-step-flow-organism.lolo +25 -1
  42. package/behaviors/lolo/ui/marketing/organisms/ui-team-organism.lolo +23 -1
  43. package/behaviors/lolo/ui/marketing/templates/ui-about-page-template.lolo +11 -1
  44. package/behaviors/lolo/ui/marketing/templates/ui-feature-detail-page-template.lolo +11 -1
  45. package/behaviors/lolo/ui/marketing/templates/ui-landing-page-template.lolo +13 -1
  46. package/behaviors/lolo/ui/marketing/templates/ui-pricing-page-template.lolo +11 -1
  47. package/behaviors/registry/infra/atoms/std-approval-gate.orb +717 -45
  48. package/behaviors/registry/infra/atoms/std-status-lifecycle.orb +85 -10
  49. package/behaviors/registry/ui/core/atoms/std-billable-hour.orb +5 -2
  50. package/behaviors/registry/ui/core/atoms/std-browse.orb +12 -0
  51. package/behaviors/registry/ui/core/atoms/std-image-upload-multi.orb +129 -7
  52. package/behaviors/registry/ui/core/atoms/std-modal.orb +95 -3
  53. package/behaviors/registry/ui/core/atoms/std-record-detail.orb +319 -3
  54. package/behaviors/registry/ui/core/atoms/std-tag-taxonomy.orb +539 -1
  55. package/behaviors/registry/ui/core/atoms/std-version-history.orb +5 -1
  56. package/behaviors/registry/ui/core/molecules/ui-calendar-grid.orb +43 -0
  57. package/behaviors/registry/ui/core/molecules/ui-data-grid.orb +55 -0
  58. package/behaviors/registry/ui/core/molecules/ui-data-list.orb +64 -0
  59. package/behaviors/registry/ui/core/molecules/ui-document-details.orb +106 -1
  60. package/behaviors/registry/ui/core/molecules/ui-document-panel.orb +18 -80
  61. package/behaviors/registry/ui/core/molecules/ui-positioned-canvas.orb +37 -0
  62. package/behaviors/registry/ui/core/molecules/ui-reply-tree.orb +39 -0
  63. package/behaviors/registry/ui/core/molecules/ui-sortable-list.orb +45 -0
  64. package/behaviors/registry/ui/core/molecules/ui-table-view.orb +54 -0
  65. package/behaviors/registry/ui/core/organisms/ui-detail-panel.orb +59 -0
  66. package/behaviors/registry/ui/core/organisms/ui-entity-cards.orb +52 -0
  67. package/behaviors/registry/ui/core/organisms/ui-entity-list.orb +48 -0
  68. package/behaviors/registry/ui/core/organisms/ui-entity-table.orb +54 -0
  69. package/behaviors/registry/ui/core/organisms/ui-form-section.orb +57 -0
  70. package/behaviors/registry/ui/core/organisms/ui-form.orb +57 -0
  71. package/behaviors/registry/ui/core/organisms/ui-master-detail.orb +42 -0
  72. package/behaviors/registry/ui/core/organisms/ui-media-gallery.orb +48 -0
  73. package/behaviors/registry/ui/core/organisms/ui-stat-card.orb +55 -0
  74. package/behaviors/registry/ui/core/organisms/ui-timeline.orb +36 -0
  75. package/behaviors/registry/ui/core/templates/ui-counter-template.orb +39 -0
  76. package/behaviors/registry/ui/core/templates/ui-generic-app-template.orb +34 -0
  77. package/behaviors/registry/ui/game/atoms/std-platformer-body.orb +543 -496
  78. package/behaviors/registry/ui/marketing/organisms/ui-book-chapter-view.orb +30 -0
  79. package/behaviors/registry/ui/marketing/organisms/ui-book-table-of-contents.orb +31 -0
  80. package/behaviors/registry/ui/marketing/organisms/ui-book-viewer.orb +41 -0
  81. package/behaviors/registry/ui/marketing/organisms/ui-case-study-organism.orb +41 -0
  82. package/behaviors/registry/ui/marketing/organisms/ui-feature-grid-organism.orb +42 -0
  83. package/behaviors/registry/ui/marketing/organisms/ui-hero-organism.orb +40 -0
  84. package/behaviors/registry/ui/marketing/organisms/ui-pricing-organism.orb +41 -0
  85. package/behaviors/registry/ui/marketing/organisms/ui-showcase-organism.orb +42 -0
  86. package/behaviors/registry/ui/marketing/organisms/ui-stats-organism.orb +40 -0
  87. package/behaviors/registry/ui/marketing/organisms/ui-step-flow-organism.orb +43 -0
  88. package/behaviors/registry/ui/marketing/organisms/ui-team-organism.orb +41 -0
  89. package/behaviors/registry/ui/marketing/templates/ui-about-page-template.orb +29 -0
  90. package/behaviors/registry/ui/marketing/templates/ui-feature-detail-page-template.orb +29 -0
  91. package/behaviors/registry/ui/marketing/templates/ui-landing-page-template.orb +31 -0
  92. package/behaviors/registry/ui/marketing/templates/ui-pricing-page-template.orb +29 -0
  93. package/dist/behaviors/exports-reader.js +2651 -50
  94. package/dist/behaviors/functions/index.d.ts +138 -19
  95. package/dist/behaviors/functions/index.js +2650 -51
  96. package/dist/behaviors/index.d.ts +1 -1
  97. package/dist/behaviors/index.js +2652 -51
  98. package/dist/behaviors/query.js +2651 -50
  99. package/dist/behaviors-embeddings.hash.json +3 -3
  100. package/dist/behaviors-embeddings.json +961 -929
  101. package/dist/behaviors-registry.json +106 -58
  102. package/dist/index.d.ts +1 -1
  103. package/dist/index.js +2652 -51
  104. package/dist/knob-embeddings.hash.json +3 -3
  105. package/dist/knob-embeddings.json +1 -1
  106. package/dist/registry/factory-signatures.json +1 -1
  107. package/package.json +3 -3
@@ -1,5 +1,5 @@
1
- app std-approval-gate "1.0.1"
2
- "std-approval-gate — generic approval gate / approver workflow. Use when a user voices that an action must be reviewed, approved, signed off, or authorized by a privileged role before it proceeds (admin approval, supervisor sign-off, manager approval, gatekeeper review, editor approval, senior editor approval, reviewer approval, publishing approval, draft approval, content review, submit drafts then editors review and approve or reject, needs approval before going live, requires sign-off before publish). Persists ApprovalRequest rows + surfaces them in a review queue with Approve / Deny / Reject actions, optionally requiring a written rejection reason or feedback comment. On approval emits ApprovalGranted; on denial emits ApprovalDenied. Compose onto any orbital whose action you want to gate: the target orbital emits an external event, an ApprovalRequest row is created, and downstream effects wait on ApprovalGranted. Differs from std-mod-queue, which is content-moderation-specific."
1
+ app std-approval-gate "1.1.0"
2
+ "std-approval-gate — generic approval gate / approver workflow. Use when a user voices that an action must be reviewed, approved, signed off, or authorized by a privileged role before it proceeds (admin approval, supervisor sign-off, manager approval, gatekeeper review, editor approval, senior editor approval, reviewer approval, publishing approval, draft approval, content review, submit drafts then editors review and approve or reject, needs approval before going live, requires sign-off before publish). Persists ApprovalRequest rows + surfaces the PENDING ones in a review queue showing what is being approved and who asked, with Approve / Deny actions — Deny captures a written reason. approverRoles is ENFORCED by the review transitions themselves: an APPROVE/DENY from a viewer whose role is not listed does not fire. On approval emits ApprovalGranted; on denial emits ApprovalDenied (with the reason). Hosts that replay the gated action MUST route a failed replay back via APPROVAL_REPLAY_FAILED so the request reopens instead of being silently consumed. Compose onto any orbital whose action you want to gate: the target orbital emits an external event, an ApprovalRequest row is created (stamp subjectLabel/subjectId/requestedBy so reviewers see what they are judging), and downstream effects wait on ApprovalGranted. Differs from std-mod-queue, which is content-moderation-specific."
3
3
  orbital ApprovalGateOrbital {
4
4
  uses Spinner from "std/behaviors/ui-spinner"
5
5
  uses Typography from "std/behaviors/ui-typography"
@@ -18,17 +18,34 @@ orbital ApprovalGateOrbital {
18
18
 
19
19
  type ApprovalGranted = Event { requestId : string, payload : object } "Re-emitted on approval; carries the held payload so the downstream listener can replay the gated action without a lookup."
20
20
 
21
- type ApprovalDenied = Event { requestId : string } "Emitted on denial; downstream listeners can log or notify."
21
+ type DenyCommit = { id : string!, notes : string }
22
22
 
23
+ type ApprovalDenied = Event { requestId : string, reason : string } "Emitted on denial; carries the reviewer's written reason. Downstream listeners log, notify, or return the subject to its prior state."
24
+
25
+ ;; The queue is a shared review surface — every signed-in role may need to
26
+ ;; SEE what is pending about their own submissions; WHO may decide is the
27
+ ;; role guard on the APPROVE/DENY transitions (the FSM enforces it on the
28
+ ;; affordance and on any bus-delivered event alike). Entity-layer access
29
+ ;; predicates CANNOT live here: a generic atom declares no [identity]
30
+ ;; entity, and ORB_S_ACCESS_POLICY_REQUIRES_IDENTITY (correctly) rejects a
31
+ ;; @user-reading directive without a roster — while a host cannot add
32
+ ;; predicates to a foreign atom's entity. Ledgered
33
+ ;; (R-APPROVAL-REQUEST-ENTITY-LAYER-UNGATED); the guard layer above is the
34
+ ;; enforcement both paths carry today.
23
35
  entity ApprovalRequest [persistent: approval_requests] {
36
+ @read none "Shared review surface: submitters watch their requests, approvers work the queue; decision authority is enforced at the transitions, not by hiding rows."
24
37
  id : string!
25
38
  gatedEvent : string @description "The event that triggered the approval request." @synonyms "trigger, event, source" = ""
39
+ subjectLabel : string @description "Human title of the thing being approved (the article's headline, the page's title) — what the reviewer sees in the queue." @synonyms "title, subject, what is being approved" = ""
40
+ subjectId : string @description "Id of the record being approved, for a queue row's View click-through to the subject's own surface." @synonyms "record id, target id, article id, page id" = ""
41
+ requestedBy : string @description "User id of the submitter." @synonyms "submitter, author, requester id" = ""
42
+ requestedByName : string @description "Display name of the submitter shown in the queue." @synonyms "submitter name, requester, who asked" = ""
26
43
  status : "pending" | "approved" | "denied" | "escalated" @description "Current state of the approval process." @synonyms "approval status, state, condition" = pending
27
44
  reviewer : string @description "The user or system responsible for reviewing the request." @synonyms "assignee, owner, approver, responsible party" = ""
28
45
  requestedAt : number @description "Timestamp indicating when the approval request was initiated." @synonyms "creation time, request date, submission time" = 0
29
46
  decidedAt : number @description "Timestamp indicating when a decision was made on the request." @synonyms "decision time, resolved at, completed at" = 0
30
47
  escalatedAt : number @description "Timestamp indicating when the request was escalated." @synonyms "escalation time, escalation timestamp, escalated time" = 0
31
- notes : string @description "Free-form text providing additional context or details." @synonyms "comments, remarks, explanation, details" = ""
48
+ notes : string @description "The reviewer's written reason, captured by the deny step." @synonyms "comments, remarks, explanation, reason, details" = ""
32
49
  payload : object @description "The held data of the gated action, replayed by the downstream listener when the request is granted." @synonyms "request data, held payload, pending data, gated data" = {}
33
50
  }
34
51
 
@@ -63,6 +80,21 @@ orbital ApprovalGateOrbital {
63
80
  }
64
81
  }
65
82
 
83
+ trait DenyReasonTitle = Typography.traits.TypographyRender -> ApprovalRequest {
84
+ config {
85
+ content: "Deny request"
86
+ variant: h3
87
+ }
88
+ }
89
+
90
+ trait DenyReasonHint = Typography.traits.TypographyRender -> ApprovalRequest {
91
+ config {
92
+ color: muted
93
+ content: "Tell the submitter why — the reason is recorded on the request."
94
+ variant: caption
95
+ }
96
+ }
97
+
66
98
  trait CloseButton = Button.traits.ButtonRender -> ApprovalRequest {
67
99
  config {
68
100
  action: CLOSE
@@ -86,15 +118,30 @@ orbital ApprovalGateOrbital {
86
118
  trait ApprovalGateReview -> @rebindable ApprovalRequest [interaction, collection, approval] {
87
119
  initial: idle
88
120
 
121
+ ;; Every queue fetch is scoped to PENDING rows: a decided row must leave
122
+ ;; the queue, or its still-live Approve action re-emits ApprovalGranted
123
+ ;; with the ORIGINAL payload and replays a stale edit over current state.
89
124
  state idle {
90
125
  INIT -> idle
91
126
  (fetch ApprovalRequest {
127
+ filter: ["=", (object/get @entity status), "pending"]
92
128
  emit: { failure: ApprovalRequestLoadFailed, success: ApprovalRequestLoaded }
93
129
  })
94
130
  ApprovalRequestLoaded -> idle
95
131
  ApprovalRequestLoadFailed -> idle
132
+ ;; A replay failure can also arrive while the queue is closed.
133
+ APPROVAL_REPLAY_FAILED -> idle
134
+ (set @entity.id ?requestId)
135
+ (set @entity.status pending)
136
+ (set @entity.reviewer "")
137
+ (set @entity.decidedAt 0)
138
+ (persist update ApprovalRequest @entity {
139
+ emit: { failure: ApprovalRequestReviewFailed, success: ApprovalRequestReviewed }
140
+ })
141
+ (notify "Approval could not be applied — the request was reopened." error)
96
142
  OPEN -> loading when @config.enabled
97
143
  (fetch ApprovalRequest {
144
+ filter: ["=", (object/get @entity status), "pending"]
98
145
  emit: { failure: ApprovalRequestLoadFailed, success: ApprovalRequestLoaded }
99
146
  })
100
147
  (render-ui @config.reviewSlot {
@@ -127,20 +174,29 @@ orbital ApprovalGateOrbital {
127
174
  cols: 1
128
175
  entity: ?data
129
176
  fields: [{
177
+ label: What
178
+ name: subjectLabel
179
+ variant: h4
180
+ }, {
130
181
  label: Action
131
182
  name: gatedEvent
132
183
  variant: badge
184
+ }, {
185
+ label: "Requested by"
186
+ name: requestedByName
187
+ variant: caption
133
188
  }, {
134
189
  label: Requested
135
190
  name: requestedAt
136
191
  variant: caption
137
- }, {
138
- label: Status
139
- name: status
140
- variant: badge
141
192
  }]
142
193
  gap: sm
143
194
  itemActions: [{
195
+ event: OPEN_SUBJECT
196
+ icon: eye
197
+ label: View
198
+ variant: ghost
199
+ }, {
144
200
  event: APPROVE
145
201
  icon: check
146
202
  label: Approve
@@ -168,27 +224,92 @@ orbital ApprovalGateOrbital {
168
224
  }
169
225
 
170
226
  state reviewing {
171
- APPROVE -> reviewing
227
+ ;; The role guard IS the enforcement (both execution paths honor
228
+ ;; transition guards, for the button and for a bus-delivered event
229
+ ;; alike): empty approverRoles = ungated, otherwise the viewer's role
230
+ ;; must be listed or the decision does not fire.
231
+ APPROVE -> reviewing when (or ["=", (array/len @config.approverRoles), 0] (array/includes @config.approverRoles @user.role))
172
232
  (set @entity.id ?id)
173
233
  (set @entity.status approved)
174
234
  (set @entity.reviewer @user.id)
235
+ (set @entity.decidedAt (time/now))
175
236
  (persist update ApprovalRequest @entity {
176
237
  emit: { failure: ApprovalRequestReviewFailed, success: ApprovalRequestReviewed }
177
238
  })
178
239
  (emit ApprovalGranted { payload: ?row.payload, requestId: ?id })
179
- DENY -> reviewing
240
+ ;; Deny is a two-step: capture the written reason first, commit second.
241
+ DENY -> denying when (or ["=", (array/len @config.approverRoles), 0] (array/includes @config.approverRoles @user.role))
180
242
  (set @entity.id ?id)
243
+ (set @entity.subjectLabel (str/default (object/get ?row subjectLabel) ""))
244
+ (render-ui @config.reviewSlot {
245
+ children: [{
246
+ align: center
247
+ children: [@trait.DenyReasonTitle, @trait.CloseButton]
248
+ direction: horizontal
249
+ gap: sm
250
+ justify: between
251
+ type: stack
252
+ }, @trait.DenyReasonHint, @trait.ApprovalDivider, {
253
+ cancelEvent: CANCEL_DENY
254
+ entity: { id: @entity.id }
255
+ fields: (notes)
256
+ mode: edit
257
+ submitEvent: CONFIRM_DENY
258
+ type: form-section
259
+ }]
260
+ direction: vertical
261
+ gap: md
262
+ type: stack
263
+ })
264
+ OPEN_SUBJECT -> reviewing
265
+ ApprovalRequestReviewed -> loading
266
+ (fetch ApprovalRequest {
267
+ filter: ["=", (object/get @entity status), "pending"]
268
+ emit: { failure: ApprovalRequestLoadFailed, success: ApprovalRequestLoaded }
269
+ })
270
+ ApprovalRequestReviewFailed -> reviewing
271
+ (render-ui @config.reviewSlot {
272
+ children: [@trait.ErrorAlert]
273
+ type: stack
274
+ })
275
+ ;; A host reports the gated action could not be applied after approval
276
+ ;; (an entity-access denial, a failed write): reopen the request so it
277
+ ;; is never silently consumed.
278
+ APPROVAL_REPLAY_FAILED -> reviewing
279
+ (set @entity.id ?requestId)
280
+ (set @entity.status pending)
281
+ (set @entity.reviewer "")
282
+ (set @entity.decidedAt 0)
283
+ (persist update ApprovalRequest @entity {
284
+ emit: { failure: ApprovalRequestReviewFailed, success: ApprovalRequestReviewed }
285
+ })
286
+ (notify "Approval could not be applied — the request was reopened." error)
287
+ CLOSE -> idle
288
+ (render-ui @config.reviewSlot null)
289
+ }
290
+
291
+ state denying {
292
+ CONFIRM_DENY -> reviewing
293
+ (set @entity.id (object/get ?data id))
181
294
  (set @entity.status denied)
182
295
  (set @entity.reviewer @user.id)
296
+ (set @entity.decidedAt (time/now))
297
+ (set @entity.notes (str/default (object/get ?data notes) ""))
183
298
  (persist update ApprovalRequest @entity {
184
299
  emit: { failure: ApprovalRequestReviewFailed, success: ApprovalRequestReviewed }
185
300
  })
186
- (emit ApprovalDenied { requestId: ?id })
301
+ (emit ApprovalDenied { requestId: (object/get ?data id), reason: (str/default (object/get ?data notes) "") })
302
+ CANCEL_DENY -> loading
303
+ (fetch ApprovalRequest {
304
+ filter: ["=", (object/get @entity status), "pending"]
305
+ emit: { failure: ApprovalRequestLoadFailed, success: ApprovalRequestLoaded }
306
+ })
187
307
  ApprovalRequestReviewed -> loading
188
308
  (fetch ApprovalRequest {
309
+ filter: ["=", (object/get @entity status), "pending"]
189
310
  emit: { failure: ApprovalRequestLoadFailed, success: ApprovalRequestLoaded }
190
311
  })
191
- ApprovalRequestReviewFailed -> reviewing
312
+ ApprovalRequestReviewFailed -> denying
192
313
  (render-ui @config.reviewSlot {
193
314
  children: [@trait.ErrorAlert]
194
315
  type: stack
@@ -200,6 +321,7 @@ orbital ApprovalGateOrbital {
200
321
  state error {
201
322
  OPEN -> loading
202
323
  (fetch ApprovalRequest {
324
+ filter: ["=", (object/get @entity status), "pending"]
203
325
  emit: { failure: ApprovalRequestLoadFailed, success: ApprovalRequestLoaded }
204
326
  })
205
327
  (render-ui @config.reviewSlot {
@@ -222,9 +344,27 @@ orbital ApprovalGateOrbital {
222
344
  id : string!
223
345
  row : ApprovalRequest
224
346
  }
225
- @description "Signals that an approval request has been denied."
347
+ @description "Opens the deny step for a request; the decision commits with the written reason."
226
348
  @synonyms "reject, decline"
227
349
  @tier "domain"
350
+ OPEN_SUBJECT -> external {
351
+ id : string!
352
+ subjectId : string
353
+ subjectLabel : string
354
+ }
355
+ @description "A reviewer clicked View on a queue row. Hosts route subjectId to the subject record's own surface so the reviewer can read what they are approving. Scalars only — the row stays in the queue."
356
+ @synonyms "view subject, open record, inspect request, see the article"
357
+ @tier "domain"
358
+ CONFIRM_DENY {
359
+ data : DenyCommit
360
+ }
361
+ @description "The deny form committed; data carries the request id + the written reason."
362
+ @synonyms "submit denial, confirm rejection"
363
+ @tier "internal"
364
+ CANCEL_DENY
365
+ @description "The deny form was dismissed without committing; the queue repaints."
366
+ @synonyms "cancel denial, back to queue"
367
+ @tier "internal"
228
368
  CLOSE
229
369
  @description "Dismisses the review queue overlay."
230
370
  @synonyms "dismiss, hide, cancel, done"
@@ -233,13 +373,14 @@ orbital ApprovalGateOrbital {
233
373
  requestId : string
234
374
  payload : object
235
375
  }
236
- @description "Signals that an approval process has been successfully completed; carries the held payload for the downstream listener to replay."
376
+ @description "Signals that an approval process has been successfully completed; carries the held payload for the downstream listener to replay. Contract: if the replay fails (access denial, write failure), the host MUST emit APPROVAL_REPLAY_FAILED back so the request reopens."
237
377
  @synonyms "approved, authorized, confirmed, validated"
238
378
  @tier "domain"
239
379
  ApprovalDenied -> external {
240
380
  requestId : string
381
+ reason : string
241
382
  }
242
- @description "Indicates an approval request has been denied."
383
+ @description "Indicates an approval request has been denied, with the reviewer's written reason."
243
384
  @synonyms "rejected, declined"
244
385
  @tier "domain"
245
386
  ApprovalRequestLoaded {
@@ -291,9 +432,34 @@ orbital ApprovalGateOrbital {
291
432
  id : string!
292
433
  row : ApprovalRequest
293
434
  }
294
- @description "Indicates an approval request has been rejected."
435
+ @description "Opens the deny step for a request."
295
436
  @synonyms "reject, decline, deny"
296
437
  @tier "domain"
438
+ OPEN_SUBJECT {
439
+ id : string!
440
+ subjectId : string
441
+ subjectLabel : string
442
+ }
443
+ @description "A reviewer asked to view the subject of a queue row; the host routes the navigation."
444
+ @synonyms "view subject, open record, inspect"
445
+ @tier "domain"
446
+ CONFIRM_DENY {
447
+ data : DenyCommit
448
+ }
449
+ @description "The deny form committed with the written reason."
450
+ @synonyms "submit denial, confirm rejection"
451
+ @tier "internal"
452
+ CANCEL_DENY
453
+ @description "The deny form was dismissed; return to the queue."
454
+ @synonyms "cancel denial, back"
455
+ @tier "internal"
456
+ APPROVAL_REPLAY_FAILED {
457
+ requestId : string!
458
+ error : string
459
+ }
460
+ @description "The host could not apply the granted action (access denial, failed write). The request reopens as pending so a decision is never silently lost."
461
+ @synonyms "replay failed, apply failed, approval rollback"
462
+ @tier "domain"
297
463
  ApprovalRequestLoaded {
298
464
  data : [ApprovalRequest]
299
465
  }
@@ -330,7 +496,7 @@ orbital ApprovalGateOrbital {
330
496
  @tier "policy"
331
497
  approverRoles : [string] = []
332
498
  @label "Who can approve requests?"
333
- @description "Roles eligible to approve. The host application enforces role-gating on the page."
499
+ @description "Roles eligible to decide. ENFORCED by the APPROVE/DENY transition guards: a viewer whose @user.role is not listed cannot fire a decision, from the button or from the bus. Empty = every signed-in viewer may decide (ungated)."
334
500
  @synonyms "approver, reviewer, authorized roles, sign-off roles, who approves"
335
501
  @tier "policy"
336
502
  threshold : number = 0
@@ -370,4 +536,4 @@ orbital ApprovalGateOrbital {
370
536
  }
371
537
 
372
538
  page "/approvals/queue" as ApprovalGateReviewPage -> ApprovalGateReview
373
- }
539
+ }
@@ -20,9 +20,14 @@ orbital StatusLifecycleOrbital {
20
20
  state idle {
21
21
  ChangeStatusRequested -> idle when (and @config.enabled (array/some @config.transitions (fn t (and ["=", (object/get @t from), ?from] ["=", (object/get @t to), ?to]))))
22
22
  (persist update @config.targetEntity (object/set { id: ?id } @config.statusField ?to) {
23
- emit: { failure: StatusChangeFailed, success: StatusChanged }
23
+ emit: { failure: StatusChangeFailed, success: StatusPersisted }
24
24
  })
25
- (emit StatusChanged { entityId: ?id, status: ?to })
25
+ ;; StatusChanged is re-emitted ONLY off the persist's success (guarded
26
+ ;; on ?id against the optimistic/authoritative double-fire) — the old
27
+ ;; synchronous emit fired even when the write failed, so a denied
28
+ ;; mutation still looked like a completed transition downstream.
29
+ StatusPersisted -> idle when ["!=", (str/default ?id ""), ""]
30
+ (emit StatusChanged { entityId: ?id, status: (str/default (object/get ?row @config.statusField) "") })
26
31
  StatusChangeFailed -> idle
27
32
  }
28
33
 
@@ -31,7 +36,11 @@ orbital StatusLifecycleOrbital {
31
36
  entityId : string
32
37
  status : string
33
38
  }
34
- StatusChangeFailed {
39
+ StatusPersisted {
40
+ id : string
41
+ row : StatusRecord
42
+ }
43
+ StatusChangeFailed -> external {
35
44
  error : string
36
45
  code : string
37
46
  }
@@ -43,6 +52,10 @@ orbital StatusLifecycleOrbital {
43
52
  from : string
44
53
  to : string
45
54
  }
55
+ StatusPersisted {
56
+ id : string
57
+ row : StatusRecord
58
+ }
46
59
  StatusChangeFailed {
47
60
  error : string
48
61
  code : string
@@ -30,6 +30,7 @@ orbital BillableHourOrbital {
30
30
  itemActions: @config.itemActions
31
31
  look: @config.tableLook
32
32
  maxInlineActions: 2
33
+ selfFetch: false
33
34
  }
34
35
 
35
36
  listens {
@@ -38,6 +38,7 @@ orbital BrowseItemOrbital {
38
38
  itemActions: @config.itemActions
39
39
  maxInlineActions: @config.maxInlineActions
40
40
  pageSize: @config.displayPageSize
41
+ selfFetch: false
41
42
  }
42
43
 
43
44
  listens {
@@ -55,6 +56,7 @@ orbital BrowseItemOrbital {
55
56
  maxInlineActions: @config.maxInlineActions
56
57
  pageSize: @config.displayPageSize
57
58
  selectable: @config.selectable
59
+ selfFetch: false
58
60
  }
59
61
 
60
62
  listens {
@@ -69,6 +71,7 @@ orbital BrowseItemOrbital {
69
71
  itemClickEvent: @config.itemClickEvent
70
72
  maxInlineActions: @config.maxInlineActions
71
73
  variant: compact
74
+ selfFetch: false
72
75
  }
73
76
 
74
77
  listens {
@@ -1,6 +1,10 @@
1
1
  app std-image-upload-multi "1.0.0"
2
2
  "std-image-upload-multi — multi-image upload with drop zone, gallery preview, and per-batch progress."
3
3
  orbital UploadedImageOrbital {
4
+ type UploadBatchFile = { name : string!, size : number, type : string, content : string }
5
+
6
+ type StoredImageItem = { id : string!, key : string, url : string, name : string, sizeBytes : number, mimeType : string }
7
+
4
8
  type UploadedImageLoaded = Event { data : [UploadedImage] } "Fired when the uploaded-image collection finishes loading"
5
9
 
6
10
  type UploadedImageLoadFailed = Event { error : string, code : string } "Fired when the uploaded-image collection fails to load"
@@ -96,8 +100,13 @@ orbital UploadedImageOrbital {
96
100
  type: stack
97
101
  })
98
102
  UPLOAD -> uploading
99
- (persist create UploadedImage ?files {
100
- emit: { failure: UploadedImageUploadFailed, success: UploadedImageCreated }
103
+ (call-service storage uploadMany {
104
+ acl: public
105
+ bucket: @config.bucket
106
+ files: ?files
107
+ maxSize: @config.maxBytesPerImage
108
+ } {
109
+ emit: { failure: UploadedImageUploadFailed, success: UploadedImageStored }
101
110
  })
102
111
  (render-ui main {
103
112
  align: center
@@ -133,6 +142,12 @@ orbital UploadedImageOrbital {
133
142
  }
134
143
 
135
144
  state uploading {
145
+ ;; The storage service owns the bytes; rows persist only the returned
146
+ ;; addresses — ?items is shaped for this bulk create (url/name/sizeBytes/mimeType).
147
+ UploadedImageStored -> uploading
148
+ (persist create UploadedImage ?items {
149
+ emit: { failure: UploadedImageUploadFailed, success: UploadedImageCreated }
150
+ })
136
151
  UploadedImageCreated -> loading
137
152
  (fetch UploadedImage {
138
153
  emit: { failure: UploadedImageLoadFailed, success: UploadedImageLoaded }
@@ -192,7 +207,7 @@ orbital UploadedImageOrbital {
192
207
 
193
208
  emits {
194
209
  UPLOAD {
195
- files : object!
210
+ files : [UploadBatchFile]!
196
211
  }
197
212
  @description "Signals that an image upload process has begun."
198
213
  @synonyms "start, initiate, begin"
@@ -216,6 +231,13 @@ orbital UploadedImageOrbital {
216
231
  @description "A new image has been successfully uploaded."
217
232
  @synonyms "added, new, received"
218
233
  @tier "domain"
234
+ UploadedImageStored {
235
+ items : [StoredImageItem]
236
+ count : number
237
+ }
238
+ @description "The storage service accepted the whole batch; items carry each file's stored address and metadata, ready to persist."
239
+ @synonyms "uploaded, stored, saved to storage"
240
+ @tier "internal"
219
241
  UploadedImageUploadFailed {
220
242
  error : string
221
243
  code : string
@@ -240,7 +262,7 @@ orbital UploadedImageOrbital {
240
262
 
241
263
  listens {
242
264
  UPLOAD {
243
- files : object!
265
+ files : [UploadBatchFile]!
244
266
  }
245
267
  @description "Indicates a file upload has begun."
246
268
  @synonyms "start, begin, initiate"
@@ -270,6 +292,13 @@ orbital UploadedImageOrbital {
270
292
  @description "A new image has been successfully uploaded."
271
293
  @synonyms "added, received, new image"
272
294
  @tier "domain"
295
+ UploadedImageStored {
296
+ items : [StoredImageItem]
297
+ count : number
298
+ }
299
+ @description "The storage service accepted the batch; the items are persisted as uploaded-image rows."
300
+ @synonyms "uploaded, stored, saved to storage"
301
+ @tier "internal"
273
302
  UploadedImageUploadFailed {
274
303
  error : string
275
304
  code : string
@@ -309,6 +338,10 @@ orbital UploadedImageOrbital {
309
338
  @label "MIME filter"
310
339
  @description "Browser accept filter for the file picker"
311
340
  @tier "domain"
341
+ bucket : string = "images"
342
+ @label "Storage bucket"
343
+ @description "Object-storage bucket the batch uploads into"
344
+ @tier "domain"
312
345
  }
313
346
  }
314
347
 
@@ -24,6 +24,7 @@ orbital ModalRecordOrbital {
24
24
  name : string @description "A user-facing identifier for the modal." @synonyms "title, label, identifier"
25
25
  description : string @description "A textual explanation of the modal's content or function." @synonyms "details, explanation, notes, summary"
26
26
  status : "active" | "inactive" | "pending" @description "Current operational state of the record." @synonyms "state, condition, flag" = active
27
+ seedRow : ModalRecord @intrinsic @description "The row an edit-mode OPEN pre-fills from when its payload carries no row. Stashed by SEED — hosts route the surface's loaded event here (e.g. a document look's RecordItemLoaded) so scalar-only action events ({ id }) can still open a fully seeded settings form." = {}
27
28
  createdAt : string
28
29
  }
29
30
 
@@ -35,12 +36,16 @@ orbital ModalRecordOrbital {
35
36
  (fetch ModalRecord {
36
37
  emit: { failure: ModalRecordLoadFailed, success: ModalRecordLoaded }
37
38
  })
39
+ ;; stash only — a SEED never opens anything; OPEN reads it on demand
40
+ SEED -> closed
41
+ (set @entity.seedRow (object/merge ?row {}))
38
42
  ;; Mode-aware guard: in create mode the modal opens unconditionally
39
- ;; (no row exists yet); in edit mode @payload.row must be present
40
- ;; or the OPEN is rejected (no silent first-row fallback). The
41
- ;; @config.mode literal is substituted at molecule inline time —
42
- ;; atoms validate this with @config allowed-but-unresolved.
43
- OPEN -> open when (or ["=", @config.mode, create] ?row)
43
+ ;; (no row exists yet); in edit mode the row must be present — on the
44
+ ;; OPEN payload or already stashed by SEED — or the OPEN is rejected
45
+ ;; (no silent first-row fallback). The @config.mode literal is
46
+ ;; substituted at molecule inline time — atoms validate this with
47
+ ;; @config allowed-but-unresolved.
48
+ OPEN -> open when (or ["=", @config.mode, create] ?row ["!=", (str/default (object/get @entity.seedRow id) ""), ""])
44
49
  (set @entity.id ?id)
45
50
  (render-ui @config.detailSlot {
46
51
  children: [{
@@ -54,7 +59,7 @@ orbital ModalRecordOrbital {
54
59
  type: stack
55
60
  }, { type: divider }, {
56
61
  cancelEvent: CLOSE
57
- entity: ?row
62
+ entity: (object/merge @entity.seedRow (object/merge ?row {}))
58
63
  fieldOverrides: @config.fieldOverrides
59
64
  fields: @config.fields
60
65
  mode: @config.mode
@@ -68,6 +73,9 @@ orbital ModalRecordOrbital {
68
73
  }
69
74
 
70
75
  state open {
76
+ ;; a refetch echo while the modal is open refreshes the stash in place
77
+ SEED -> open
78
+ (set @entity.seedRow (object/merge ?row {}))
71
79
  CLOSE -> closed
72
80
  (render-ui @config.detailSlot null)
73
81
  (notify Cancelled info)
@@ -104,6 +112,12 @@ orbital ModalRecordOrbital {
104
112
  @description "A request to display the modal window."
105
113
  @synonyms "show, display, present, reveal"
106
114
  @tier "domain"
115
+ SEED {
116
+ row : ModalRecord
117
+ }
118
+ @description "Stash the row an edit-mode OPEN will pre-fill from. Hosts route the owning surface's loaded event here (e.g. RecordItemLoaded from a document look) so an OPEN carrying only { id } still opens a fully seeded form."
119
+ @synonyms "preload row, stash record, seed form, prefill"
120
+ @tier "domain"
107
121
  SAVE {
108
122
  data : @entity!
109
123
  }