@almadar/std 16.188.0 → 16.190.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 (46) hide show
  1. package/behaviors/lolo/ui/core/.hand-authored.json +0 -4
  2. package/behaviors/lolo/ui/core/atoms/std-calendar.lolo +20 -8
  3. package/behaviors/lolo/ui/core/atoms/std-record-detail.lolo +428 -26
  4. package/behaviors/lolo/ui/core/atoms/std-related.lolo +5 -1
  5. package/behaviors/lolo/ui/core/atoms/std-version-history.lolo +1 -1
  6. package/behaviors/lolo/ui/core/molecules/ui-document-details.lolo +80 -0
  7. package/behaviors/lolo/ui/core/molecules/ui-document-panel.lolo +213 -0
  8. package/behaviors/lolo/ui/core/molecules/{ui-rich-block-editor.lolo → ui-rich-text-editor.lolo} +25 -25
  9. package/behaviors/lolo/ui/game/atoms/std-choice-gate.lolo +100 -0
  10. package/behaviors/lolo/ui/game/atoms/std-coef-pad.lolo +114 -0
  11. package/behaviors/lolo/ui/game/atoms/std-curve-fog.lolo +50 -0
  12. package/behaviors/lolo/ui/game/atoms/std-pad-lock.lolo +5 -1
  13. package/behaviors/lolo/ui/game/atoms/std-platformer-body.lolo +29 -3
  14. package/behaviors/lolo/ui/game/atoms/std-zone-card.lolo +95 -0
  15. package/behaviors/lolo/ui/game/organisms/std-platformer-board-2d.lolo +9 -0
  16. package/behaviors/registry/ui/core/atoms/std-calendar.orb +51 -27
  17. package/behaviors/registry/ui/core/atoms/std-record-detail.orb +2399 -180
  18. package/behaviors/registry/ui/core/atoms/std-related.orb +23 -1
  19. package/behaviors/registry/ui/core/atoms/std-version-history.orb +1 -1
  20. package/behaviors/registry/ui/core/molecules/ui-document-details.orb +342 -0
  21. package/behaviors/registry/ui/core/molecules/ui-document-panel.orb +655 -0
  22. package/behaviors/registry/ui/core/molecules/{ui-rich-block-editor.orb → ui-rich-text-editor.orb} +84 -97
  23. package/behaviors/registry/ui/game/atoms/std-choice-gate.orb +855 -0
  24. package/behaviors/registry/ui/game/atoms/std-coef-pad.orb +928 -0
  25. package/behaviors/registry/ui/game/atoms/std-curve-fog.orb +337 -0
  26. package/behaviors/registry/ui/game/atoms/std-pad-lock.orb +31 -1
  27. package/behaviors/registry/ui/game/atoms/std-platformer-body.orb +847 -401
  28. package/behaviors/registry/ui/game/atoms/std-zone-card.orb +642 -0
  29. package/behaviors/registry/ui/game/organisms/std-platformer-board-2d.orb +38 -6
  30. package/dist/behaviors/exports-reader.js +6491 -6163
  31. package/dist/behaviors/functions/index.d.ts +669 -300
  32. package/dist/behaviors/functions/index.js +6469 -6152
  33. package/dist/behaviors/index.d.ts +1 -1
  34. package/dist/behaviors/index.js +6492 -6164
  35. package/dist/behaviors/query.js +6491 -6163
  36. package/dist/behaviors-embeddings.hash.json +3 -3
  37. package/dist/behaviors-embeddings.json +367 -375
  38. package/dist/behaviors-registry.json +4104 -2597
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.js +6492 -6164
  41. package/dist/knob-embeddings.hash.json +3 -3
  42. package/dist/knob-embeddings.json +1 -1
  43. package/dist/registry/factory-signatures.json +1 -1
  44. package/package.json +4 -4
  45. package/behaviors/lolo/ui/core/atoms/std-rich-editor.lolo +0 -790
  46. package/behaviors/registry/ui/core/atoms/std-rich-editor.orb +0 -2225
@@ -1,13 +1,16 @@
1
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 carried on INIT and renders the panel. `detailLook` (panel default / slide-over / page) selects one of three render bodies. 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"
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
4
  orbital RecordItemOrbital {
5
5
  type RecordAction = { label : string!, event : string!, icon : string, variant : primary | secondary | ghost | danger }
6
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 }
7
8
  type RecordSection = { title : string!, fields : [string] }
8
9
 
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."
10
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
+
11
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."
12
15
 
13
16
  entity RecordItem [runtime] {
@@ -15,6 +18,10 @@ orbital RecordItemOrbital {
15
18
  name : string! @description "The record's title or identifier." @synonyms "title, label, heading"
16
19
  description : string @description "A textual explanation or summary of the record." @synonyms "summary, details, notes"
17
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" = ""
18
25
  createdAt : string
19
26
  }
20
27
 
@@ -28,14 +35,46 @@ orbital RecordItemOrbital {
28
35
  initial: idle
29
36
 
30
37
  state idle {
31
- INIT -> loading when ["=", @config.openOn, "init"]
38
+ INIT -> loading when (and ["=", @config.openOn, "init"] ["!=", (str/default ?id ""), ""])
32
39
  (set @entity.id ?id)
40
+ (set @entity.openDraft (str/default ?draft ""))
33
41
  (fetch RecordItem {
34
42
  emit: { failure: RecordItemLoadFailed, success: RecordItemLoaded }
35
43
  id: ?id
36
44
  include: @config.include
37
45
  })
38
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
39
78
  ;; ONE unguarded arm on purpose. This briefly carried a second,
40
79
  ;; `when @config.preloaded`-guarded arm that rendered the OPEN_VIEW row
41
80
  ;; directly to skip a round-trip. Both execution paths mishandled it:
@@ -58,8 +97,9 @@ orbital RecordItemOrbital {
58
97
  }
59
98
 
60
99
  state loading {
61
- INIT -> loading
100
+ INIT -> loading when ["!=", (str/default ?id ""), ""]
62
101
  (set @entity.id ?id)
102
+ (set @entity.openDraft (str/default ?draft ""))
63
103
  (fetch RecordItem {
64
104
  emit: { failure: RecordItemLoadFailed, success: RecordItemLoaded }
65
105
  id: ?id
@@ -68,37 +108,173 @@ orbital RecordItemOrbital {
68
108
  ;; Skeleton, not a blank frame: the hand-authored detail traits this
69
109
  ;; atom replaces rendered nothing until the record arrived.
70
110
  (render-ui main { rows: 4, type: skeleton })
71
- RecordItemLoaded -> viewing
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))
72
136
  (set @entity.name (object/get ?data @config.titleField))
73
- (set @entity.description (object/get ?data @config.subtitleField))
74
- (if ["=", @config.detailLook, slide-over]
75
- (render-ui main @config.slideOverBodyContent)
76
- (if ["=", @config.detailLook, page]
77
- (render-ui main @config.pageBodyContent)
78
- (render-ui main @config.bodyContent)))
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 })
79
168
  RecordItemLoadFailed -> error
80
169
  (render-ui main { message: ?error, title: "Couldn't load this record", type: alert, variant: error })
81
170
  }
82
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
+
83
221
  state viewing {
84
222
  ;; No skeleton on re-entry: from a populated panel a skeleton unmounts the
85
223
  ;; record and flickers. First mount is unaffected — `initial: loading` and
86
224
  ;; `state loading`'s own INIT still paint it.
87
- INIT -> loading
225
+ INIT -> loading when ["!=", (str/default ?id ""), ""]
88
226
  (set @entity.id ?id)
227
+ (set @entity.openDraft (str/default ?draft ""))
89
228
  (fetch RecordItem {
90
229
  emit: { failure: RecordItemLoadFailed, success: RecordItemLoaded }
91
230
  id: ?id
92
231
  include: @config.include
93
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
+ })
94
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))
95
245
  (set @entity.name (object/get ?data @config.titleField))
96
- (set @entity.description (object/get ?data @config.subtitleField))
97
- (if ["=", @config.detailLook, slide-over]
98
- (render-ui main @config.slideOverBodyContent)
99
- (if ["=", @config.detailLook, page]
100
- (render-ui main @config.pageBodyContent)
101
- (render-ui main @config.bodyContent)))
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))
102
278
  RecordItemLoadFailed -> error
103
279
  (render-ui main { message: ?error, title: "Couldn't load this record", type: alert, variant: error })
104
280
  CLOSE_VIEW -> idle
@@ -106,14 +282,24 @@ orbital RecordItemOrbital {
106
282
  }
107
283
 
108
284
  state error {
109
- INIT -> loading
285
+ INIT -> loading when ["!=", (str/default ?id ""), ""]
110
286
  (set @entity.id ?id)
287
+ (set @entity.openDraft (str/default ?draft ""))
111
288
  (fetch RecordItem {
112
289
  emit: { failure: RecordItemLoadFailed, success: RecordItemLoaded }
113
290
  id: ?id
114
291
  include: @config.include
115
292
  })
116
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 })
117
303
  ;; The hand-authored traits sent a failed load back to `loading` with no
118
304
  ;; effect, so the page stayed blank forever with no way out. RETRY is the
119
305
  ;; way out.
@@ -131,6 +317,10 @@ orbital RecordItemOrbital {
131
317
  @description "Signals that the record has been successfully loaded."
132
318
  @synonyms "loaded, received, ready, fetched"
133
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"
134
324
  RecordItemLoadFailed
135
325
  @description "Indicates a failure to load the record."
136
326
  @synonyms "error, failed, problem, unsuccessful"
@@ -149,15 +339,67 @@ orbital RecordItemOrbital {
149
339
  @description "Signals a request to remove the record being viewed."
150
340
  @synonyms "remove, discard, delete, archive"
151
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"
152
371
  }
153
372
 
154
373
  listens {
155
374
  INIT {
156
375
  id : string
376
+ slug : string
377
+ draft : string
157
378
  }
158
- @description "Opens the detail surface for the record with this id."
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."
159
380
  @synonyms "open record, view record, show details, drill in"
160
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"
161
403
  OPEN_VIEW {
162
404
  id : string!
163
405
  row : RecordItem
@@ -242,8 +484,8 @@ orbital RecordItemOrbital {
242
484
  @tier "presentation"
243
485
  subtitleField : string = description
244
486
  @label "Which field is the subtitle?"
245
- @description "Name of the field shown under the title (e.g. company, category). Leave at the default when the record has no meaningful second line."
246
- @synonyms "subtitle field, secondary field, subheading"
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"
247
489
  @tier "presentation"
248
490
  include : [string] = []
249
491
  @label "Which relation fields to hydrate?"
@@ -255,10 +497,31 @@ orbital RecordItemOrbital {
255
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."
256
498
  @synonyms "open on click, slide over, row click, blank until opened, list embedded"
257
499
  @tier "policy"
258
- detailLook : "panel" | "slide-over" | "page" = panel
500
+ detailLook : "panel" | "slide-over" | "page" | "document" = panel
259
501
  @label "Detail look"
260
- @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). Selecting a non-panel look swaps which body tree renders after the record loads; any body may still be overridden directly at the call site."
261
- @synonyms "detail style, drawer, slide over, full page"
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."
262
525
  @tier "presentation"
263
526
  bodyContent : render-ui = {
264
527
  children: [{
@@ -322,8 +585,147 @@ orbital RecordItemOrbital {
322
585
  @label "Page look body"
323
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)."
324
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"
325
727
  }
326
728
  }
327
729
 
328
- page "/recorditems" as RecordItemDetailPage -> RecordItemDetail
730
+ page "/recorditems" as RecordItemDetailPage -> RecordItemDetail, RecordItemDetails
329
731
  }
@@ -76,7 +76,7 @@ orbital RelatedItemOrbital {
76
76
  gap: sm
77
77
  type: stack
78
78
  }, { type: divider }, {
79
- entity: ?data
79
+ entity: (array/map ?data (fn row { id: @row.id, name: (object/get @row @config.labelField) }))
80
80
  fields: []
81
81
  look: @config.tableLook
82
82
  renderItem: (fn item {
@@ -218,6 +218,10 @@ orbital RelatedItemOrbital {
218
218
  @label "Section title"
219
219
  @description "Heading shown above the related list."
220
220
  @tier "presentation"
221
+ labelField : string = name
222
+ @label "Which field labels each related row?"
223
+ @description "Name of the field on the (rebound) related row rendered as the row label and carried as SELECT_RELATED's name — set it to `title` (etc.) whenever the bound entity labels rows differently. The rows are mapped through this at render time, because a trait-level `fields {}` rename is declaration-only on the runtime path and every populated row used to render a blank label."
224
+ @tier "presentation"
221
225
  relationField : string = ""
222
226
  @label "Relation field"
223
227
  @description "RelatedItem field compared against SHOW_RELATED's key to scope the fetch to one parent document; empty renders the full unfiltered collection (default)."
@@ -16,7 +16,7 @@ orbital RevisionOrbital {
16
16
  versionNumber : number! @description "A sequential identifier representing the revision's order." @synonyms "sequence, order, iteration"
17
17
  authorId : string @description "Unique identifier for the user who made the change." @synonyms "user identifier, creator, contributor"
18
18
  authorName : string @description "The name of the user who made the change." @synonyms "user, contributor, owner"
19
- content : string @description "The textual data of the revision." @synonyms "text, body, data, value"
19
+ content : string @description "The snapshotted body of the revision — plain text or the rich-text HTML a document-look record carries. Rollback hands the whole row back to the host, which restores content onto the source document." @synonyms "text, body, data, value"
20
20
  summary : string @description "A brief explanation of the revision's changes." @synonyms "description, note, comment, explanation"
21
21
  searchTerm : string @description "The current search query the user submitted." @synonyms "query, term, q" = ""
22
22
  createdAt : string