@almadar/std 16.187.0 → 16.188.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 -0
- 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 +141 -3
- 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 +329 -0
- 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/game/atoms/std-area-fill.lolo +93 -0
- package/behaviors/lolo/ui/game/atoms/std-loop-lock.lolo +79 -0
- package/behaviors/lolo/ui/game/atoms/std-pad-lock.lolo +93 -0
- package/behaviors/lolo/ui/game/atoms/std-patrol-hazard.lolo +14 -7
- package/behaviors/lolo/ui/game/atoms/std-platformer-body.lolo +21 -4
- package/behaviors/lolo/ui/game/atoms/std-velocity-gate.lolo +101 -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 +344 -0
- 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 +1141 -0
- 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/game/atoms/std-area-fill.orb +955 -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 +903 -0
- package/behaviors/registry/ui/game/atoms/std-patrol-hazard.orb +190 -44
- package/behaviors/registry/ui/game/atoms/std-platformer-body.orb +868 -637
- package/behaviors/registry/ui/game/atoms/std-velocity-gate.orb +1040 -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 +7 -4
- 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 +7541 -3153
- package/dist/behaviors/functions/index.d.ts +598 -18
- package/dist/behaviors/functions/index.js +7524 -3154
- package/dist/behaviors/index.d.ts +1 -1
- package/dist/behaviors/index.js +7542 -3154
- package/dist/behaviors/query.js +7541 -3153
- package/dist/behaviors-embeddings.hash.json +3 -3
- package/dist/behaviors-embeddings.json +485 -461
- package/dist/behaviors-registry.json +2612 -934
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7542 -3154
- 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 +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
app std-list "1.0.0"
|
|
2
2
|
"std-list as a Function"
|
|
3
|
+
type FilterSpec = { field : string!, label : string, options : [string] }
|
|
3
4
|
;;
|
|
4
5
|
;; Daisy-chain composition showcase: no `on EVENT { ... }` trait-ref overrides.
|
|
5
6
|
;; Modal + Confirmation trait refs are configured via `config: { ... }` at the
|
|
@@ -71,7 +72,37 @@ orbital ListItemOrbital {
|
|
|
71
72
|
gap: md
|
|
72
73
|
justify: between
|
|
73
74
|
type: stack
|
|
74
|
-
}, { type: divider }, {
|
|
75
|
+
}, { type: divider }, (if @config.bodySearch {
|
|
76
|
+
children: [{
|
|
77
|
+
className: "w-full max-w-md"
|
|
78
|
+
clearable: true
|
|
79
|
+
event: REFETCH_QUERY
|
|
80
|
+
placeholder: @config.searchPlaceholder
|
|
81
|
+
type: search-input
|
|
82
|
+
}]
|
|
83
|
+
direction: horizontal
|
|
84
|
+
gap: sm
|
|
85
|
+
type: stack
|
|
86
|
+
} {
|
|
87
|
+
children: []
|
|
88
|
+
gap: none
|
|
89
|
+
type: stack
|
|
90
|
+
}), (if [">", (array/len @config.filters), 0] {
|
|
91
|
+
children: [{
|
|
92
|
+
entity: ListItem
|
|
93
|
+
event: REFETCH_FILTER
|
|
94
|
+
filters: @config.filters
|
|
95
|
+
look: @config.filterBarLook
|
|
96
|
+
type: filter-group
|
|
97
|
+
}]
|
|
98
|
+
direction: horizontal
|
|
99
|
+
gap: sm
|
|
100
|
+
type: stack
|
|
101
|
+
} {
|
|
102
|
+
children: []
|
|
103
|
+
gap: none
|
|
104
|
+
type: stack
|
|
105
|
+
}), {
|
|
75
106
|
entity: ?data
|
|
76
107
|
fields: [{
|
|
77
108
|
icon: list
|
|
@@ -96,6 +127,7 @@ orbital ListItemOrbital {
|
|
|
96
127
|
label: Delete
|
|
97
128
|
variant: danger
|
|
98
129
|
}]
|
|
130
|
+
pageSize: @config.displayPageSize
|
|
99
131
|
type: data-grid
|
|
100
132
|
}]
|
|
101
133
|
className: "max-w-5xl mx-auto w-full"
|
|
@@ -139,6 +171,114 @@ orbital ListItemOrbital {
|
|
|
139
171
|
emit: { failure: ListItemLoadFailed, success: ListItemLoaded }
|
|
140
172
|
})
|
|
141
173
|
(render-ui main { type: spinner })
|
|
174
|
+
;; REFETCH_* stays in `browsing` and DOES NOT replace the grid with a
|
|
175
|
+
;; spinner — same reasoning as std-browse's REFETCH_* arms: a skeleton
|
|
176
|
+
;; on refetch unmounts the grid and flickers every keystroke. The fetch
|
|
177
|
+
;; fires in the background; the ListItemLoaded arm below re-renders in
|
|
178
|
+
;; place once it lands.
|
|
179
|
+
REFETCH_QUERY -> browsing
|
|
180
|
+
(fetch ListItem {
|
|
181
|
+
emit: { failure: ListItemLoadFailed, success: ListItemLoaded }
|
|
182
|
+
filter: (or ["=", ?searchTerm, ""] (str/includes (object/get @entity name) ?searchTerm))
|
|
183
|
+
})
|
|
184
|
+
REFETCH_FILTER -> browsing
|
|
185
|
+
(fetch ListItem {
|
|
186
|
+
emit: { failure: ListItemLoadFailed, success: ListItemLoaded }
|
|
187
|
+
filter: (or ["=", ?value, ""] ["=", (object/get @entity ?field), ?value])
|
|
188
|
+
})
|
|
189
|
+
;; No filter carried over on REFETCH_PAGE, mirroring std-browse exactly.
|
|
190
|
+
REFETCH_PAGE -> browsing
|
|
191
|
+
(fetch ListItem {
|
|
192
|
+
emit: { failure: ListItemLoadFailed, success: ListItemLoaded }
|
|
193
|
+
limit: @config.pageSize
|
|
194
|
+
offset: ["*", ["-", ?page, 1], @config.pageSize]
|
|
195
|
+
})
|
|
196
|
+
ListItemLoaded -> browsing
|
|
197
|
+
(render-ui main {
|
|
198
|
+
children: [{
|
|
199
|
+
children: [{
|
|
200
|
+
children: [{ name: list, type: icon }, {
|
|
201
|
+
content: ListItems
|
|
202
|
+
type: typography
|
|
203
|
+
variant: h2
|
|
204
|
+
}]
|
|
205
|
+
direction: horizontal
|
|
206
|
+
gap: md
|
|
207
|
+
type: stack
|
|
208
|
+
}, {
|
|
209
|
+
action: CREATE
|
|
210
|
+
icon: plus
|
|
211
|
+
label: Create
|
|
212
|
+
type: button
|
|
213
|
+
variant: primary
|
|
214
|
+
}]
|
|
215
|
+
direction: horizontal
|
|
216
|
+
gap: md
|
|
217
|
+
justify: between
|
|
218
|
+
type: stack
|
|
219
|
+
}, { type: divider }, (if @config.bodySearch {
|
|
220
|
+
children: [{
|
|
221
|
+
className: "w-full max-w-md"
|
|
222
|
+
clearable: true
|
|
223
|
+
event: REFETCH_QUERY
|
|
224
|
+
placeholder: @config.searchPlaceholder
|
|
225
|
+
type: search-input
|
|
226
|
+
}]
|
|
227
|
+
direction: horizontal
|
|
228
|
+
gap: sm
|
|
229
|
+
type: stack
|
|
230
|
+
} {
|
|
231
|
+
children: []
|
|
232
|
+
gap: none
|
|
233
|
+
type: stack
|
|
234
|
+
}), (if [">", (array/len @config.filters), 0] {
|
|
235
|
+
children: [{
|
|
236
|
+
entity: ListItem
|
|
237
|
+
event: REFETCH_FILTER
|
|
238
|
+
filters: @config.filters
|
|
239
|
+
look: @config.filterBarLook
|
|
240
|
+
type: filter-group
|
|
241
|
+
}]
|
|
242
|
+
direction: horizontal
|
|
243
|
+
gap: sm
|
|
244
|
+
type: stack
|
|
245
|
+
} {
|
|
246
|
+
children: []
|
|
247
|
+
gap: none
|
|
248
|
+
type: stack
|
|
249
|
+
}), {
|
|
250
|
+
entity: ?data
|
|
251
|
+
fields: [{
|
|
252
|
+
icon: list
|
|
253
|
+
label: Name
|
|
254
|
+
name: name
|
|
255
|
+
variant: h4
|
|
256
|
+
}, {
|
|
257
|
+
label: Description
|
|
258
|
+
name: description
|
|
259
|
+
variant: caption
|
|
260
|
+
}, {
|
|
261
|
+
label: Status
|
|
262
|
+
name: status
|
|
263
|
+
variant: badge
|
|
264
|
+
}]
|
|
265
|
+
itemActions: [{
|
|
266
|
+
event: EDIT
|
|
267
|
+
label: Edit
|
|
268
|
+
variant: ghost
|
|
269
|
+
}, {
|
|
270
|
+
event: DELETE
|
|
271
|
+
label: Delete
|
|
272
|
+
variant: danger
|
|
273
|
+
}]
|
|
274
|
+
pageSize: @config.displayPageSize
|
|
275
|
+
type: data-grid
|
|
276
|
+
}]
|
|
277
|
+
className: "max-w-5xl mx-auto w-full"
|
|
278
|
+
direction: vertical
|
|
279
|
+
gap: lg
|
|
280
|
+
type: stack
|
|
281
|
+
})
|
|
142
282
|
}
|
|
143
283
|
|
|
144
284
|
state error {
|
|
@@ -190,6 +330,58 @@ orbital ListItemOrbital {
|
|
|
190
330
|
ListItemPersistor.ITEM_CREATED -> INIT
|
|
191
331
|
ListItemPersistor.ITEM_UPDATED -> INIT
|
|
192
332
|
ListItemPersistor.ITEM_DELETED -> INIT
|
|
333
|
+
REFETCH_QUERY {
|
|
334
|
+
searchTerm : string!
|
|
335
|
+
}
|
|
336
|
+
@description "Triggers a list refresh based on the current search term."
|
|
337
|
+
@synonyms "refresh, reload, update, re-query, search"
|
|
338
|
+
@tier "domain"
|
|
339
|
+
REFETCH_FILTER {
|
|
340
|
+
field : string!
|
|
341
|
+
value : string!
|
|
342
|
+
}
|
|
343
|
+
@description "Triggers a list refresh based on current filter criteria."
|
|
344
|
+
@synonyms "refresh, filter, update, reload"
|
|
345
|
+
@tier "domain"
|
|
346
|
+
REFETCH_PAGE {
|
|
347
|
+
page : number!
|
|
348
|
+
}
|
|
349
|
+
@description "Triggers a list page refresh."
|
|
350
|
+
@synonyms "reload, refresh, update, paginate"
|
|
351
|
+
@tier "domain"
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
config {
|
|
355
|
+
bodySearch : boolean = false
|
|
356
|
+
@label "Show the list's own search box?"
|
|
357
|
+
@description "Embeds a search box above the grid. Set true only when the page has no other search affordance (e.g. no page-level std-search), so the surface shows one search box instead of two. False is the default — this atom has never had a search box (0 consumers today)."
|
|
358
|
+
@synonyms "inline search, list search, built-in search, show search"
|
|
359
|
+
@tier "presentation"
|
|
360
|
+
searchPlaceholder : string = "Search…"
|
|
361
|
+
@label "Search box placeholder text"
|
|
362
|
+
@description "Hint text inside the list search box."
|
|
363
|
+
@synonyms "search hint, placeholder, search box text"
|
|
364
|
+
@tier "presentation"
|
|
365
|
+
filters : [FilterSpec] = []
|
|
366
|
+
@label "Which filters should appear above the grid?"
|
|
367
|
+
@description "Dropdown filter facets by field, rendered as an embedded filter bar above the rows; each selection fires REFETCH_FILTER on this trait. Empty (the default) = no filter bar, the right setting whenever the page already composes std-filter beside this list and routes XFilter.FILTER -> REFETCH_FILTER — that is the wired owner and remains the primary path. Use this knob only for a standalone list with no page-level filter affordance."
|
|
368
|
+
@synonyms "filter bar, faceted filters, field filters, dropdowns"
|
|
369
|
+
@tier "presentation"
|
|
370
|
+
filterBarLook : "toolbar" | "chips" | "pills" | "popover-trigger" | "inline-column-header" = toolbar
|
|
371
|
+
@label "Filter bar look"
|
|
372
|
+
@description "Visual treatment for the embedded filter bar, mirroring std-filter's own enum. Only applies when `filters` is non-empty."
|
|
373
|
+
@synonyms "filter bar style, facet style, chips, pills"
|
|
374
|
+
@tier "presentation"
|
|
375
|
+
pageSize : number = 20
|
|
376
|
+
@label "How many rows per page when paging?"
|
|
377
|
+
@description "Records fetched from the server per REFETCH_PAGE request. The initial load and search/filter refetches remain unpaginated (load everything that matches) unless a consumer wires REFETCH_PAGE, so the default list surface is unchanged."
|
|
378
|
+
@synonyms "fetch limit, records per page, page limit"
|
|
379
|
+
@tier "presentation"
|
|
380
|
+
displayPageSize : number = 20
|
|
381
|
+
@label "How many rows to show at once?"
|
|
382
|
+
@description "Rows visible per page before the data-grid's own pagination control appears — 20 matches the CRUD-list density sweet spot (Miller's law, 10-25 rows)."
|
|
383
|
+
@synonyms "visible rows, items per page, display limit, show per page"
|
|
384
|
+
@tier "presentation"
|
|
193
385
|
}
|
|
194
386
|
}
|
|
195
387
|
|
|
@@ -0,0 +1,329 @@
|
|
|
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"
|
|
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 RecordSection = { title : string!, fields : [string] }
|
|
8
|
+
|
|
9
|
+
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
|
+
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
|
+
|
|
13
|
+
entity RecordItem [runtime] {
|
|
14
|
+
id : string!
|
|
15
|
+
name : string! @description "The record's title or identifier." @synonyms "title, label, heading"
|
|
16
|
+
description : string @description "A textual explanation or summary of the record." @synonyms "summary, details, notes"
|
|
17
|
+
status : "active" | "inactive" | "pending" @description "The record's current state." @synonyms "state, condition, stage" = active
|
|
18
|
+
createdAt : string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
trait RecordItemDetail -> @rebindable RecordItem [interaction, instance] {
|
|
22
|
+
;; One machine, two shapes. A page detail (`openOn: init`, the default)
|
|
23
|
+
;; falls straight through idle on INIT and behaves exactly as before. An
|
|
24
|
+
;; overlay (`openOn: event`) stays blank until a row-click OPEN_VIEW hands
|
|
25
|
+
;; it an id, which is the topology 23 hand-authored viewer traits use and
|
|
26
|
+
;; the reason they could not compose an `initial: loading` atom: mounting
|
|
27
|
+
;; one on a list page would fetch a record it has no id for.
|
|
28
|
+
initial: idle
|
|
29
|
+
|
|
30
|
+
state idle {
|
|
31
|
+
INIT -> loading when ["=", @config.openOn, "init"]
|
|
32
|
+
(set @entity.id ?id)
|
|
33
|
+
(fetch RecordItem {
|
|
34
|
+
emit: { failure: RecordItemLoadFailed, success: RecordItemLoaded }
|
|
35
|
+
id: ?id
|
|
36
|
+
include: @config.include
|
|
37
|
+
})
|
|
38
|
+
(render-ui main { rows: 4, type: skeleton })
|
|
39
|
+
;; ONE unguarded arm on purpose. This briefly carried a second,
|
|
40
|
+
;; `when @config.preloaded`-guarded arm that rendered the OPEN_VIEW row
|
|
41
|
+
;; directly to skip a round-trip. Both execution paths mishandled it:
|
|
42
|
+
;; the live runtime PASSED the guard with `preloaded: false`
|
|
43
|
+
;; (StateMachineCore's permissive mode turns an unresolved config guard
|
|
44
|
+
;; into true), so overlays rendered from `?row` and never fetched —
|
|
45
|
+
;; silently skipping `include` relation hydration; and the canonical
|
|
46
|
+
;; evaluator FAILED the guard and then did not fall through to this arm,
|
|
47
|
+
;; reporting the event as not firing at all. A guarded arm is not
|
|
48
|
+
;; currently a trustworthy correctness boundary, so the atom no longer
|
|
49
|
+
;; depends on one. Zero consumers had opted into the skip.
|
|
50
|
+
OPEN_VIEW -> loading
|
|
51
|
+
(set @entity.id ?id)
|
|
52
|
+
(fetch RecordItem {
|
|
53
|
+
emit: { failure: RecordItemLoadFailed, success: RecordItemLoaded }
|
|
54
|
+
id: ?id
|
|
55
|
+
include: @config.include
|
|
56
|
+
})
|
|
57
|
+
(render-ui main { rows: 4, type: skeleton })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
state loading {
|
|
61
|
+
INIT -> loading
|
|
62
|
+
(set @entity.id ?id)
|
|
63
|
+
(fetch RecordItem {
|
|
64
|
+
emit: { failure: RecordItemLoadFailed, success: RecordItemLoaded }
|
|
65
|
+
id: ?id
|
|
66
|
+
include: @config.include
|
|
67
|
+
})
|
|
68
|
+
;; Skeleton, not a blank frame: the hand-authored detail traits this
|
|
69
|
+
;; atom replaces rendered nothing until the record arrived.
|
|
70
|
+
(render-ui main { rows: 4, type: skeleton })
|
|
71
|
+
RecordItemLoaded -> viewing
|
|
72
|
+
(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)))
|
|
79
|
+
RecordItemLoadFailed -> error
|
|
80
|
+
(render-ui main { message: ?error, title: "Couldn't load this record", type: alert, variant: error })
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
state viewing {
|
|
84
|
+
;; No skeleton on re-entry: from a populated panel a skeleton unmounts the
|
|
85
|
+
;; record and flickers. First mount is unaffected — `initial: loading` and
|
|
86
|
+
;; `state loading`'s own INIT still paint it.
|
|
87
|
+
INIT -> loading
|
|
88
|
+
(set @entity.id ?id)
|
|
89
|
+
(fetch RecordItem {
|
|
90
|
+
emit: { failure: RecordItemLoadFailed, success: RecordItemLoaded }
|
|
91
|
+
id: ?id
|
|
92
|
+
include: @config.include
|
|
93
|
+
})
|
|
94
|
+
RecordItemLoaded -> viewing
|
|
95
|
+
(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)))
|
|
102
|
+
RecordItemLoadFailed -> error
|
|
103
|
+
(render-ui main { message: ?error, title: "Couldn't load this record", type: alert, variant: error })
|
|
104
|
+
CLOSE_VIEW -> idle
|
|
105
|
+
(render-ui main null)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
state error {
|
|
109
|
+
INIT -> loading
|
|
110
|
+
(set @entity.id ?id)
|
|
111
|
+
(fetch RecordItem {
|
|
112
|
+
emit: { failure: RecordItemLoadFailed, success: RecordItemLoaded }
|
|
113
|
+
id: ?id
|
|
114
|
+
include: @config.include
|
|
115
|
+
})
|
|
116
|
+
(render-ui main { rows: 4, type: skeleton })
|
|
117
|
+
;; The hand-authored traits sent a failed load back to `loading` with no
|
|
118
|
+
;; effect, so the page stayed blank forever with no way out. RETRY is the
|
|
119
|
+
;; way out.
|
|
120
|
+
RETRY -> loading
|
|
121
|
+
(fetch RecordItem {
|
|
122
|
+
emit: { failure: RecordItemLoadFailed, success: RecordItemLoaded }
|
|
123
|
+
id: @entity.id
|
|
124
|
+
include: @config.include
|
|
125
|
+
})
|
|
126
|
+
(render-ui main { rows: 4, type: skeleton })
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
emits {
|
|
130
|
+
RecordItemLoaded
|
|
131
|
+
@description "Signals that the record has been successfully loaded."
|
|
132
|
+
@synonyms "loaded, received, ready, fetched"
|
|
133
|
+
@tier "domain"
|
|
134
|
+
RecordItemLoadFailed
|
|
135
|
+
@description "Indicates a failure to load the record."
|
|
136
|
+
@synonyms "error, failed, problem, unsuccessful"
|
|
137
|
+
@tier "internal"
|
|
138
|
+
EDIT -> external {
|
|
139
|
+
id : string!
|
|
140
|
+
row : RecordItem
|
|
141
|
+
}
|
|
142
|
+
@description "Signals a request to edit the record being viewed."
|
|
143
|
+
@synonyms "modify, update, change"
|
|
144
|
+
@tier "domain"
|
|
145
|
+
DELETE -> external {
|
|
146
|
+
id : string!
|
|
147
|
+
row : RecordItem
|
|
148
|
+
}
|
|
149
|
+
@description "Signals a request to remove the record being viewed."
|
|
150
|
+
@synonyms "remove, discard, delete, archive"
|
|
151
|
+
@tier "domain"
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
listens {
|
|
155
|
+
INIT {
|
|
156
|
+
id : string
|
|
157
|
+
}
|
|
158
|
+
@description "Opens the detail surface for the record with this id."
|
|
159
|
+
@synonyms "open record, view record, show details, drill in"
|
|
160
|
+
@tier "domain"
|
|
161
|
+
OPEN_VIEW {
|
|
162
|
+
id : string!
|
|
163
|
+
row : RecordItem
|
|
164
|
+
}
|
|
165
|
+
@description "A row was opened from a list; shows that record in this surface."
|
|
166
|
+
@synonyms "open record, view row, show details, drill in, expand"
|
|
167
|
+
@tier "domain"
|
|
168
|
+
CLOSE_VIEW
|
|
169
|
+
@description "Closes the record surface and returns it to blank."
|
|
170
|
+
@synonyms "close, dismiss, back to list"
|
|
171
|
+
@tier "domain"
|
|
172
|
+
RETRY
|
|
173
|
+
@description "Re-runs the fetch after a failed load."
|
|
174
|
+
@synonyms "try again, reload, refresh"
|
|
175
|
+
@tier "domain"
|
|
176
|
+
RecordItemLoadFailed {
|
|
177
|
+
error : string
|
|
178
|
+
code : string
|
|
179
|
+
}
|
|
180
|
+
@description "Indicates a failure to load the record."
|
|
181
|
+
@synonyms "error, failure, problem, unsuccessful"
|
|
182
|
+
@tier "internal"
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
config {
|
|
186
|
+
fields : [RecordFieldSpec] = [{
|
|
187
|
+
header: Name
|
|
188
|
+
key: name
|
|
189
|
+
}, {
|
|
190
|
+
header: Description
|
|
191
|
+
key: description
|
|
192
|
+
}, {
|
|
193
|
+
header: Status
|
|
194
|
+
key: status
|
|
195
|
+
}, {
|
|
196
|
+
format: date
|
|
197
|
+
header: Created
|
|
198
|
+
key: createdAt
|
|
199
|
+
}]
|
|
200
|
+
@label "Which fields appear in the record panel?"
|
|
201
|
+
@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."
|
|
202
|
+
@synonyms "detail fields, record fields, panel fields, shown fields"
|
|
203
|
+
@tier "presentation"
|
|
204
|
+
sections : [RecordSection] = []
|
|
205
|
+
@label "How should the fields be grouped?"
|
|
206
|
+
@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."
|
|
207
|
+
@synonyms "field groups, grouped fields, panel sections, fieldsets"
|
|
208
|
+
@tier "presentation"
|
|
209
|
+
actions : [RecordAction] = [{
|
|
210
|
+
event: EDIT
|
|
211
|
+
label: Edit
|
|
212
|
+
variant: secondary
|
|
213
|
+
}, {
|
|
214
|
+
event: DELETE
|
|
215
|
+
label: Delete
|
|
216
|
+
variant: danger
|
|
217
|
+
}]
|
|
218
|
+
@label "What actions can users take on this record?"
|
|
219
|
+
@description "Buttons on the panel header. One primary at most; the rest collapse past maxInlineActions."
|
|
220
|
+
@synonyms "record actions, detail actions, panel buttons"
|
|
221
|
+
@tier "presentation"
|
|
222
|
+
maxInlineActions : number = 3
|
|
223
|
+
@label "How many actions before collapsing?"
|
|
224
|
+
@description "Actions beyond this count collapse into a '⋯' overflow menu."
|
|
225
|
+
@synonyms "inline actions limit, overflow threshold"
|
|
226
|
+
@tier "presentation"
|
|
227
|
+
showActions : boolean = true
|
|
228
|
+
@label "Show the action buttons?"
|
|
229
|
+
@description "Set false for a read-only record view; a panel with actions nothing listens to is a dead affordance."
|
|
230
|
+
@synonyms "read only, hide actions, view only"
|
|
231
|
+
@tier "presentation"
|
|
232
|
+
;; Field NAMES, not binding paths. A path knob (`subtitleBinding:
|
|
233
|
+
;; @entity.company`) makes the consumer responsible for an @entity field
|
|
234
|
+
;; this atom never sets — an ORB_BINDING_ENTITY_FIELD_NEVER_SET and an
|
|
235
|
+
;; empty subtitle, which is exactly what the first migration of std-crm
|
|
236
|
+
;; ContactDetail hit. Naming the source field instead lets the atom do the
|
|
237
|
+
;; (set) itself off the payload, so the trap cannot be reintroduced.
|
|
238
|
+
titleField : string = name
|
|
239
|
+
@label "Which field is the record's title?"
|
|
240
|
+
@description "Name of the field on the loaded record shown as the panel title (e.g. contactName, subject, title)."
|
|
241
|
+
@synonyms "title field, heading field, name field, label field"
|
|
242
|
+
@tier "presentation"
|
|
243
|
+
subtitleField : string = description
|
|
244
|
+
@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"
|
|
247
|
+
@tier "presentation"
|
|
248
|
+
include : [string] = []
|
|
249
|
+
@label "Which relation fields to hydrate?"
|
|
250
|
+
@description "Entity relation field names to hydrate on fetch, so panel fields can use dot-paths like owner.name. Empty = no hydration, the default."
|
|
251
|
+
@synonyms "relation hydration, populate relations, joined fields"
|
|
252
|
+
@tier "internal"
|
|
253
|
+
openOn : "init" | "event" = init
|
|
254
|
+
@label "When does this surface load?"
|
|
255
|
+
@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
|
+
@synonyms "open on click, slide over, row click, blank until opened, list embedded"
|
|
257
|
+
@tier "policy"
|
|
258
|
+
detailLook : "panel" | "slide-over" | "page" = panel
|
|
259
|
+
@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"
|
|
262
|
+
@tier "presentation"
|
|
263
|
+
bodyContent : render-ui = {
|
|
264
|
+
children: [{
|
|
265
|
+
actions: @config.actions
|
|
266
|
+
entity: ?data
|
|
267
|
+
fields: @config.fields
|
|
268
|
+
initialData: ?data
|
|
269
|
+
maxInlineActions: @config.maxInlineActions
|
|
270
|
+
sections: @config.sections
|
|
271
|
+
showActions: @config.showActions
|
|
272
|
+
subtitle: @entity.description
|
|
273
|
+
title: @entity.name
|
|
274
|
+
type: detail-panel
|
|
275
|
+
}]
|
|
276
|
+
direction: vertical
|
|
277
|
+
gap: lg
|
|
278
|
+
type: stack
|
|
279
|
+
}
|
|
280
|
+
@label "Body content tree"
|
|
281
|
+
@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."
|
|
282
|
+
@tier "internal"
|
|
283
|
+
slideOverBodyContent : render-ui = {
|
|
284
|
+
children: [{
|
|
285
|
+
actions: @config.actions
|
|
286
|
+
entity: ?data
|
|
287
|
+
fields: @config.fields
|
|
288
|
+
initialData: ?data
|
|
289
|
+
maxInlineActions: @config.maxInlineActions
|
|
290
|
+
sections: @config.sections
|
|
291
|
+
showActions: @config.showActions
|
|
292
|
+
slideOver: true
|
|
293
|
+
subtitle: @entity.description
|
|
294
|
+
title: @entity.name
|
|
295
|
+
type: detail-panel
|
|
296
|
+
}]
|
|
297
|
+
direction: vertical
|
|
298
|
+
gap: none
|
|
299
|
+
type: stack
|
|
300
|
+
}
|
|
301
|
+
@label "Slide-over look body"
|
|
302
|
+
@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."
|
|
303
|
+
@tier "internal"
|
|
304
|
+
pageBodyContent : render-ui = {
|
|
305
|
+
children: [{
|
|
306
|
+
actions: @config.actions
|
|
307
|
+
entity: ?data
|
|
308
|
+
fields: @config.fields
|
|
309
|
+
initialData: ?data
|
|
310
|
+
maxInlineActions: @config.maxInlineActions
|
|
311
|
+
sections: @config.sections
|
|
312
|
+
showActions: @config.showActions
|
|
313
|
+
subtitle: @entity.description
|
|
314
|
+
title: @entity.name
|
|
315
|
+
type: detail-panel
|
|
316
|
+
}]
|
|
317
|
+
className: "w-full max-w-4xl"
|
|
318
|
+
direction: vertical
|
|
319
|
+
gap: lg
|
|
320
|
+
type: stack
|
|
321
|
+
}
|
|
322
|
+
@label "Page look body"
|
|
323
|
+
@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
|
+
@tier "internal"
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
page "/recorditems" as RecordItemDetailPage -> RecordItemDetail
|
|
329
|
+
}
|