@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.
- package/behaviors/lolo/ui/core/.hand-authored.json +4 -4
- package/behaviors/lolo/ui/core/atoms/std-board.lolo +138 -4
- package/behaviors/lolo/ui/core/atoms/std-browse.lolo +148 -8
- package/behaviors/lolo/ui/core/atoms/std-calendar.lolo +161 -11
- package/behaviors/lolo/ui/core/atoms/std-event-log.lolo +255 -2
- package/behaviors/lolo/ui/core/atoms/std-gallery.lolo +148 -1
- package/behaviors/lolo/ui/core/atoms/std-list.lolo +193 -1
- package/behaviors/lolo/ui/core/atoms/std-record-detail.lolo +731 -0
- package/behaviors/lolo/ui/core/atoms/std-related.lolo +5 -1
- package/behaviors/lolo/ui/core/atoms/std-selection.lolo +182 -1
- package/behaviors/lolo/ui/core/atoms/std-tree.lolo +113 -3
- package/behaviors/lolo/ui/core/atoms/std-version-history.lolo +1 -1
- package/behaviors/lolo/ui/core/molecules/ui-document-details.lolo +80 -0
- package/behaviors/lolo/ui/core/molecules/ui-document-panel.lolo +213 -0
- package/behaviors/lolo/ui/core/molecules/{ui-rich-block-editor.lolo → ui-rich-text-editor.lolo} +25 -25
- package/behaviors/lolo/ui/game/atoms/std-area-fill.lolo +93 -0
- package/behaviors/lolo/ui/game/atoms/std-choice-gate.lolo +100 -0
- package/behaviors/lolo/ui/game/atoms/std-coef-pad.lolo +114 -0
- package/behaviors/lolo/ui/game/atoms/std-curve-fog.lolo +50 -0
- package/behaviors/lolo/ui/game/atoms/std-loop-lock.lolo +79 -0
- package/behaviors/lolo/ui/game/atoms/std-pad-lock.lolo +97 -0
- package/behaviors/lolo/ui/game/atoms/std-patrol-hazard.lolo +14 -7
- package/behaviors/lolo/ui/game/atoms/std-platformer-body.lolo +48 -5
- package/behaviors/lolo/ui/game/atoms/std-velocity-gate.lolo +101 -0
- package/behaviors/lolo/ui/game/atoms/std-zone-card.lolo +95 -0
- package/behaviors/lolo/ui/game/organisms/std-platformer-board-2d.lolo +9 -0
- package/behaviors/registry/infra/atoms/std-approval-gate.orb +16 -8
- package/behaviors/registry/infra/atoms/std-cache-aside.orb +62 -31
- package/behaviors/registry/infra/atoms/std-circuit-breaker.orb +32 -16
- package/behaviors/registry/infra/atoms/std-data-erasure.orb +16 -8
- package/behaviors/registry/infra/atoms/std-mod-queue.orb +16 -8
- package/behaviors/registry/infra/atoms/std-rate-limiter.orb +28 -14
- package/behaviors/registry/infra/organisms/std-status-lifecycle-control.orb +2 -1
- package/behaviors/registry/ml/organisms/std-escalating-decision.orb +10 -5
- package/behaviors/registry/ml/organisms/std-knowledge-tracing.orb +4 -2
- package/behaviors/registry/ui/core/atoms/std-billable-hour.orb +2 -1
- package/behaviors/registry/ui/core/atoms/std-board.orb +355 -2
- package/behaviors/registry/ui/core/atoms/std-browse.orb +304 -31
- package/behaviors/registry/ui/core/atoms/std-calendar.orb +393 -25
- package/behaviors/registry/ui/core/atoms/std-dashboard-grid-embedded.orb +2 -1
- package/behaviors/registry/ui/core/atoms/std-esign-flow.orb +4 -2
- package/behaviors/registry/ui/core/atoms/std-esign-request.orb +6 -3
- package/behaviors/registry/ui/core/atoms/std-event-log.orb +773 -0
- package/behaviors/registry/ui/core/atoms/std-export.orb +4 -2
- package/behaviors/registry/ui/core/atoms/std-gallery.orb +390 -0
- package/behaviors/registry/ui/core/atoms/std-import.orb +4 -2
- package/behaviors/registry/ui/core/atoms/std-list.orb +451 -0
- package/behaviors/registry/ui/core/atoms/std-record-detail.orb +3360 -0
- package/behaviors/registry/ui/core/atoms/std-related.orb +23 -1
- package/behaviors/registry/ui/core/atoms/std-selection.orb +436 -0
- package/behaviors/registry/ui/core/atoms/std-tree.orb +292 -1
- package/behaviors/registry/ui/core/atoms/std-version-history.orb +1 -1
- package/behaviors/registry/ui/core/molecules/ui-document-details.orb +342 -0
- package/behaviors/registry/ui/core/molecules/ui-document-panel.orb +655 -0
- package/behaviors/registry/ui/core/molecules/{ui-rich-block-editor.orb → ui-rich-text-editor.orb} +84 -97
- package/behaviors/registry/ui/game/atoms/std-area-fill.orb +955 -0
- package/behaviors/registry/ui/game/atoms/std-choice-gate.orb +855 -0
- package/behaviors/registry/ui/game/atoms/std-coef-pad.orb +928 -0
- package/behaviors/registry/ui/game/atoms/std-curve-fog.orb +337 -0
- package/behaviors/registry/ui/game/atoms/std-fx-trail.orb +1 -1
- package/behaviors/registry/ui/game/atoms/std-gate-lock.orb +1 -1
- package/behaviors/registry/ui/game/atoms/std-lives.orb +1 -1
- package/behaviors/registry/ui/game/atoms/std-loop-lock.orb +785 -0
- package/behaviors/registry/ui/game/atoms/std-pad-lock.orb +933 -0
- package/behaviors/registry/ui/game/atoms/std-patrol-hazard.orb +190 -44
- package/behaviors/registry/ui/game/atoms/std-platformer-body.orb +1330 -653
- package/behaviors/registry/ui/game/atoms/std-velocity-gate.orb +1040 -0
- package/behaviors/registry/ui/game/atoms/std-zone-card.orb +642 -0
- package/behaviors/registry/ui/game/organisms/std-arcade-board-3d.orb +8 -4
- package/behaviors/registry/ui/game/organisms/std-crew-board-2d.orb +6 -3
- package/behaviors/registry/ui/game/organisms/std-dungeon-board-2d.orb +10 -5
- package/behaviors/registry/ui/game/organisms/std-market-board-3d.orb +8 -4
- package/behaviors/registry/ui/game/organisms/std-objective-board-3d.orb +8 -4
- package/behaviors/registry/ui/game/organisms/std-platformer-board-2d.orb +36 -1
- package/behaviors/registry/ui/game/organisms/std-survival-board-3d.orb +8 -4
- package/behaviors/registry/ui/game/organisms/std-tactics-board-2d.orb +12 -6
- package/behaviors/registry/ui/game/organisms/std-tactics-board-3d.orb +8 -4
- package/behaviors/registry/ui/game/organisms/std-worldmap-board-2d.orb +6 -3
- package/behaviors/registry/ui/learning/atoms/std-learn-slope-probe.orb +1 -1
- package/behaviors/registry/ui/marketing/atoms/std-marketing-article.orb +2 -1
- package/behaviors/registry/ui/marketing/atoms/std-marketing-footer.orb +2 -1
- package/behaviors/registry/ui/marketing/atoms/std-marketing-split.orb +2 -1
- package/behaviors/registry/ui/marketing/atoms/std-marketing-stats.orb +2 -1
- package/behaviors/registry/ui/marketing/atoms/std-marketing-steps.orb +2 -1
- package/behaviors/registry/ui/marketing/atoms/std-marketing-team.orb +2 -1
- package/dist/behaviors/exports-reader.js +10971 -6255
- package/dist/behaviors/functions/index.d.ts +1205 -256
- package/dist/behaviors/functions/index.js +10932 -6245
- package/dist/behaviors/index.d.ts +1 -1
- package/dist/behaviors/index.js +10972 -6256
- package/dist/behaviors/query.js +10971 -6255
- package/dist/behaviors-embeddings.hash.json +3 -3
- package/dist/behaviors-embeddings.json +485 -469
- package/dist/behaviors-registry.json +6125 -2940
- package/dist/index.d.ts +1 -1
- package/dist/index.js +10972 -6256
- package/dist/knob-embeddings.hash.json +3 -3
- package/dist/knob-embeddings.json +1 -1
- package/dist/registry/factory-signatures.json +1 -1
- package/package.json +4 -4
- package/behaviors/lolo/ui/core/atoms/std-rich-editor.lolo +0 -790
- 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
|
+
}
|