@almadar/std 16.191.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 (103) 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-record-detail.lolo +37 -0
  7. package/behaviors/lolo/ui/core/atoms/std-tag-taxonomy.lolo +176 -4
  8. package/behaviors/lolo/ui/core/atoms/std-version-history.lolo +3 -1
  9. package/behaviors/lolo/ui/core/molecules/ui-calendar-grid.lolo +25 -1
  10. package/behaviors/lolo/ui/core/molecules/ui-data-grid.lolo +37 -1
  11. package/behaviors/lolo/ui/core/molecules/ui-data-list.lolo +46 -1
  12. package/behaviors/lolo/ui/core/molecules/ui-document-details.lolo +30 -2
  13. package/behaviors/lolo/ui/core/molecules/ui-document-panel.lolo +14 -0
  14. package/behaviors/lolo/ui/core/molecules/ui-positioned-canvas.lolo +19 -1
  15. package/behaviors/lolo/ui/core/molecules/ui-reply-tree.lolo +21 -1
  16. package/behaviors/lolo/ui/core/molecules/ui-sortable-list.lolo +15 -1
  17. package/behaviors/lolo/ui/core/molecules/ui-table-view.lolo +36 -1
  18. package/behaviors/lolo/ui/core/organisms/ui-detail-panel.lolo +41 -1
  19. package/behaviors/lolo/ui/core/organisms/ui-entity-cards.lolo +34 -1
  20. package/behaviors/lolo/ui/core/organisms/ui-entity-list.lolo +30 -1
  21. package/behaviors/lolo/ui/core/organisms/ui-entity-table.lolo +36 -1
  22. package/behaviors/lolo/ui/core/organisms/ui-form-section.lolo +39 -1
  23. package/behaviors/lolo/ui/core/organisms/ui-form.lolo +39 -1
  24. package/behaviors/lolo/ui/core/organisms/ui-master-detail.lolo +24 -1
  25. package/behaviors/lolo/ui/core/organisms/ui-media-gallery.lolo +30 -1
  26. package/behaviors/lolo/ui/core/organisms/ui-stat-card.lolo +37 -1
  27. package/behaviors/lolo/ui/core/organisms/ui-timeline.lolo +18 -1
  28. package/behaviors/lolo/ui/core/templates/ui-counter-template.lolo +21 -1
  29. package/behaviors/lolo/ui/core/templates/ui-generic-app-template.lolo +16 -1
  30. package/behaviors/lolo/ui/marketing/organisms/ui-book-chapter-view.lolo +12 -1
  31. package/behaviors/lolo/ui/marketing/organisms/ui-book-table-of-contents.lolo +13 -1
  32. package/behaviors/lolo/ui/marketing/organisms/ui-book-viewer.lolo +23 -1
  33. package/behaviors/lolo/ui/marketing/organisms/ui-case-study-organism.lolo +23 -1
  34. package/behaviors/lolo/ui/marketing/organisms/ui-feature-grid-organism.lolo +24 -1
  35. package/behaviors/lolo/ui/marketing/organisms/ui-hero-organism.lolo +22 -1
  36. package/behaviors/lolo/ui/marketing/organisms/ui-pricing-organism.lolo +23 -1
  37. package/behaviors/lolo/ui/marketing/organisms/ui-showcase-organism.lolo +24 -1
  38. package/behaviors/lolo/ui/marketing/organisms/ui-stats-organism.lolo +22 -1
  39. package/behaviors/lolo/ui/marketing/organisms/ui-step-flow-organism.lolo +25 -1
  40. package/behaviors/lolo/ui/marketing/organisms/ui-team-organism.lolo +23 -1
  41. package/behaviors/lolo/ui/marketing/templates/ui-about-page-template.lolo +11 -1
  42. package/behaviors/lolo/ui/marketing/templates/ui-feature-detail-page-template.lolo +11 -1
  43. package/behaviors/lolo/ui/marketing/templates/ui-landing-page-template.lolo +13 -1
  44. package/behaviors/lolo/ui/marketing/templates/ui-pricing-page-template.lolo +11 -1
  45. package/behaviors/registry/infra/atoms/std-approval-gate.orb +717 -45
  46. package/behaviors/registry/infra/atoms/std-status-lifecycle.orb +85 -10
  47. package/behaviors/registry/ui/core/atoms/std-billable-hour.orb +5 -2
  48. package/behaviors/registry/ui/core/atoms/std-browse.orb +12 -0
  49. package/behaviors/registry/ui/core/atoms/std-image-upload-multi.orb +129 -7
  50. package/behaviors/registry/ui/core/atoms/std-record-detail.orb +316 -0
  51. package/behaviors/registry/ui/core/atoms/std-tag-taxonomy.orb +539 -1
  52. package/behaviors/registry/ui/core/atoms/std-version-history.orb +5 -1
  53. package/behaviors/registry/ui/core/molecules/ui-calendar-grid.orb +43 -0
  54. package/behaviors/registry/ui/core/molecules/ui-data-grid.orb +55 -0
  55. package/behaviors/registry/ui/core/molecules/ui-data-list.orb +64 -0
  56. package/behaviors/registry/ui/core/molecules/ui-document-details.orb +106 -1
  57. package/behaviors/registry/ui/core/molecules/ui-document-panel.orb +18 -0
  58. package/behaviors/registry/ui/core/molecules/ui-positioned-canvas.orb +37 -0
  59. package/behaviors/registry/ui/core/molecules/ui-reply-tree.orb +39 -0
  60. package/behaviors/registry/ui/core/molecules/ui-sortable-list.orb +45 -0
  61. package/behaviors/registry/ui/core/molecules/ui-table-view.orb +54 -0
  62. package/behaviors/registry/ui/core/organisms/ui-detail-panel.orb +59 -0
  63. package/behaviors/registry/ui/core/organisms/ui-entity-cards.orb +52 -0
  64. package/behaviors/registry/ui/core/organisms/ui-entity-list.orb +48 -0
  65. package/behaviors/registry/ui/core/organisms/ui-entity-table.orb +54 -0
  66. package/behaviors/registry/ui/core/organisms/ui-form-section.orb +57 -0
  67. package/behaviors/registry/ui/core/organisms/ui-form.orb +57 -0
  68. package/behaviors/registry/ui/core/organisms/ui-master-detail.orb +42 -0
  69. package/behaviors/registry/ui/core/organisms/ui-media-gallery.orb +48 -0
  70. package/behaviors/registry/ui/core/organisms/ui-stat-card.orb +55 -0
  71. package/behaviors/registry/ui/core/organisms/ui-timeline.orb +36 -0
  72. package/behaviors/registry/ui/core/templates/ui-counter-template.orb +39 -0
  73. package/behaviors/registry/ui/core/templates/ui-generic-app-template.orb +34 -0
  74. package/behaviors/registry/ui/marketing/organisms/ui-book-chapter-view.orb +30 -0
  75. package/behaviors/registry/ui/marketing/organisms/ui-book-table-of-contents.orb +31 -0
  76. package/behaviors/registry/ui/marketing/organisms/ui-book-viewer.orb +41 -0
  77. package/behaviors/registry/ui/marketing/organisms/ui-case-study-organism.orb +41 -0
  78. package/behaviors/registry/ui/marketing/organisms/ui-feature-grid-organism.orb +42 -0
  79. package/behaviors/registry/ui/marketing/organisms/ui-hero-organism.orb +40 -0
  80. package/behaviors/registry/ui/marketing/organisms/ui-pricing-organism.orb +41 -0
  81. package/behaviors/registry/ui/marketing/organisms/ui-showcase-organism.orb +42 -0
  82. package/behaviors/registry/ui/marketing/organisms/ui-stats-organism.orb +40 -0
  83. package/behaviors/registry/ui/marketing/organisms/ui-step-flow-organism.orb +43 -0
  84. package/behaviors/registry/ui/marketing/organisms/ui-team-organism.orb +41 -0
  85. package/behaviors/registry/ui/marketing/templates/ui-about-page-template.orb +29 -0
  86. package/behaviors/registry/ui/marketing/templates/ui-feature-detail-page-template.orb +29 -0
  87. package/behaviors/registry/ui/marketing/templates/ui-landing-page-template.orb +31 -0
  88. package/behaviors/registry/ui/marketing/templates/ui-pricing-page-template.orb +29 -0
  89. package/dist/behaviors/exports-reader.js +2566 -44
  90. package/dist/behaviors/functions/index.d.ts +134 -11
  91. package/dist/behaviors/functions/index.js +2565 -45
  92. package/dist/behaviors/index.d.ts +1 -1
  93. package/dist/behaviors/index.js +2567 -45
  94. package/dist/behaviors/query.js +2566 -44
  95. package/dist/behaviors-embeddings.hash.json +3 -3
  96. package/dist/behaviors-embeddings.json +11 -11
  97. package/dist/behaviors-registry.json +96 -53
  98. package/dist/index.d.ts +1 -1
  99. package/dist/index.js +2567 -45
  100. package/dist/knob-embeddings.hash.json +3 -3
  101. package/dist/knob-embeddings.json +1 -1
  102. package/dist/registry/factory-signatures.json +1 -1
  103. package/package.json +2 -2
@@ -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
 
@@ -20,6 +20,8 @@ orbital RecordItemOrbital {
20
20
  status : "active" | "inactive" | "pending" @description "The record's current state." @synonyms "state, condition, stage" = active
21
21
  slug : string @description "URL-friendly identifier, for records opened by address instead of id." @synonyms "permalink, url key, address" = ""
22
22
  content : string @description "The record's rich-text body as HTML, edited in place by the document look." @synonyms "body, text, document, article body" = ""
23
+ coverUrl : string @intrinsic @description "The document look's cover/hero image URL, stamped from the loaded row's coverField." = ""
24
+ iconName : string @intrinsic @description "The document look's icon name, stamped from the loaded row's iconField." = ""
23
25
  loadedRow : object @intrinsic @description "The full row exactly as last fetched (relations hydrated when config.include asks). The details drawer binds it, and hosts that need it (e.g. a settings modal) stash it from RecordItemLoaded — the document panel itself takes only scalar props and emits scalar payloads ({ id, title })." = {}
24
26
  openDraft : string @intrinsic @description "Set from the navigate payload when the surface was opened on a record that was JUST minted blank; read once the row loads to decide whether the document body opens authoring (draft) or read-only. Absent on a refresh or a normal open, so the draft entry never repeats." @synonyms "new document, just created, draft open" = ""
25
27
  createdAt : string
@@ -128,6 +130,8 @@ orbital RecordItemOrbital {
128
130
  (set @entity.name (object/get ?data @config.titleField))
129
131
  (set @entity.description (str/default (object/get ?data @config.subtitleField) ""))
130
132
  (set @entity.content (object/get ?data @config.contentField))
133
+ (set @entity.coverUrl (str/default (object/get ?data @config.coverField) ""))
134
+ (set @entity.iconName (str/default (object/get ?data @config.iconField) ""))
131
135
  (render-ui main @config.documentEditingContent)
132
136
  RecordItemLoaded -> viewing when ["!=", (str/default (object/get @entity openDraft) ""), "1"]
133
137
  ;; (object/merge … {}) is a type adapter only: type_compat lacks "declared object accepts a named row type" (ledgered)
@@ -136,6 +140,8 @@ orbital RecordItemOrbital {
136
140
  (set @entity.name (object/get ?data @config.titleField))
137
141
  (set @entity.description (str/default (object/get ?data @config.subtitleField) ""))
138
142
  (set @entity.content (object/get ?data @config.contentField))
143
+ (set @entity.coverUrl (str/default (object/get ?data @config.coverField) ""))
144
+ (set @entity.iconName (str/default (object/get ?data @config.iconField) ""))
139
145
  (if ["=", @config.detailLook, document]
140
146
  (render-ui main @config.documentBodyContent)
141
147
  (if ["=", @config.detailLook, slide-over]
@@ -149,6 +155,8 @@ orbital RecordItemOrbital {
149
155
  (set @entity.name (object/get (array/first ?data) @config.titleField))
150
156
  (set @entity.description (str/default (object/get (array/first ?data) @config.subtitleField) ""))
151
157
  (set @entity.content (object/get (array/first ?data) @config.contentField))
158
+ (set @entity.coverUrl (str/default (object/get (array/first ?data) @config.coverField) ""))
159
+ (set @entity.iconName (str/default (object/get (array/first ?data) @config.iconField) ""))
152
160
  (render-ui main @config.documentEditingContent)
153
161
  RecordItemSlugLoaded -> viewing when (and [">", (array/len ?data), 0] ["!=", (str/default (object/get @entity openDraft) ""), "1"])
154
162
  (set @entity.loadedRow (array/first ?data))
@@ -156,6 +164,8 @@ orbital RecordItemOrbital {
156
164
  (set @entity.name (object/get (array/first ?data) @config.titleField))
157
165
  (set @entity.description (str/default (object/get (array/first ?data) @config.subtitleField) ""))
158
166
  (set @entity.content (object/get (array/first ?data) @config.contentField))
167
+ (set @entity.coverUrl (str/default (object/get (array/first ?data) @config.coverField) ""))
168
+ (set @entity.iconName (str/default (object/get (array/first ?data) @config.iconField) ""))
159
169
  (if ["=", @config.detailLook, document]
160
170
  (render-ui main @config.documentBodyContent)
161
171
  (if ["=", @config.detailLook, slide-over]
@@ -245,6 +255,8 @@ orbital RecordItemOrbital {
245
255
  (set @entity.name (object/get ?data @config.titleField))
246
256
  (set @entity.description (str/default (object/get ?data @config.subtitleField) ""))
247
257
  (set @entity.content (object/get ?data @config.contentField))
258
+ (set @entity.coverUrl (str/default (object/get ?data @config.coverField) ""))
259
+ (set @entity.iconName (str/default (object/get ?data @config.iconField) ""))
248
260
  (if ["=", @config.detailLook, document]
249
261
  (render-ui main @config.documentBodyContent)
250
262
  (if ["=", @config.detailLook, slide-over]
@@ -258,6 +270,8 @@ orbital RecordItemOrbital {
258
270
  (set @entity.name (object/get (array/first ?data) @config.titleField))
259
271
  (set @entity.description (str/default (object/get (array/first ?data) @config.subtitleField) ""))
260
272
  (set @entity.content (object/get (array/first ?data) @config.contentField))
273
+ (set @entity.coverUrl (str/default (object/get (array/first ?data) @config.coverField) ""))
274
+ (set @entity.iconName (str/default (object/get (array/first ?data) @config.iconField) ""))
261
275
  (if ["=", @config.detailLook, document]
262
276
  (render-ui main @config.documentBodyContent)
263
277
  (if ["=", @config.detailLook, slide-over]
@@ -507,6 +521,16 @@ orbital RecordItemOrbital {
507
521
  @description "Name of the field on the loaded record that carries the rich-text HTML body the document look renders and edits (e.g. content, body, articleText)."
508
522
  @synonyms "body field, content field, document field"
509
523
  @tier "presentation"
524
+ coverField : string = ""
525
+ @label "Which field is the cover image?"
526
+ @description "Name of the image-typed field on the loaded record painted as the document look's full-bleed hero banner (e.g. coverImage, heroImage). Empty = no banner, the default."
527
+ @synonyms "cover field, hero image field, banner field"
528
+ @tier "presentation"
529
+ iconField : string = ""
530
+ @label "Which field is the document icon?"
531
+ @description "Name of the field on the loaded record whose icon name renders beside the document title (e.g. icon). Empty = no icon, the default."
532
+ @synonyms "icon field, glyph field"
533
+ @tier "presentation"
510
534
  editLabel : string = "Edit"
511
535
  @label "Edit button label"
512
536
  @description "Label of the quiet button above the document body that starts an editing session (the body itself is also clickable)."
@@ -587,6 +611,8 @@ orbital RecordItemOrbital {
587
611
  @tier "internal"
588
612
  documentBodyContent : render-ui = {
589
613
  actions: @config.actions
614
+ coverImage: @entity.coverUrl
615
+ icon: @entity.iconName
590
616
  editEvent: EDIT_CONTENT
591
617
  editLabel: @config.editLabel
592
618
  id: @entity.id
@@ -604,6 +630,8 @@ orbital RecordItemOrbital {
604
630
  @tier "internal"
605
631
  documentEditingContent : render-ui = {
606
632
  actions: @config.actions
633
+ coverImage: @entity.coverUrl
634
+ icon: @entity.iconName
607
635
  autosaveHint: @config.autosaveHint
608
636
  contentChangeEvent: CONTENT_CHANGE
609
637
  doneEvent: DONE_EDITING
@@ -675,6 +703,14 @@ orbital RecordItemOrbital {
675
703
  @description "A property in the settings rail committed in place; patch carries the row id plus the one changed field, ready to hand straight to a persist update. Quiet contract: the host persists without refetching or repainting the open document."
676
704
  @synonyms "property saved, metadata changed, field committed, update property"
677
705
  @tier "domain"
706
+ RELATION_OPEN -> external {
707
+ field : string!
708
+ id : string!
709
+ name : string
710
+ }
711
+ @description "A relation chip in the details drawer was clicked (a tag, the author, the category); id is the linked row's id. Hosts route it to the linked record's own surface — a navigate, a scoped overlay, a filtered list."
712
+ @synonyms "open relation, linked record clicked, tag clicked, chip clicked"
713
+ @tier "domain"
678
714
  }
679
715
 
680
716
  listens {
@@ -718,6 +754,7 @@ orbital RecordItemOrbital {
718
754
  detailsContent : render-ui = {
719
755
  entity: @entity.loadedRow
720
756
  fields: @config.metaFields
757
+ relationEvent: RELATION_OPEN
721
758
  title: @config.detailsTitle
722
759
  type: document-details
723
760
  }