@almadar/std 16.187.0 → 16.189.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 (102) hide show
  1. package/behaviors/lolo/ui/core/.hand-authored.json +4 -4
  2. package/behaviors/lolo/ui/core/atoms/std-board.lolo +138 -4
  3. package/behaviors/lolo/ui/core/atoms/std-browse.lolo +148 -8
  4. package/behaviors/lolo/ui/core/atoms/std-calendar.lolo +161 -11
  5. package/behaviors/lolo/ui/core/atoms/std-event-log.lolo +255 -2
  6. package/behaviors/lolo/ui/core/atoms/std-gallery.lolo +148 -1
  7. package/behaviors/lolo/ui/core/atoms/std-list.lolo +193 -1
  8. package/behaviors/lolo/ui/core/atoms/std-record-detail.lolo +731 -0
  9. package/behaviors/lolo/ui/core/atoms/std-related.lolo +5 -1
  10. package/behaviors/lolo/ui/core/atoms/std-selection.lolo +182 -1
  11. package/behaviors/lolo/ui/core/atoms/std-tree.lolo +113 -3
  12. package/behaviors/lolo/ui/core/atoms/std-version-history.lolo +1 -1
  13. package/behaviors/lolo/ui/core/molecules/ui-document-details.lolo +80 -0
  14. package/behaviors/lolo/ui/core/molecules/ui-document-panel.lolo +213 -0
  15. package/behaviors/lolo/ui/core/molecules/{ui-rich-block-editor.lolo → ui-rich-text-editor.lolo} +25 -25
  16. package/behaviors/lolo/ui/game/atoms/std-area-fill.lolo +93 -0
  17. package/behaviors/lolo/ui/game/atoms/std-choice-gate.lolo +100 -0
  18. package/behaviors/lolo/ui/game/atoms/std-coef-pad.lolo +114 -0
  19. package/behaviors/lolo/ui/game/atoms/std-curve-fog.lolo +50 -0
  20. package/behaviors/lolo/ui/game/atoms/std-loop-lock.lolo +79 -0
  21. package/behaviors/lolo/ui/game/atoms/std-pad-lock.lolo +97 -0
  22. package/behaviors/lolo/ui/game/atoms/std-patrol-hazard.lolo +14 -7
  23. package/behaviors/lolo/ui/game/atoms/std-platformer-body.lolo +48 -5
  24. package/behaviors/lolo/ui/game/atoms/std-velocity-gate.lolo +101 -0
  25. package/behaviors/lolo/ui/game/atoms/std-zone-card.lolo +95 -0
  26. package/behaviors/lolo/ui/game/organisms/std-platformer-board-2d.lolo +9 -0
  27. package/behaviors/registry/infra/atoms/std-approval-gate.orb +16 -8
  28. package/behaviors/registry/infra/atoms/std-cache-aside.orb +62 -31
  29. package/behaviors/registry/infra/atoms/std-circuit-breaker.orb +32 -16
  30. package/behaviors/registry/infra/atoms/std-data-erasure.orb +16 -8
  31. package/behaviors/registry/infra/atoms/std-mod-queue.orb +16 -8
  32. package/behaviors/registry/infra/atoms/std-rate-limiter.orb +28 -14
  33. package/behaviors/registry/infra/organisms/std-status-lifecycle-control.orb +2 -1
  34. package/behaviors/registry/ml/organisms/std-escalating-decision.orb +10 -5
  35. package/behaviors/registry/ml/organisms/std-knowledge-tracing.orb +4 -2
  36. package/behaviors/registry/ui/core/atoms/std-billable-hour.orb +2 -1
  37. package/behaviors/registry/ui/core/atoms/std-board.orb +355 -2
  38. package/behaviors/registry/ui/core/atoms/std-browse.orb +304 -31
  39. package/behaviors/registry/ui/core/atoms/std-calendar.orb +393 -25
  40. package/behaviors/registry/ui/core/atoms/std-dashboard-grid-embedded.orb +2 -1
  41. package/behaviors/registry/ui/core/atoms/std-esign-flow.orb +4 -2
  42. package/behaviors/registry/ui/core/atoms/std-esign-request.orb +6 -3
  43. package/behaviors/registry/ui/core/atoms/std-event-log.orb +773 -0
  44. package/behaviors/registry/ui/core/atoms/std-export.orb +4 -2
  45. package/behaviors/registry/ui/core/atoms/std-gallery.orb +390 -0
  46. package/behaviors/registry/ui/core/atoms/std-import.orb +4 -2
  47. package/behaviors/registry/ui/core/atoms/std-list.orb +451 -0
  48. package/behaviors/registry/ui/core/atoms/std-record-detail.orb +3360 -0
  49. package/behaviors/registry/ui/core/atoms/std-related.orb +23 -1
  50. package/behaviors/registry/ui/core/atoms/std-selection.orb +436 -0
  51. package/behaviors/registry/ui/core/atoms/std-tree.orb +292 -1
  52. package/behaviors/registry/ui/core/atoms/std-version-history.orb +1 -1
  53. package/behaviors/registry/ui/core/molecules/ui-document-details.orb +342 -0
  54. package/behaviors/registry/ui/core/molecules/ui-document-panel.orb +655 -0
  55. package/behaviors/registry/ui/core/molecules/{ui-rich-block-editor.orb → ui-rich-text-editor.orb} +84 -97
  56. package/behaviors/registry/ui/game/atoms/std-area-fill.orb +955 -0
  57. package/behaviors/registry/ui/game/atoms/std-choice-gate.orb +855 -0
  58. package/behaviors/registry/ui/game/atoms/std-coef-pad.orb +928 -0
  59. package/behaviors/registry/ui/game/atoms/std-curve-fog.orb +337 -0
  60. package/behaviors/registry/ui/game/atoms/std-fx-trail.orb +1 -1
  61. package/behaviors/registry/ui/game/atoms/std-gate-lock.orb +1 -1
  62. package/behaviors/registry/ui/game/atoms/std-lives.orb +1 -1
  63. package/behaviors/registry/ui/game/atoms/std-loop-lock.orb +785 -0
  64. package/behaviors/registry/ui/game/atoms/std-pad-lock.orb +933 -0
  65. package/behaviors/registry/ui/game/atoms/std-patrol-hazard.orb +190 -44
  66. package/behaviors/registry/ui/game/atoms/std-platformer-body.orb +1330 -653
  67. package/behaviors/registry/ui/game/atoms/std-velocity-gate.orb +1040 -0
  68. package/behaviors/registry/ui/game/atoms/std-zone-card.orb +642 -0
  69. package/behaviors/registry/ui/game/organisms/std-arcade-board-3d.orb +8 -4
  70. package/behaviors/registry/ui/game/organisms/std-crew-board-2d.orb +6 -3
  71. package/behaviors/registry/ui/game/organisms/std-dungeon-board-2d.orb +10 -5
  72. package/behaviors/registry/ui/game/organisms/std-market-board-3d.orb +8 -4
  73. package/behaviors/registry/ui/game/organisms/std-objective-board-3d.orb +8 -4
  74. package/behaviors/registry/ui/game/organisms/std-platformer-board-2d.orb +36 -1
  75. package/behaviors/registry/ui/game/organisms/std-survival-board-3d.orb +8 -4
  76. package/behaviors/registry/ui/game/organisms/std-tactics-board-2d.orb +12 -6
  77. package/behaviors/registry/ui/game/organisms/std-tactics-board-3d.orb +8 -4
  78. package/behaviors/registry/ui/game/organisms/std-worldmap-board-2d.orb +6 -3
  79. package/behaviors/registry/ui/learning/atoms/std-learn-slope-probe.orb +1 -1
  80. package/behaviors/registry/ui/marketing/atoms/std-marketing-article.orb +2 -1
  81. package/behaviors/registry/ui/marketing/atoms/std-marketing-footer.orb +2 -1
  82. package/behaviors/registry/ui/marketing/atoms/std-marketing-split.orb +2 -1
  83. package/behaviors/registry/ui/marketing/atoms/std-marketing-stats.orb +2 -1
  84. package/behaviors/registry/ui/marketing/atoms/std-marketing-steps.orb +2 -1
  85. package/behaviors/registry/ui/marketing/atoms/std-marketing-team.orb +2 -1
  86. package/dist/behaviors/exports-reader.js +10971 -6255
  87. package/dist/behaviors/functions/index.d.ts +1205 -256
  88. package/dist/behaviors/functions/index.js +10932 -6245
  89. package/dist/behaviors/index.d.ts +1 -1
  90. package/dist/behaviors/index.js +10972 -6256
  91. package/dist/behaviors/query.js +10971 -6255
  92. package/dist/behaviors-embeddings.hash.json +3 -3
  93. package/dist/behaviors-embeddings.json +485 -469
  94. package/dist/behaviors-registry.json +6125 -2940
  95. package/dist/index.d.ts +1 -1
  96. package/dist/index.js +10972 -6256
  97. package/dist/knob-embeddings.hash.json +3 -3
  98. package/dist/knob-embeddings.json +1 -1
  99. package/dist/registry/factory-signatures.json +1 -1
  100. package/package.json +4 -4
  101. package/behaviors/lolo/ui/core/atoms/std-rich-editor.lolo +0 -790
  102. package/behaviors/registry/ui/core/atoms/std-rich-editor.orb +0 -2225
@@ -0,0 +1,731 @@
1
+ app std-record-detail "1.0.0"
2
+ "std-record-detail — single primitive: detail-panel. Owns the fetch-one-record state machine (loading → viewing → error, with RETRY), the record counterpart to std-browse's collection machine. Consumers configure fields/sections/actions and the title/subtitle/status bindings; the atom fetches by the id (or, for slug-routed records, the slug) carried on INIT and renders the panel. `detailLook` (panel default / slide-over / page / document) selects the render body, exactly as std-browse's browseLook selects a collection shape. The `document` look is for records whose shape IS a document (wiki page, note, article): the pure writing surface — a big inline-editable title with the rich-text body DIRECTLY beneath it, Edit/Done and the actions menu tucked into the header corner — where clicking the body drops a cursor in, every typing pause autosaves (CONTENT_SAVED, which the host persists QUIETLY — no refetch, or the editor unmounts mid-keystroke), and Done emits CONTENT_COMMITTED (the host refetches and snapshots version history on this one). Metadata never sits between the title and the words: compose the sibling RecordItemDetails trait beside the doc for a Gutenberg-style settings rail. A row minted blank by a document-first create arrives with draft=1 on INIT and opens straight in the editor. Pair with std-detail-layout for the back + breadcrumb chrome — that atom owns navigation, this one owns the record — exactly as a list page pairs std-browse with std-app-layout."
3
+ ;; @capabilities "record detail, single record view, entity detail panel, drill-down record, show one record, record profile, detail page, view details, document editor, wiki page, note taking, article body, rich text editing, write and edit content"
4
+ orbital RecordItemOrbital {
5
+ type RecordAction = { label : string!, event : string!, icon : string, variant : primary | secondary | ghost | danger }
6
+ type RecordFieldSpec = { key : string!, header : string, format : date | currency | number | boolean | percent }
7
+ type MetaFieldSpec = { key : string!, label : string, kind : text | boolean | select | readonly, options : [string], icon : string, format : date | currency | number | boolean | percent }
8
+ type RecordSection = { title : string!, fields : [string] }
9
+
10
+ type RecordItemLoaded = Event { data : RecordItem, totalCount : number } "Fired when the single record finishes loading; payload.data holds the record. totalCount is accepted so a fetch emitter shared with collection atoms still validates."
11
+
12
+ type RecordItemSlugLoaded = Event { data : [RecordItem], totalCount : number } "Fired when a slug-routed record finishes loading. The fetch filters on the slug field, so the payload is a collection — (array/first ?data) is the record, and an empty array means no record matches the address."
13
+
14
+ type RecordItemLoadFailed = Event { error : string, code : string } "Fired when the record fails to load; drives the error state so a failed load shows an alert with a retry instead of an empty page."
15
+
16
+ entity RecordItem [runtime] {
17
+ id : string!
18
+ name : string! @description "The record's title or identifier." @synonyms "title, label, heading"
19
+ description : string @description "A textual explanation or summary of the record." @synonyms "summary, details, notes"
20
+ status : "active" | "inactive" | "pending" @description "The record's current state." @synonyms "state, condition, stage" = active
21
+ slug : string @description "URL-friendly identifier, for records opened by address instead of id." @synonyms "permalink, url key, address" = ""
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
+ loadedRow : object @intrinsic @description "The full row exactly as last fetched (relations hydrated when config.include asks). The document look binds it as the panel's entity and hands it to header actions as ?row, so a settings modal seeds from the real record instead of the atom's four mapped columns." = {}
24
+ 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
+ createdAt : string
26
+ }
27
+
28
+ trait RecordItemDetail -> @rebindable RecordItem [interaction, instance] {
29
+ ;; One machine, two shapes. A page detail (`openOn: init`, the default)
30
+ ;; falls straight through idle on INIT and behaves exactly as before. An
31
+ ;; overlay (`openOn: event`) stays blank until a row-click OPEN_VIEW hands
32
+ ;; it an id, which is the topology 23 hand-authored viewer traits use and
33
+ ;; the reason they could not compose an `initial: loading` atom: mounting
34
+ ;; one on a list page would fetch a record it has no id for.
35
+ initial: idle
36
+
37
+ state idle {
38
+ INIT -> loading when (and ["=", @config.openOn, "init"] ["!=", (str/default ?id ""), ""])
39
+ (set @entity.id ?id)
40
+ (set @entity.openDraft (str/default ?draft ""))
41
+ (fetch RecordItem {
42
+ emit: { failure: RecordItemLoadFailed, success: RecordItemLoaded }
43
+ id: ?id
44
+ include: @config.include
45
+ })
46
+ (render-ui main { rows: 4, type: skeleton })
47
+ ;; Slug-routed open (/things/:slug): no id on the payload, so fetch by
48
+ ;; the slug field instead. Filtered rather than by-id, so the success
49
+ ;; payload is a collection — its own event keeps the two shapes honest.
50
+ INIT -> loading when (and ["=", @config.openOn, "init"] ["=", (str/default ?id ""), ""] ["!=", (str/default ?slug ""), ""])
51
+ (set @entity.slug ?slug)
52
+ (set @entity.openDraft (str/default ?draft ""))
53
+ (fetch RecordItem {
54
+ filter: ["=", (object/get @entity slug), ?slug]
55
+ emit: { failure: RecordItemLoadFailed, success: RecordItemSlugLoaded }
56
+ include: @config.include
57
+ })
58
+ (render-ui main { rows: 4, type: skeleton })
59
+ ;; Fallback: an init-opened surface whose INIT carried neither key —
60
+ ;; fetch by the (missing) id so the failure surfaces as the error
61
+ ;; state's alert + Retry instead of a silent blank page. This is the
62
+ ;; pre-slug atom's exact behavior, kept as the explicit last arm.
63
+ INIT -> loading when (and ["=", @config.openOn, "init"] ["=", (str/default ?id ""), ""] ["=", (str/default ?slug ""), ""])
64
+ (set @entity.openDraft (str/default ?draft ""))
65
+ (fetch RecordItem {
66
+ emit: { failure: RecordItemLoadFailed, success: RecordItemLoaded }
67
+ id: ?id
68
+ include: @config.include
69
+ })
70
+ (render-ui main { rows: 4, type: skeleton })
71
+ ;; self-loops on the external emits so the validator sees in-atom
72
+ ;; listeners — the real consumers are the composing organism's
73
+ ;; persistor (CONTENT_SAVED, TITLE_COMMITTED) and its refetch /
74
+ ;; revision path (CONTENT_COMMITTED).
75
+ CONTENT_SAVED -> idle
76
+ CONTENT_COMMITTED -> idle
77
+ TITLE_COMMITTED -> idle
78
+ ;; ONE unguarded arm on purpose. This briefly carried a second,
79
+ ;; `when @config.preloaded`-guarded arm that rendered the OPEN_VIEW row
80
+ ;; directly to skip a round-trip. Both execution paths mishandled it:
81
+ ;; the live runtime PASSED the guard with `preloaded: false`
82
+ ;; (StateMachineCore's permissive mode turns an unresolved config guard
83
+ ;; into true), so overlays rendered from `?row` and never fetched —
84
+ ;; silently skipping `include` relation hydration; and the canonical
85
+ ;; evaluator FAILED the guard and then did not fall through to this arm,
86
+ ;; reporting the event as not firing at all. A guarded arm is not
87
+ ;; currently a trustworthy correctness boundary, so the atom no longer
88
+ ;; depends on one. Zero consumers had opted into the skip.
89
+ OPEN_VIEW -> loading
90
+ (set @entity.id ?id)
91
+ (fetch RecordItem {
92
+ emit: { failure: RecordItemLoadFailed, success: RecordItemLoaded }
93
+ id: ?id
94
+ include: @config.include
95
+ })
96
+ (render-ui main { rows: 4, type: skeleton })
97
+ }
98
+
99
+ state loading {
100
+ INIT -> loading when ["!=", (str/default ?id ""), ""]
101
+ (set @entity.id ?id)
102
+ (set @entity.openDraft (str/default ?draft ""))
103
+ (fetch RecordItem {
104
+ emit: { failure: RecordItemLoadFailed, success: RecordItemLoaded }
105
+ id: ?id
106
+ include: @config.include
107
+ })
108
+ ;; Skeleton, not a blank frame: the hand-authored detail traits this
109
+ ;; atom replaces rendered nothing until the record arrived.
110
+ (render-ui main { rows: 4, type: skeleton })
111
+ INIT -> loading when (and ["=", (str/default ?id ""), ""] ["!=", (str/default ?slug ""), ""])
112
+ (set @entity.slug ?slug)
113
+ (set @entity.openDraft (str/default ?draft ""))
114
+ (fetch RecordItem {
115
+ filter: ["=", (object/get @entity slug), ?slug]
116
+ emit: { failure: RecordItemLoadFailed, success: RecordItemSlugLoaded }
117
+ include: @config.include
118
+ })
119
+ (render-ui main { rows: 4, type: skeleton })
120
+ ;; A record minted blank by a document-first create arrives with
121
+ ;; draft=1 and lands straight in the editor — a distinct target state,
122
+ ;; not a flag on the render, so the machine stays honest about being
123
+ ;; in an editing session.
124
+ RecordItemLoaded -> editing when ["=", (str/default (object/get @entity openDraft) ""), "1"]
125
+ ;; (object/merge … {}) is a type adapter only: type_compat lacks "declared object accepts a named row type" (ledgered)
126
+ (set @entity.loadedRow (object/merge ?data {}))
127
+ (set @entity.id (object/get ?data id))
128
+ (set @entity.name (object/get ?data @config.titleField))
129
+ (set @entity.description (str/default (object/get ?data @config.subtitleField) ""))
130
+ (set @entity.content (object/get ?data @config.contentField))
131
+ (render-ui main @config.documentEditingContent)
132
+ RecordItemLoaded -> viewing when ["!=", (str/default (object/get @entity openDraft) ""), "1"]
133
+ ;; (object/merge … {}) is a type adapter only: type_compat lacks "declared object accepts a named row type" (ledgered)
134
+ (set @entity.loadedRow (object/merge ?data {}))
135
+ (set @entity.id (object/get ?data id))
136
+ (set @entity.name (object/get ?data @config.titleField))
137
+ (set @entity.description (str/default (object/get ?data @config.subtitleField) ""))
138
+ (set @entity.content (object/get ?data @config.contentField))
139
+ (if ["=", @config.detailLook, document]
140
+ (render-ui main @config.documentBodyContent)
141
+ (if ["=", @config.detailLook, slide-over]
142
+ (render-ui main @config.slideOverBodyContent)
143
+ (if ["=", @config.detailLook, page]
144
+ (render-ui main @config.pageBodyContent)
145
+ (render-ui main @config.bodyContent))))
146
+ RecordItemSlugLoaded -> editing when (and [">", (array/len ?data), 0] ["=", (str/default (object/get @entity openDraft) ""), "1"])
147
+ (set @entity.loadedRow (array/first ?data))
148
+ (set @entity.id (object/get (array/first ?data) id))
149
+ (set @entity.name (object/get (array/first ?data) @config.titleField))
150
+ (set @entity.description (str/default (object/get (array/first ?data) @config.subtitleField) ""))
151
+ (set @entity.content (object/get (array/first ?data) @config.contentField))
152
+ (render-ui main @config.documentEditingContent)
153
+ RecordItemSlugLoaded -> viewing when (and [">", (array/len ?data), 0] ["!=", (str/default (object/get @entity openDraft) ""), "1"])
154
+ (set @entity.loadedRow (array/first ?data))
155
+ (set @entity.id (object/get (array/first ?data) id))
156
+ (set @entity.name (object/get (array/first ?data) @config.titleField))
157
+ (set @entity.description (str/default (object/get (array/first ?data) @config.subtitleField) ""))
158
+ (set @entity.content (object/get (array/first ?data) @config.contentField))
159
+ (if ["=", @config.detailLook, document]
160
+ (render-ui main @config.documentBodyContent)
161
+ (if ["=", @config.detailLook, slide-over]
162
+ (render-ui main @config.slideOverBodyContent)
163
+ (if ["=", @config.detailLook, page]
164
+ (render-ui main @config.pageBodyContent)
165
+ (render-ui main @config.bodyContent))))
166
+ RecordItemSlugLoaded -> error when ["=", (array/len ?data), 0]
167
+ (render-ui main { description: "No record matches this address.", icon: search-x, title: "Not found", type: empty-state })
168
+ RecordItemLoadFailed -> error
169
+ (render-ui main { message: ?error, title: "Couldn't load this record", type: alert, variant: error })
170
+ }
171
+
172
+ ;; The document look's editing session. Reached from a draft landing or
173
+ ;; the read view's EDIT_CONTENT; every typing pause autosaves through
174
+ ;; CONTENT_SAVED, and Done commits. INIT is handled ONLY for a
175
+ ;; DIFFERENT record: a page navigation (open another note, open a
176
+ ;; just-minted subpage) delivers INIT and must not be swallowed — the
177
+ ;; session's server-side trait state survives navigation, so an
178
+ ;; INIT-less editing state left every later record page blank. The
179
+ ;; SAME record's INIT (the refetch echo after an update) stays
180
+ ;; unhandled on purpose: a repaint would unmount the editor under the
181
+ ;; author mid-keystroke. Work is already autosaved when we leave.
182
+ state editing {
183
+ INIT -> loading when (and ["!=", (str/default ?id ""), ""] ["!=", ?id, @entity.id])
184
+ (set @entity.id ?id)
185
+ (set @entity.openDraft (str/default ?draft ""))
186
+ (fetch RecordItem {
187
+ emit: { failure: RecordItemLoadFailed, success: RecordItemLoaded }
188
+ id: ?id
189
+ include: @config.include
190
+ })
191
+ (render-ui main { rows: 4, type: skeleton })
192
+ INIT -> loading when (and ["=", (str/default ?id ""), ""] ["!=", (str/default ?slug ""), ""] ["!=", ?slug, @entity.slug])
193
+ (set @entity.slug ?slug)
194
+ (set @entity.openDraft (str/default ?draft ""))
195
+ (fetch RecordItem {
196
+ filter: ["=", (object/get @entity slug), ?slug]
197
+ emit: { failure: RecordItemLoadFailed, success: RecordItemSlugLoaded }
198
+ include: @config.include
199
+ })
200
+ (render-ui main { rows: 4, type: skeleton })
201
+ CONTENT_CHANGE -> editing
202
+ (set @entity.content ?value)
203
+ (set @entity.loadedRow (object/set @entity.loadedRow @config.contentField ?value))
204
+ (emit CONTENT_SAVED { id: @entity.id, content: @entity.content })
205
+ TITLE_COMMITTED -> editing
206
+ (set @entity.name ?title)
207
+ (set @entity.loadedRow (object/set @entity.loadedRow @config.titleField ?title))
208
+ DONE_EDITING -> viewing
209
+ (emit CONTENT_SAVED { id: @entity.id, content: @entity.content })
210
+ (emit CONTENT_COMMITTED { id: @entity.id, content: @entity.content })
211
+ (render-ui main @config.documentBodyContent)
212
+ ;; The editing card renders the same header actions as the read view,
213
+ ;; so a consumer-configured Close must not be a dead button here:
214
+ ;; flush the autosave stream and close, same contract as Done minus
215
+ ;; the commit.
216
+ CLOSE_VIEW -> idle
217
+ (emit CONTENT_SAVED { id: @entity.id, content: @entity.content })
218
+ (render-ui main null)
219
+ }
220
+
221
+ state viewing {
222
+ ;; No skeleton on re-entry: from a populated panel a skeleton unmounts the
223
+ ;; record and flickers. First mount is unaffected — `initial: loading` and
224
+ ;; `state loading`'s own INIT still paint it.
225
+ INIT -> loading when ["!=", (str/default ?id ""), ""]
226
+ (set @entity.id ?id)
227
+ (set @entity.openDraft (str/default ?draft ""))
228
+ (fetch RecordItem {
229
+ emit: { failure: RecordItemLoadFailed, success: RecordItemLoaded }
230
+ id: ?id
231
+ include: @config.include
232
+ })
233
+ INIT -> loading when (and ["=", (str/default ?id ""), ""] ["!=", (str/default ?slug ""), ""])
234
+ (set @entity.slug ?slug)
235
+ (set @entity.openDraft (str/default ?draft ""))
236
+ (fetch RecordItem {
237
+ filter: ["=", (object/get @entity slug), ?slug]
238
+ emit: { failure: RecordItemLoadFailed, success: RecordItemSlugLoaded }
239
+ include: @config.include
240
+ })
241
+ RecordItemLoaded -> viewing
242
+ ;; (object/merge … {}) is a type adapter only: type_compat lacks "declared object accepts a named row type" (ledgered)
243
+ (set @entity.loadedRow (object/merge ?data {}))
244
+ (set @entity.id (object/get ?data id))
245
+ (set @entity.name (object/get ?data @config.titleField))
246
+ (set @entity.description (str/default (object/get ?data @config.subtitleField) ""))
247
+ (set @entity.content (object/get ?data @config.contentField))
248
+ (if ["=", @config.detailLook, document]
249
+ (render-ui main @config.documentBodyContent)
250
+ (if ["=", @config.detailLook, slide-over]
251
+ (render-ui main @config.slideOverBodyContent)
252
+ (if ["=", @config.detailLook, page]
253
+ (render-ui main @config.pageBodyContent)
254
+ (render-ui main @config.bodyContent))))
255
+ RecordItemSlugLoaded -> viewing when [">", (array/len ?data), 0]
256
+ (set @entity.loadedRow (array/first ?data))
257
+ (set @entity.id (object/get (array/first ?data) id))
258
+ (set @entity.name (object/get (array/first ?data) @config.titleField))
259
+ (set @entity.description (str/default (object/get (array/first ?data) @config.subtitleField) ""))
260
+ (set @entity.content (object/get (array/first ?data) @config.contentField))
261
+ (if ["=", @config.detailLook, document]
262
+ (render-ui main @config.documentBodyContent)
263
+ (if ["=", @config.detailLook, slide-over]
264
+ (render-ui main @config.slideOverBodyContent)
265
+ (if ["=", @config.detailLook, page]
266
+ (render-ui main @config.pageBodyContent)
267
+ (render-ui main @config.bodyContent))))
268
+ RecordItemSlugLoaded -> error when ["=", (array/len ?data), 0]
269
+ (render-ui main { description: "No record matches this address.", icon: search-x, title: "Not found", type: empty-state })
270
+ ;; The document body is its own edit affordance: clicking it (or its
271
+ ;; Edit button) starts the editing session with a cursor in the text.
272
+ EDIT_CONTENT -> editing
273
+ (render-ui main @config.documentEditingContent)
274
+ ;; The title renames from the read view too, without an editing session.
275
+ TITLE_COMMITTED -> viewing
276
+ (set @entity.name ?title)
277
+ (set @entity.loadedRow (object/set @entity.loadedRow @config.titleField ?title))
278
+ RecordItemLoadFailed -> error
279
+ (render-ui main { message: ?error, title: "Couldn't load this record", type: alert, variant: error })
280
+ CLOSE_VIEW -> idle
281
+ (render-ui main null)
282
+ }
283
+
284
+ state error {
285
+ INIT -> loading when ["!=", (str/default ?id ""), ""]
286
+ (set @entity.id ?id)
287
+ (set @entity.openDraft (str/default ?draft ""))
288
+ (fetch RecordItem {
289
+ emit: { failure: RecordItemLoadFailed, success: RecordItemLoaded }
290
+ id: ?id
291
+ include: @config.include
292
+ })
293
+ (render-ui main { rows: 4, type: skeleton })
294
+ INIT -> loading when (and ["=", (str/default ?id ""), ""] ["!=", (str/default ?slug ""), ""])
295
+ (set @entity.slug ?slug)
296
+ (set @entity.openDraft (str/default ?draft ""))
297
+ (fetch RecordItem {
298
+ filter: ["=", (object/get @entity slug), ?slug]
299
+ emit: { failure: RecordItemLoadFailed, success: RecordItemSlugLoaded }
300
+ include: @config.include
301
+ })
302
+ (render-ui main { rows: 4, type: skeleton })
303
+ ;; The hand-authored traits sent a failed load back to `loading` with no
304
+ ;; effect, so the page stayed blank forever with no way out. RETRY is the
305
+ ;; way out.
306
+ RETRY -> loading
307
+ (fetch RecordItem {
308
+ emit: { failure: RecordItemLoadFailed, success: RecordItemLoaded }
309
+ id: @entity.id
310
+ include: @config.include
311
+ })
312
+ (render-ui main { rows: 4, type: skeleton })
313
+ }
314
+
315
+ emits {
316
+ RecordItemLoaded
317
+ @description "Signals that the record has been successfully loaded."
318
+ @synonyms "loaded, received, ready, fetched"
319
+ @tier "domain"
320
+ RecordItemSlugLoaded
321
+ @description "A slug-routed record finished loading; the payload is a collection whose first element is the record. Composing traits that need the loaded row (comments scoping, status controls) listen to this."
322
+ @synonyms "loaded by address, slug resolved, record found"
323
+ @tier "domain"
324
+ RecordItemLoadFailed
325
+ @description "Indicates a failure to load the record."
326
+ @synonyms "error, failed, problem, unsuccessful"
327
+ @tier "internal"
328
+ EDIT -> external {
329
+ id : string!
330
+ row : RecordItem
331
+ }
332
+ @description "Signals a request to edit the record being viewed."
333
+ @synonyms "modify, update, change"
334
+ @tier "domain"
335
+ DELETE -> external {
336
+ id : string!
337
+ row : RecordItem
338
+ }
339
+ @description "Signals a request to remove the record being viewed."
340
+ @synonyms "remove, discard, delete, archive"
341
+ @tier "domain"
342
+ CONTENT_SAVED -> external {
343
+ id : string!
344
+ content : string
345
+ }
346
+ @description "The document look's autosave stream: fired at every typing pause while editing, and once more on Done. This atom never persists — the composing organism routes CONTENT_SAVED into a QUIET persist (one that does not refetch or repaint this surface, or the editor unmounts mid-keystroke)."
347
+ @synonyms "autosave, saved, persist content, draft saved"
348
+ @tier "domain"
349
+ CONTENT_COMMITTED -> external {
350
+ id : string!
351
+ content : string
352
+ }
353
+ @description "The author finished the document editing session (pressed Done). Hosts refetch the row, repaint, and snapshot version history on THIS event — never on the CONTENT_SAVED autosave stream."
354
+ @synonyms "done, finished editing, commit, snapshot"
355
+ @tier "domain"
356
+ TITLE_COMMITTED -> external {
357
+ title : string
358
+ id : string
359
+ }
360
+ @description "The viewer renamed the record in place by editing the document title; carries the committed title and the row id for a partial persist."
361
+ @synonyms "rename, retitle, change title, edit title"
362
+ @tier "domain"
363
+ EDIT_CONTENT
364
+ @description "The viewer clicked the document body (or its Edit button) to start an editing session."
365
+ @synonyms "edit, start editing, click to edit"
366
+ @tier "presentation"
367
+ DONE_EDITING
368
+ @description "The viewer pressed Done to end the document editing session."
369
+ @synonyms "done, finish, stop editing"
370
+ @tier "presentation"
371
+ }
372
+
373
+ listens {
374
+ INIT {
375
+ id : string
376
+ slug : string
377
+ draft : string
378
+ }
379
+ @description "Opens the detail surface for one record: by id, or by slug for address-routed records (the internal re-fetch after an update still carries id). `draft` is set only by a document-first create's navigate, and opens the document look's body straight in the editor."
380
+ @synonyms "open record, view record, show details, drill in"
381
+ @tier "domain"
382
+ CONTENT_CHANGE {
383
+ value : string
384
+ }
385
+ @description "The rich text surface reported an edit (already debounced to typing pauses by the component)."
386
+ @synonyms "typing, content changed, body edited"
387
+ @tier "domain"
388
+ EDIT_CONTENT
389
+ @description "Start the document editing session."
390
+ @synonyms "edit, start editing, click to edit"
391
+ @tier "presentation"
392
+ DONE_EDITING
393
+ @description "End the document editing session; work is already autosaved when it fires."
394
+ @synonyms "done, finish, stop editing"
395
+ @tier "presentation"
396
+ TITLE_COMMITTED {
397
+ title : string
398
+ id : string
399
+ }
400
+ @description "The document title was renamed in place."
401
+ @synonyms "rename, retitle, change title"
402
+ @tier "domain"
403
+ OPEN_VIEW {
404
+ id : string!
405
+ row : RecordItem
406
+ }
407
+ @description "A row was opened from a list; shows that record in this surface."
408
+ @synonyms "open record, view row, show details, drill in, expand"
409
+ @tier "domain"
410
+ CLOSE_VIEW
411
+ @description "Closes the record surface and returns it to blank."
412
+ @synonyms "close, dismiss, back to list"
413
+ @tier "domain"
414
+ RETRY
415
+ @description "Re-runs the fetch after a failed load."
416
+ @synonyms "try again, reload, refresh"
417
+ @tier "domain"
418
+ RecordItemLoadFailed {
419
+ error : string
420
+ code : string
421
+ }
422
+ @description "Indicates a failure to load the record."
423
+ @synonyms "error, failure, problem, unsuccessful"
424
+ @tier "internal"
425
+ }
426
+
427
+ config {
428
+ fields : [RecordFieldSpec] = [{
429
+ header: Name
430
+ key: name
431
+ }, {
432
+ header: Description
433
+ key: description
434
+ }, {
435
+ header: Status
436
+ key: status
437
+ }, {
438
+ format: date
439
+ header: Created
440
+ key: createdAt
441
+ }]
442
+ @label "Which fields appear in the record panel?"
443
+ @description "Each entry maps one entity field to a labelled row in the panel. Unlike a list row, a detail panel SHOULD label every field — the value stands alone here with no column header or repeated row position to name it."
444
+ @synonyms "detail fields, record fields, panel fields, shown fields"
445
+ @tier "presentation"
446
+ sections : [RecordSection] = []
447
+ @label "How should the fields be grouped?"
448
+ @description "Optional titled groups over `fields`, e.g. [{title: Contact, fields: [email, phone]}]. Group into sections of 3-5 once a record carries more than about seven fields — a flat list past that is the cognitive-load problem Almadar_UX.md §1.4 (Miller) describes. Empty = one ungrouped list, the default."
449
+ @synonyms "field groups, grouped fields, panel sections, fieldsets"
450
+ @tier "presentation"
451
+ actions : [RecordAction] = [{
452
+ event: EDIT
453
+ label: Edit
454
+ variant: secondary
455
+ }, {
456
+ event: DELETE
457
+ label: Delete
458
+ variant: danger
459
+ }]
460
+ @label "What actions can users take on this record?"
461
+ @description "Buttons on the panel header. One primary at most; the rest collapse past maxInlineActions."
462
+ @synonyms "record actions, detail actions, panel buttons"
463
+ @tier "presentation"
464
+ maxInlineActions : number = 3
465
+ @label "How many actions before collapsing?"
466
+ @description "Actions beyond this count collapse into a '⋯' overflow menu."
467
+ @synonyms "inline actions limit, overflow threshold"
468
+ @tier "presentation"
469
+ showActions : boolean = true
470
+ @label "Show the action buttons?"
471
+ @description "Set false for a read-only record view; a panel with actions nothing listens to is a dead affordance."
472
+ @synonyms "read only, hide actions, view only"
473
+ @tier "presentation"
474
+ ;; Field NAMES, not binding paths. A path knob (`subtitleBinding:
475
+ ;; @entity.company`) makes the consumer responsible for an @entity field
476
+ ;; this atom never sets — an ORB_BINDING_ENTITY_FIELD_NEVER_SET and an
477
+ ;; empty subtitle, which is exactly what the first migration of std-crm
478
+ ;; ContactDetail hit. Naming the source field instead lets the atom do the
479
+ ;; (set) itself off the payload, so the trap cannot be reintroduced.
480
+ titleField : string = name
481
+ @label "Which field is the record's title?"
482
+ @description "Name of the field on the loaded record shown as the panel title (e.g. contactName, subject, title)."
483
+ @synonyms "title field, heading field, name field, label field"
484
+ @tier "presentation"
485
+ subtitleField : string = description
486
+ @label "Which field is the subtitle?"
487
+ @description "Name of the field shown under the title (e.g. company, category). Set to \"\" for no subtitle at all — the right choice for the document look, whose metadata lives in the properties strip, not a second line."
488
+ @synonyms "subtitle field, secondary field, subheading, no subtitle"
489
+ @tier "presentation"
490
+ include : [string] = []
491
+ @label "Which relation fields to hydrate?"
492
+ @description "Entity relation field names to hydrate on fetch, so panel fields can use dot-paths like owner.name. Empty = no hydration, the default."
493
+ @synonyms "relation hydration, populate relations, joined fields"
494
+ @tier "internal"
495
+ openOn : "init" | "event" = init
496
+ @label "When does this surface load?"
497
+ @description "init = a routed detail page; the record loads as soon as the page mounts (the default). event = an overlay that stays blank until a row-click OPEN_VIEW hands it an id, which is what a list-embedded slide-over needs — mounting an init-loading surface on a list page would fetch a record it has no id for."
498
+ @synonyms "open on click, slide over, row click, blank until opened, list embedded"
499
+ @tier "policy"
500
+ detailLook : "panel" | "slide-over" | "page" | "document" = panel
501
+ @label "Detail look"
502
+ @description "Visual treatment for the record surface. panel = an inline detail-panel in the page flow (bodyContent); slide-over = the same panel as a right-hand drawer over the list behind it (slideOverBodyContent); page = a full-width record page with roomier spacing (pageBodyContent); document = a wiki-page/note/article surface — big editable title over a rich-text body, no labelled field rows, click-to-edit with autosave (documentBodyContent / documentEditingContent). Selecting a look swaps which body tree renders after the record loads; any body may still be overridden directly at the call site."
503
+ @synonyms "detail style, drawer, slide over, full page, document, wiki page, note, article"
504
+ @tier "presentation"
505
+ contentField : string = content
506
+ @label "Which field holds the document body?"
507
+ @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
+ @synonyms "body field, content field, document field"
509
+ @tier "presentation"
510
+ editLabel : string = "Edit"
511
+ @label "Edit button label"
512
+ @description "Label of the quiet button above the document body that starts an editing session (the body itself is also clickable)."
513
+ @tier "presentation"
514
+ doneLabel : string = "Done"
515
+ @label "Done button label"
516
+ @description "Label of the button that ends the document editing session; work is already autosaved when it is pressed."
517
+ @tier "presentation"
518
+ autosaveHint : string = "Saves as you type"
519
+ @label "Autosave hint"
520
+ @description "Muted caption next to the Done button telling the author their work persists on its own."
521
+ @tier "presentation"
522
+ placeholder : string = "Start writing…"
523
+ @label "Placeholder text"
524
+ @description "Placeholder shown in the document body when it is empty."
525
+ @tier "presentation"
526
+ bodyContent : render-ui = {
527
+ children: [{
528
+ actions: @config.actions
529
+ entity: ?data
530
+ fields: @config.fields
531
+ initialData: ?data
532
+ maxInlineActions: @config.maxInlineActions
533
+ sections: @config.sections
534
+ showActions: @config.showActions
535
+ subtitle: @entity.description
536
+ title: @entity.name
537
+ type: detail-panel
538
+ }]
539
+ direction: vertical
540
+ gap: lg
541
+ type: stack
542
+ }
543
+ @label "Body content tree"
544
+ @description "Render-ui SExpr rendered after the record loads when detailLook = panel (the default). See slideOverBodyContent/pageBodyContent for the other looks; any may be overridden directly while inheriting the same trait, state machine, emits, and listens."
545
+ @tier "internal"
546
+ slideOverBodyContent : render-ui = {
547
+ children: [{
548
+ actions: @config.actions
549
+ entity: ?data
550
+ fields: @config.fields
551
+ initialData: ?data
552
+ maxInlineActions: @config.maxInlineActions
553
+ sections: @config.sections
554
+ showActions: @config.showActions
555
+ slideOver: true
556
+ subtitle: @entity.description
557
+ title: @entity.name
558
+ type: detail-panel
559
+ }]
560
+ direction: vertical
561
+ gap: none
562
+ type: stack
563
+ }
564
+ @label "Slide-over look body"
565
+ @description "Render-ui SExpr rendered after the record loads when detailLook = slide-over: the same panel as a right-hand drawer, so the list behind stays in place. Use when the record is a glance, not a workspace."
566
+ @tier "internal"
567
+ pageBodyContent : render-ui = {
568
+ children: [{
569
+ actions: @config.actions
570
+ entity: ?data
571
+ fields: @config.fields
572
+ initialData: ?data
573
+ maxInlineActions: @config.maxInlineActions
574
+ sections: @config.sections
575
+ showActions: @config.showActions
576
+ subtitle: @entity.description
577
+ title: @entity.name
578
+ type: detail-panel
579
+ }]
580
+ className: "w-full max-w-4xl"
581
+ direction: vertical
582
+ gap: lg
583
+ type: stack
584
+ }
585
+ @label "Page look body"
586
+ @description "Render-ui SExpr rendered after the record loads when detailLook = page: a full record page, width-capped so field rows do not stretch past a readable measure (Almadar_UI_Beauty.md §3)."
587
+ @tier "internal"
588
+ documentBodyContent : render-ui = {
589
+ actions: @config.actions
590
+ editEvent: EDIT_CONTENT
591
+ editLabel: @config.editLabel
592
+ entity: @entity.loadedRow
593
+ maxInlineActions: @config.maxInlineActions
594
+ placeholder: @config.placeholder
595
+ subtitle: @entity.description
596
+ title: @entity.name
597
+ titleCommitEvent: TITLE_COMMITTED
598
+ type: document-panel
599
+ value: @entity.content
600
+ className: "w-full"
601
+ }
602
+ @label "Document look body (reading)"
603
+ @description "Render-ui SExpr for detailLook = document while reading: the pure writing surface — editable title with the rich-text body DIRECTLY beneath it, Edit and the actions menu tucked into the header corner. Metadata never sits between title and content; compose the sibling RecordItemDetails trait beside this one for the settings rail. Binds only @entity fields, never ?data, because it is also painted by arms whose payload is not the record."
604
+ @tier "internal"
605
+ documentEditingContent : render-ui = {
606
+ actions: @config.actions
607
+ autosaveHint: @config.autosaveHint
608
+ contentChangeEvent: CONTENT_CHANGE
609
+ doneEvent: DONE_EDITING
610
+ doneLabel: @config.doneLabel
611
+ editing: true
612
+ entity: @entity.loadedRow
613
+ maxInlineActions: @config.maxInlineActions
614
+ placeholder: @config.placeholder
615
+ subtitle: @entity.description
616
+ title: @entity.name
617
+ titleCommitEvent: TITLE_COMMITTED
618
+ type: document-panel
619
+ value: @entity.content
620
+ className: "w-full"
621
+ }
622
+ @label "Document look body (editing)"
623
+ @description "Render-ui SExpr for detailLook = document during an editing session: the same surface with the body live and the header corner showing the autosave hint + Done. Binds only @entity fields, never ?data."
624
+ @tier "internal"
625
+ }
626
+ }
627
+
628
+ ;; The document's info panel — an on-demand DRAWER, never page furniture:
629
+ ;; the composing organism routes the doc's loaded event into SHOW_DETAILS
630
+ ;; (which only stashes the row) and a "Details" action on the document's
631
+ ;; menu into OPEN_DETAILS, which slides the panel in from the right over
632
+ ;; the content. The drawer's X / backdrop dismiss emits CLOSE back to this
633
+ ;; trait. A sibling of RecordItemDetail: the organism just MOUNTS this
634
+ ;; trait and wires the two listens — no layout wiring.
635
+ ;; EDITING metadata is not done here: it rides the document's "⋯" menu as
636
+ ;; normal actions (the settings modal, favorite/status workflows). The
637
+ ;; META_CHANGE → META_SAVED commit circuit stays available for a call-site
638
+ ;; detailsContent override that opts back into in-place editing.
639
+ trait RecordItemDetails -> @rebindable RecordItem [interaction, instance] {
640
+ initial: idle
641
+
642
+ state idle {
643
+ INIT -> idle
644
+ ;; stash only — the drawer opens on demand, never on load
645
+ SHOW_DETAILS -> idle
646
+ (set @entity.loadedRow (object/merge ?data {}))
647
+ OPEN_DETAILS -> showing
648
+ (render-ui drawer @config.detailsContent)
649
+ ;; validator self-loop; the real consumer is the host's persistor.
650
+ META_SAVED -> idle
651
+ }
652
+
653
+ state showing {
654
+ ;; a refetch echo while the drawer is open repaints it in place
655
+ SHOW_DETAILS -> showing
656
+ (set @entity.loadedRow (object/merge ?data {}))
657
+ (render-ui drawer @config.detailsContent)
658
+ OPEN_DETAILS -> showing
659
+ (render-ui drawer @config.detailsContent)
660
+ ;; the drawer X / backdrop dismiss
661
+ CLOSE -> idle
662
+ (render-ui drawer null)
663
+ ;; navigation must never strand an open drawer over the next page
664
+ INIT -> idle
665
+ (render-ui drawer null)
666
+ META_CHANGE -> showing
667
+ (emit META_SAVED { id: ?id, patch: ?patch })
668
+ }
669
+
670
+ emits {
671
+ META_SAVED -> external {
672
+ id : string!
673
+ patch : RecordItem
674
+ }
675
+ @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
+ @synonyms "property saved, metadata changed, field committed, update property"
677
+ @tier "domain"
678
+ }
679
+
680
+ listens {
681
+ SHOW_DETAILS {
682
+ data : RecordItem
683
+ }
684
+ @description "The document finished loading; stash the row the details drawer will show. Hosts route the doc trait's RecordItemLoaded (or SlugLoaded first element) here."
685
+ @synonyms "record loaded, show metadata, populate details"
686
+ @tier "domain"
687
+ OPEN_DETAILS {
688
+ id : string
689
+ }
690
+ @description "Open the details drawer. Hosts route the document's Details action (e.g. Doc.VIEW_INFO) here."
691
+ @synonyms "show info, open details, document info, page info"
692
+ @tier "presentation"
693
+ CLOSE {
694
+ id : string
695
+ }
696
+ @description "Close the details drawer; the drawer chrome's X / backdrop dismiss emits this."
697
+ @synonyms "dismiss, close drawer, hide details"
698
+ @tier "presentation"
699
+ META_CHANGE {
700
+ id : string
701
+ patch : RecordItem
702
+ }
703
+ @description "A property in the rail committed (switch toggled, select changed, text blurred)."
704
+ @synonyms "property changed, metadata edited, field committed"
705
+ @tier "domain"
706
+ }
707
+
708
+ config {
709
+ metaFields : [MetaFieldSpec] = []
710
+ @label "Which metadata properties appear in the details drawer?"
711
+ @description "The record's metadata shown as read-only INFO in the details drawer (Updated, address, status, …). Editing metadata is a normal action under the document's menu, not an inline affordance here."
712
+ @synonyms "properties, metadata fields, document info, page info, front matter"
713
+ @tier "presentation"
714
+ detailsTitle : string = Details
715
+ @label "Details drawer heading"
716
+ @description "Heading of the settings card (e.g. Details, Page info, Document settings)."
717
+ @tier "presentation"
718
+ detailsContent : render-ui = {
719
+ entity: @entity.loadedRow
720
+ fields: @config.metaFields
721
+ title: @config.detailsTitle
722
+ type: document-details
723
+ }
724
+ @label "Details drawer content tree"
725
+ @description "Render-ui SExpr painted into the drawer slot on OPEN_DETAILS; binds the row stashed by SHOW_DETAILS. Info-only by default — no metaCommitEvent, so every row displays rather than edits (editing rides the document's actions menu). Override at the call site to extend the panel or to opt into in-place commits via metaCommitEvent: META_CHANGE."
726
+ @tier "internal"
727
+ }
728
+ }
729
+
730
+ page "/recorditems" as RecordItemDetailPage -> RecordItemDetail, RecordItemDetails
731
+ }