@almadar/std 16.148.0 → 16.149.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 (34) hide show
  1. package/behaviors/lolo/ui/core/atoms/std-app-search.lolo +1 -1
  2. package/behaviors/lolo/ui/core/atoms/std-billable-hour.lolo +86 -41
  3. package/behaviors/lolo/ui/core/atoms/std-board.lolo +29 -16
  4. package/behaviors/lolo/ui/core/atoms/std-browse.lolo +187 -83
  5. package/behaviors/lolo/ui/core/atoms/std-dashboard-grid-embedded.lolo +8 -0
  6. package/behaviors/lolo/ui/core/atoms/std-file-store.lolo +1 -1
  7. package/behaviors/lolo/ui/core/atoms/std-stats.lolo +14 -10
  8. package/behaviors/lolo/ui/core/atoms/std-version-history.lolo +1 -29
  9. package/behaviors/registry/factory-signatures.json +9 -13
  10. package/behaviors/registry/ui/core/atoms/std-app-search.orb +9 -1
  11. package/behaviors/registry/ui/core/atoms/std-billable-hour.orb +242 -50
  12. package/behaviors/registry/ui/core/atoms/std-board.orb +86 -20
  13. package/behaviors/registry/ui/core/atoms/std-browse.orb +377 -119
  14. package/behaviors/registry/ui/core/atoms/std-dashboard-grid-embedded.orb +41 -29
  15. package/behaviors/registry/ui/core/atoms/std-file-store.orb +0 -3
  16. package/behaviors/registry/ui/core/atoms/std-stats.orb +267 -238
  17. package/behaviors/registry/ui/core/atoms/std-version-history.orb +1 -38
  18. package/dist/behaviors/behaviors-embeddings.json +475 -475
  19. package/dist/behaviors/behaviors-registry.json +10 -12
  20. package/dist/behaviors/knob-embeddings.json +1 -1
  21. package/dist/behaviors/registry/factory-signatures.json +9 -13
  22. package/dist/behaviors/registry/ui/core/atoms/std-app-search.orb +9 -1
  23. package/dist/behaviors/registry/ui/core/atoms/std-billable-hour.orb +242 -50
  24. package/dist/behaviors/registry/ui/core/atoms/std-board.orb +86 -20
  25. package/dist/behaviors/registry/ui/core/atoms/std-browse.orb +377 -119
  26. package/dist/behaviors/registry/ui/core/atoms/std-dashboard-grid-embedded.orb +41 -29
  27. package/dist/behaviors/registry/ui/core/atoms/std-file-store.orb +0 -3
  28. package/dist/behaviors/registry/ui/core/atoms/std-stats.orb +267 -238
  29. package/dist/behaviors/registry/ui/core/atoms/std-version-history.orb +1 -38
  30. package/dist/behaviors-embeddings.json +475 -475
  31. package/dist/behaviors-registry.json +10 -12
  32. package/dist/knob-embeddings.json +1 -1
  33. package/dist/registry/factory-signatures.json +9 -13
  34. package/package.json +8 -1
@@ -113,7 +113,7 @@ orbital AppSearchOrbital {
113
113
  type: typography
114
114
  variant: caption
115
115
  color: muted
116
- content: "Matching “@entity.searchTerm”"
116
+ content: (str/concat (str/concat "Matching “" @entity.searchTerm) "”")
117
117
  }, { type: divider }, {
118
118
  type: data-grid
119
119
  entity: @payload.data
@@ -1,6 +1,10 @@
1
1
  app std-billable-hour "1.0.0"
2
2
  "std-billable-hour — generic billable-hour timesheet primitive for legal, agency, and consulting domains. Lists entries in `browsing`; opens entry detail in `viewing_single` with Invoice / Write Off actions."
3
+ type ItemAction = { label : string!, event : string, navigatesTo : string, variant : string, icon : string }
4
+ type ColumnSpec = { key : string!, field : string, header : string, width : string, align : string, className : string, weight : string, format : string, icon : string }
3
5
  orbital BillableHourOrbital {
6
+ uses TableView from "std/behaviors/ui-table-view"
7
+
4
8
  type BillableHourLoaded = Event { data : [BillableHour] } "Fired when the billable-hour collection finishes loading"
5
9
 
6
10
  type BillableHourLoadFailed = Event { error : string, code : string } "Fired when the billable-hour collection fails to load"
@@ -19,6 +23,20 @@ orbital BillableHourOrbital {
19
23
  invoiceStatus : "unbilled" | "invoiced" | "paid" | "written_off" @description "The current billing status of the hour entry." @synonyms "status, billing status, payment status" = unbilled
20
24
  }
21
25
 
26
+ trait DenseHoursTable = TableView.traits.TableViewRender -> BillableHour {
27
+ config {
28
+ columns: @config.columns
29
+ emptyMessage: "No time entries"
30
+ itemActions: @config.itemActions
31
+ look: @config.tableLook
32
+ maxInlineActions: 2
33
+ }
34
+
35
+ listens {
36
+ BillableHourTimesheet.BillableHourLoaded -> TableViewLoaded
37
+ }
38
+ }
39
+
22
40
  trait BillableHourTimesheet -> BillableHour [interaction, collection] {
23
41
  initial: loading
24
42
 
@@ -53,46 +71,11 @@ orbital BillableHourOrbital {
53
71
  gap: sm
54
72
  type: stack
55
73
  }, { type: divider }, {
56
- cols: 1
57
- entity: ?data
58
- fields: [{
59
- label: Worker
60
- name: workerName
61
- variant: caption
62
- }, {
63
- label: Matter
64
- name: matterId
65
- variant: caption
66
- }, {
67
- label: Date
68
- name: workDate
69
- variant: caption
70
- }, {
71
- label: Hours
72
- name: hours
73
- variant: caption
74
- }, {
75
- label: Rate
76
- name: rate
77
- variant: caption
78
- }, {
79
- label: Status
80
- name: invoiceStatus
81
- variant: badge
82
- }]
83
- gap: sm
84
- itemActions: [{
85
- event: OPEN_HOURS
86
- icon: arrow-right
87
- label: Open
88
- variant: primary
89
- }, {
90
- event: WRITE_OFF_HOURS
91
- label: "Write Off"
92
- variant: danger
93
- }]
94
- look: @config.tableLook
95
- type: data-grid
74
+ children: [@trait.DenseHoursTable]
75
+ className: overflow-x-auto
76
+ direction: vertical
77
+ gap: none
78
+ type: stack
96
79
  }]
97
80
  direction: vertical
98
81
  gap: md
@@ -107,6 +90,11 @@ orbital BillableHourOrbital {
107
90
  }
108
91
 
109
92
  state browsing {
93
+ INIT -> loading
94
+ (fetch BillableHour {
95
+ emit: { failure: BillableHourLoadFailed, success: BillableHourLoaded }
96
+ })
97
+ (render-ui main { rows: 4, type: skeleton })
110
98
  OPEN_HOURS -> viewing_single
111
99
  (set @entity.id ?row.id)
112
100
  (set @entity.workerId ?row.workerId)
@@ -398,10 +386,67 @@ orbital BillableHourOrbital {
398
386
  @label "Section title"
399
387
  @description "Heading shown above the time-entries list"
400
388
  @tier "presentation"
401
- tableLook : "dense" | "spacious" | "striped" | "borderless" | "card-rows" = dense
389
+ tableLook : "dense" | "spacious" | "striped" | "borderless" | "bordered" = dense
402
390
  @label "Table look"
403
391
  @description "Layer 2 visual treatment for the data table rendered by this atom."
404
392
  @tier "presentation"
393
+ columns : [ColumnSpec] = [{
394
+ align: left
395
+ field: workerName
396
+ header: Worker
397
+ key: workerName
398
+ width: "minmax(8rem, 1fr)"
399
+ }, {
400
+ align: left
401
+ field: matterId
402
+ header: Matter
403
+ key: matterId
404
+ width: "8rem"
405
+ }, {
406
+ align: left
407
+ field: workDate
408
+ format: date
409
+ header: Date
410
+ key: workDate
411
+ width: "8rem"
412
+ }, {
413
+ align: right
414
+ field: hours
415
+ format: number
416
+ header: Hours
417
+ key: hours
418
+ width: "6rem"
419
+ }, {
420
+ align: right
421
+ field: rate
422
+ format: currency
423
+ header: Rate
424
+ key: rate
425
+ width: "7rem"
426
+ }, {
427
+ align: center
428
+ field: invoiceStatus
429
+ format: badge
430
+ header: Status
431
+ key: invoiceStatus
432
+ width: "8rem"
433
+ }]
434
+ @label "Table columns"
435
+ @description "ColumnSpec list for the dense hours ledger table."
436
+ @tier "presentation"
437
+ itemActions : [ItemAction] = [{
438
+ event: OPEN_HOURS
439
+ icon: arrow-right
440
+ label: Open
441
+ variant: ghost
442
+ }, {
443
+ event: WRITE_OFF_HOURS
444
+ label: "Write Off"
445
+ variant: danger
446
+ }]
447
+ @label "Row actions"
448
+ @description "Per-row actions rendered by the hours table."
449
+ @tier "presentation"
405
450
  statLook : "elevated" | "flat" | "progress-backed" | "gauge" | "sparkline" = elevated
406
451
  @label "Stat display look"
407
452
  @description "Layer 2 visual treatment for stat / KPI cards."
@@ -28,6 +28,7 @@ orbital BoardOrbital {
28
28
  currentDescription : string @intrinsic = ""
29
29
  currentStage : string @intrinsic = ""
30
30
  currentNotes : string @intrinsic = ""
31
+ currentRow : object @intrinsic = {}
31
32
  errorMessage : string @intrinsic = ""
32
33
  }
33
34
 
@@ -97,6 +98,7 @@ orbital BoardOrbital {
97
98
  (set @entity.currentDescription ?description)
98
99
  (set @entity.currentStage ?stage)
99
100
  (set @entity.currentNotes ?row.notes)
101
+ (set @entity.currentRow ?row)
100
102
  (render-ui main {
101
103
  children: [{
102
104
  align: center
@@ -164,20 +166,7 @@ orbital BoardOrbital {
164
166
  })
165
167
  type: data-list
166
168
  variant: default
167
- }, {
168
- align: center
169
- children: [{
170
- action: DELETE_CARD
171
- actionPayload: { id: @entity.currentId }
172
- icon: trash-2
173
- label: Delete
174
- type: button
175
- variant: danger
176
- }]
177
- direction: horizontal
178
- gap: sm
179
- type: stack
180
- }]
169
+ }, @config.cardActionsContent]
181
170
  direction: vertical
182
171
  gap: md
183
172
  type: stack
@@ -341,6 +330,13 @@ orbital BoardOrbital {
341
330
  @description "Signals removal of a card from the board."
342
331
  @synonyms "remove, delete, discard"
343
332
  @tier "presentation"
333
+ EDIT_CARD -> external {
334
+ id : string!
335
+ row : object
336
+ }
337
+ @description "Signals a request to edit the currently open card. The board renders no edit form itself — consumers override cardActionsContent with an Edit button and wire this emit to their edit modal/form trait."
338
+ @synonyms "edit, modify, update, change"
339
+ @tier "domain"
344
340
  BoardItemsLoaded {
345
341
  data : [ObjectSpec]
346
342
  }
@@ -516,6 +512,23 @@ orbital BoardOrbital {
516
512
  @label "Board look"
517
513
  @description "Layer 3 body layout: default N-column data-grid ('columns'), or a Trello-style wide-column kanban with sticky column headers ('kanban-classic')."
518
514
  @tier "presentation"
515
+ cardActionsContent : render-ui = {
516
+ align: center
517
+ children: [{
518
+ action: DELETE_CARD
519
+ actionPayload: { id: @entity.currentId }
520
+ icon: trash-2
521
+ label: Delete
522
+ type: button
523
+ variant: danger
524
+ }]
525
+ direction: horizontal
526
+ gap: sm
527
+ type: stack
528
+ }
529
+ @label "Card panel actions"
530
+ @description "Actions row of the open-card panel. Default is the Delete button only. Override to add consumer-wired actions — e.g. an Edit button with action: EDIT_CARD and actionPayload: { id: @entity.currentId, row: @entity.currentRow }, listened to by the organism's edit modal. Never leave a button here without a listener."
531
+ @tier "presentation"
519
532
  bodyContent : render-ui = {
520
533
  children: [{
521
534
  align: center
@@ -702,7 +715,7 @@ orbital BoardOrbital {
702
715
  type: stack
703
716
  }]
704
717
  className: "cursor-grab hover:shadow-lg transition-shadow"
705
- look: elevated
718
+ look: @config.cardLook
706
719
  padding: sm
707
720
  type: card
708
721
  })
@@ -734,7 +747,7 @@ orbital BoardOrbital {
734
747
  type: stack
735
748
  }
736
749
  @label "Kanban classic body content tree"
737
- @description "Render-ui SExpr for boardLook='kanban-classic' — Trello-style wide elevated columns with sticky headers, dense cards with a status badge, always-elevated card look."
750
+ @description "Render-ui SExpr for boardLook='kanban-classic' — Trello-style wide elevated columns with sticky headers, dense cards with a status badge; per-item cards honor config.cardLook (the column wrapper stays elevated — it is column chrome, not a card)."
738
751
  @tier "internal"
739
752
  }
740
753
  }
@@ -1,5 +1,5 @@
1
1
  app std-browse "2.3.0"
2
- "std-browse as a Function — single primitive: data-grid. Owns the fetch + REFETCH (query/filter/page) state machine. Consumers configure fields/imageField/itemActions/cols/gap/variant; the atom renders the grid. Each `fields[]` entry accepts `{ name, label, variant }` plus optional `referenceEntity` + `displayField` to render a foreign-key column as the joined entity's display value (data-grid resolves @list[referenceEntity] at render time when both keys are present). `browseLook` (table default / dense / feed / gallery) selects one of four render bodies; dense/feed/gallery were folded in from std-browse-dense/-feed/-gallery (Phase 5.B) and their consumers retarget by setting browseLook."
2
+ "std-browse as a Function — single primitive: data-grid. Owns the fetch + REFETCH (query/filter/page) state machine. Consumers configure fields/imageField/itemActions/cols/gap/variant; the atom renders the grid. Each `fields[]` entry accepts `{ name, label, variant }` plus optional `referenceEntity` + `displayField` to render a foreign-key column as the joined entity's display value (data-grid resolves @list[referenceEntity] at render time when both keys are present). `browseLook` (table default / dense / feed / gallery / master-detail / triage) selects one of six render bodies; dense/feed/gallery were folded in from std-browse-dense/-feed/-gallery (Phase 5.B); master-detail (split record view driven by the guarded VIEW/CLOSE self-transitions) and triage (inbox rows) were folded in 2026-07-26. Consumers retarget by setting browseLook."
3
3
  type FieldSpec = { name : string!, label : string, variant : string, icon : string, format : string, key : string, header : string }
4
4
  type ItemAction = { label : string!, event : string, navigatesTo : string, variant : string, icon : string }
5
5
  type ColumnSpec = { key : string!, field : string, header : string, width : string, align : string, className : string, weight : string, format : string, icon : string }
@@ -20,6 +20,7 @@ orbital BrowseItemOrbital {
20
20
  uses Typography from "std/behaviors/ui-typography"
21
21
  uses DataGrid from "std/behaviors/ui-data-grid"
22
22
  uses TableView from "std/behaviors/ui-table-view"
23
+ uses DataList from "std/behaviors/ui-data-list"
23
24
 
24
25
  trait DataGrid1 = DataGrid.traits.DataGridRender -> BrowseItem {
25
26
  config {
@@ -28,6 +29,7 @@ orbital BrowseItemOrbital {
28
29
  gap: @config.gap
29
30
  imageField: @config.imageField
30
31
  itemActions: @config.itemActions
32
+ maxInlineActions: @config.maxInlineActions
31
33
  pageSize: @config.displayPageSize
32
34
  }
33
35
 
@@ -44,7 +46,7 @@ orbital BrowseItemOrbital {
44
46
  look: dense
45
47
  maxInlineActions: @config.maxInlineActions
46
48
  pageSize: @config.displayPageSize
47
- selectable: true
49
+ selectable: @config.selectable
48
50
  }
49
51
 
50
52
  listens {
@@ -52,6 +54,20 @@ orbital BrowseItemOrbital {
52
54
  }
53
55
  }
54
56
 
57
+ trait MasterListView = DataList.traits.DataListRender -> BrowseItem {
58
+ config {
59
+ fields: @config.fields
60
+ itemActions: @config.itemActions
61
+ itemClickEvent: VIEW
62
+ maxInlineActions: @config.maxInlineActions
63
+ variant: compact
64
+ }
65
+
66
+ listens {
67
+ BrowseItemBrowse.BrowseItemLoaded -> DataListLoaded
68
+ }
69
+ }
70
+
55
71
  trait BrowseItemBrowse -> @rebindable BrowseItem [interaction, collection] {
56
72
  initial: loading
57
73
 
@@ -62,7 +78,7 @@ orbital BrowseItemOrbital {
62
78
  limit: @config.pageSize
63
79
  offset: 0
64
80
  })
65
- (render-ui main { size: sm, type: spinner })
81
+ (render-ui main { rows: 4, type: skeleton })
66
82
  BrowseItemLoaded -> browsing
67
83
  (if ["=", @config.browseLook, dense]
68
84
  (render-ui main @config.denseBodyContent)
@@ -70,9 +86,13 @@ orbital BrowseItemOrbital {
70
86
  (render-ui main @config.feedBodyContent)
71
87
  (if ["=", @config.browseLook, gallery]
72
88
  (render-ui main @config.galleryBodyContent)
73
- (render-ui main @config.bodyContent))))
89
+ (if ["=", @config.browseLook, master-detail]
90
+ (render-ui main @config.masterDetailBodyContent)
91
+ (if ["=", @config.browseLook, triage]
92
+ (render-ui main @config.triageBodyContent)
93
+ (render-ui main @config.bodyContent))))))
74
94
  BrowseItemLoadFailed -> error
75
- (render-ui main { children: [{ color: error, content: ?error, type: typography, variant: caption }], direction: vertical, type: stack })
95
+ (render-ui main { message: ?error, title: "Couldn't load this list", type: alert, variant: error })
76
96
  }
77
97
 
78
98
  state browsing {
@@ -82,7 +102,7 @@ orbital BrowseItemOrbital {
82
102
  limit: @config.pageSize
83
103
  offset: 0
84
104
  })
85
- (render-ui main { size: sm, type: spinner })
105
+ (render-ui main { rows: 4, type: skeleton })
86
106
  ;; REFETCH_* stays in `browsing` and DOES NOT replace the data-grid
87
107
  ;; with a spinner. Replacing during search-as-you-type unmounts the
88
108
  ;; grid every keystroke and causes a visible flicker. The fetch fires
@@ -109,6 +129,14 @@ orbital BrowseItemOrbital {
109
129
  limit: @config.pageSize
110
130
  offset: ["*", ["-", ?page, 1], @config.pageSize]
111
131
  })
132
+ ;; master-detail only: opening a row re-renders the split view with the
133
+ ;; clicked row's detail (payload ?row feeds the panel). Guard keeps every
134
+ ;; other look's machine untouched — VIEW still reaches external listeners
135
+ ;; either way, and no effect here re-emits the trigger.
136
+ VIEW -> browsing when ["=", @config.browseLook, "master-detail"]
137
+ (render-ui main @config.masterDetailRecordBodyContent)
138
+ CLOSE -> browsing when ["=", @config.browseLook, "master-detail"]
139
+ (render-ui main @config.masterDetailBodyContent)
112
140
  BrowseItemLoaded -> browsing
113
141
  (if ["=", @config.browseLook, dense]
114
142
  (render-ui main @config.denseBodyContent)
@@ -116,7 +144,11 @@ orbital BrowseItemOrbital {
116
144
  (render-ui main @config.feedBodyContent)
117
145
  (if ["=", @config.browseLook, gallery]
118
146
  (render-ui main @config.galleryBodyContent)
119
- (render-ui main @config.bodyContent))))
147
+ (if ["=", @config.browseLook, master-detail]
148
+ (render-ui main @config.masterDetailBodyContent)
149
+ (if ["=", @config.browseLook, triage]
150
+ (render-ui main @config.triageBodyContent)
151
+ (render-ui main @config.bodyContent))))))
120
152
  }
121
153
 
122
154
  state error {
@@ -126,7 +158,7 @@ orbital BrowseItemOrbital {
126
158
  limit: @config.pageSize
127
159
  offset: 0
128
160
  })
129
- (render-ui main { size: sm, type: spinner })
161
+ (render-ui main { rows: 4, type: skeleton })
130
162
  }
131
163
 
132
164
  emits {
@@ -300,11 +332,16 @@ orbital BrowseItemOrbital {
300
332
  @description "Dropdown filter facets by field. Each fires a refetch when the user changes the selection. Empty = no filter bar."
301
333
  @synonyms "filter bar, faceted filters, field filters, column filters, dropdowns"
302
334
  @tier "presentation"
303
- maxInlineActions : number = 99
335
+ maxInlineActions : number = 3
304
336
  @label "How many row actions to show before collapsing?"
305
337
  @description "Actions beyond this count collapse into a '⋯' overflow menu. Set 2–3 for dense tables."
306
338
  @synonyms "inline actions limit, overflow threshold, max buttons per row"
307
339
  @tier "presentation"
340
+ selectable : boolean = false
341
+ @label "Show row selection checkboxes?"
342
+ @description "Adds a checkbox column with select-all to the dense table. Enable only when the surface offers bulk actions — checkboxes with nothing to act on are a dead affordance."
343
+ @synonyms "row checkboxes, multi-select, bulk select, selection column"
344
+ @tier "presentation"
308
345
  bodySearch : boolean = true
309
346
  @label "Show the list's own search box?"
310
347
  @description "The dense/feed/gallery looks embed their own search box above the rows. Set false when the page already provides a search affordance (a toolbar Search molecule), so the surface shows one search box instead of two."
@@ -319,9 +356,14 @@ orbital BrowseItemOrbital {
319
356
  @label "Render pattern"
320
357
  @description "UI pattern used to render the collection. Used by the default bodyContent tree; Layer 3 variants override bodyContent directly instead and may ignore this knob."
321
358
  @tier "internal"
322
- browseLook : "table" | "dense" | "feed" | "gallery" = table
359
+ browseLook : "table" | "dense" | "feed" | "gallery" | "master-detail" | "triage" = table
323
360
  @label "Browse look"
324
- @description "Layer 2 visual treatment for the whole browse surface. table = the default data-grid (bodyContent); dense = spreadsheet-style sticky-header table with search bar (denseBodyContent); feed = chronological card stream (feedBodyContent); gallery = image-first responsive card grid (galleryBodyContent). Selecting a non-table look swaps which body tree renders after rows load; any of the four bodies may still be overridden directly at the call site for a one-off layout."
361
+ @description "Layer 2 visual treatment for the whole browse surface. table = the default data-grid (bodyContent); dense = spreadsheet-style table with toolbar search (denseBodyContent); feed = chronological card stream (feedBodyContent); gallery = image-first responsive card grid (galleryBodyContent); master-detail = record-work split view — dense list left, clicked record's detail right (masterDetailBodyContent); triage = inbox-register compact rows for queue work (triageBodyContent). Selecting a non-table look swaps which body tree renders after rows load; any body may still be overridden directly at the call site for a one-off layout."
362
+ @tier "presentation"
363
+ detailFields : [string] = [name, description, status, createdAt]
364
+ @label "Which fields appear in the master-detail record pane?"
365
+ @description "Field names shown in the detail panel when browseLook = master-detail; the first entry doubles as the pane title. Consumers overriding columns/fields should override this list to match their entity."
366
+ @synonyms "detail columns, record fields, detail pane fields"
325
367
  @tier "presentation"
326
368
  bodyContent : render-ui = {
327
369
  children: [@trait.DataGrid1]
@@ -334,13 +376,13 @@ orbital BrowseItemOrbital {
334
376
  denseBodyContent : render-ui = {
335
377
  children: [(if @config.bodySearch {
336
378
  children: [{
337
- className: w-full
379
+ className: "w-full max-w-md"
338
380
  clearable: true
339
381
  event: REFETCH_QUERY
340
382
  placeholder: @config.searchPlaceholder
341
383
  type: search-input
342
384
  }]
343
- className: w-full
385
+ className: "px-card-md py-card-sm border-b border-[var(--color-border)]"
344
386
  direction: horizontal
345
387
  gap: sm
346
388
  type: stack
@@ -350,13 +392,14 @@ orbital BrowseItemOrbital {
350
392
  type: stack
351
393
  }), {
352
394
  children: [@trait.DenseTableView]
353
- className: "rounded-lg border border-[var(--color-border)] overflow-x-auto bg-[var(--color-card)] shadow-sm"
395
+ className: overflow-x-auto
354
396
  direction: vertical
355
397
  gap: none
356
398
  type: stack
357
399
  }]
400
+ className: "rounded-lg border border-[var(--color-border)] bg-[var(--color-card)] shadow-sm overflow-hidden"
358
401
  direction: vertical
359
- gap: sm
402
+ gap: none
360
403
  type: stack
361
404
  }
362
405
  @label "Dense look body"
@@ -448,6 +491,7 @@ orbital BrowseItemOrbital {
448
491
  }]
449
492
  className: "rounded-lg bg-[var(--color-card)] border border-[var(--color-border)] shadow-sm transition-all duration-normal hover:shadow-main hover:border-[var(--color-border-strong)] hover:-translate-y-px"
450
493
  look: flat-bordered
494
+ padding: none
451
495
  type: card
452
496
  })
453
497
  type: data-list
@@ -471,7 +515,7 @@ orbital BrowseItemOrbital {
471
515
  placeholder: @config.searchPlaceholder
472
516
  type: search-input
473
517
  }]
474
- className: "sticky top-0 z-10 -mx-6 px-6 py-3 bg-[var(--color-surface)]/95 backdrop-blur border-b border-[var(--color-border)]"
518
+ className: "sticky top-0 z-10 py-3 bg-[var(--color-surface)]/95 backdrop-blur"
475
519
  direction: horizontal
476
520
  gap: sm
477
521
  type: stack
@@ -482,76 +526,12 @@ orbital BrowseItemOrbital {
482
526
  }), {
483
527
  className: w-full
484
528
  entity: ?data
485
- fields: []
529
+ fields: @config.fields
486
530
  gap: lg
531
+ imageField: @config.imageField
487
532
  itemActions: @config.itemActions
488
533
  maxInlineActions: @config.maxInlineActions
489
534
  minCardWidth: 260
490
- renderItem: (fn item {
491
- children: [{
492
- align: center
493
- children: [{
494
- className: "text-muted-foreground opacity-50 transition-transform duration-normal group-hover:scale-105"
495
- name: image
496
- size: lg
497
- type: icon
498
- }, {
499
- className: "px-2 text-center truncate max-w-full"
500
- color: muted
501
- content: @config.imageLabelBinding
502
- type: typography
503
- variant: caption
504
- }, {
505
- align: center
506
- children: [{
507
- label: @config.priceBinding
508
- type: badge
509
- variant: primary
510
- }]
511
- className: "absolute top-2 right-2"
512
- direction: horizontal
513
- gap: xs
514
- type: stack
515
- }, {
516
- align: center
517
- children: [{
518
- label: @config.badgeBinding
519
- type: badge
520
- variant: neutral
521
- }]
522
- className: "absolute top-2 left-2"
523
- direction: horizontal
524
- gap: xs
525
- type: stack
526
- }]
527
- className: "relative aspect-square w-full bg-muted overflow-hidden"
528
- direction: vertical
529
- gap: xs
530
- justify: center
531
- type: stack
532
- }, {
533
- children: [{
534
- className: truncate
535
- content: @config.titleBinding
536
- type: typography
537
- variant: h4
538
- }, {
539
- className: line-clamp-2
540
- color: muted
541
- content: @config.descriptionBinding
542
- type: typography
543
- variant: caption
544
- }]
545
- className: p-card-md
546
- direction: vertical
547
- gap: xs
548
- type: stack
549
- }]
550
- className: "group rounded-lg overflow-hidden bg-[var(--color-card)] border border-[var(--color-border)] shadow-sm hover:shadow-hover hover:-translate-y-0.5 transition-all duration-normal cursor-pointer"
551
- direction: vertical
552
- gap: sm
553
- type: stack
554
- })
555
535
  type: data-grid
556
536
  }]
557
537
  className: w-full
@@ -560,7 +540,131 @@ orbital BrowseItemOrbital {
560
540
  type: stack
561
541
  }
562
542
  @label "Gallery look body"
563
- @description "Render-ui SExpr rendered after rows load when browseLook = gallery. Image-first responsive card grid with auto-fit minCardWidth; composes titleBinding/descriptionBinding/badgeBinding/imageLabelBinding/priceBinding/searchPlaceholder. Folded from std-browse-gallery (Phase 5.B)."
543
+ @description "Render-ui SExpr rendered after rows load when browseLook = gallery. Image-first responsive card grid (auto-fit minCardWidth) on the data-grid fields path: imageField renders the card photo when set (falsy-safe when empty), fields drive title/badges/meta. One tree, no renderItem lambda — lambdas do not survive inside (if) arms (2026-07-27)."
544
+ @tier "internal"
545
+ triageBodyContent : render-ui = {
546
+ children: [(if @config.bodySearch {
547
+ children: [{
548
+ className: "w-full max-w-md"
549
+ clearable: true
550
+ event: REFETCH_QUERY
551
+ placeholder: @config.searchPlaceholder
552
+ type: search-input
553
+ }]
554
+ className: "px-card-md py-card-sm border-b border-[var(--color-border)]"
555
+ direction: horizontal
556
+ gap: sm
557
+ type: stack
558
+ } {
559
+ children: []
560
+ gap: none
561
+ type: stack
562
+ }), {
563
+ children: [{
564
+ entity: ?data
565
+ fields: @config.fields
566
+ itemActions: @config.itemActions
567
+ maxInlineActions: @config.maxInlineActions
568
+ type: data-list
569
+ variant: compact
570
+ }]
571
+ className: overflow-x-auto
572
+ direction: vertical
573
+ gap: none
574
+ type: stack
575
+ }]
576
+ className: "rounded-lg border border-[var(--color-border)] bg-[var(--color-card)] shadow-sm overflow-hidden"
577
+ direction: vertical
578
+ gap: none
579
+ type: stack
580
+ }
581
+ @label "Triage look body"
582
+ @description "Render-ui SExpr rendered after rows load when browseLook = triage. Inbox-register queue: one surface with a toolbar search and compact list rows (title + badges + inline meta) built for keyboard-pace queue work; row actions collapse per maxInlineActions. Composes fields/itemActions/searchPlaceholder/bodySearch."
583
+ @tier "internal"
584
+ masterDetailBodyContent : render-ui = {
585
+ children: [{
586
+ children: [{
587
+ className: "w-full max-w-md"
588
+ clearable: true
589
+ event: REFETCH_QUERY
590
+ placeholder: @config.searchPlaceholder
591
+ type: search-input
592
+ }]
593
+ className: "px-card-md py-card-sm border-b border-[var(--color-border)]"
594
+ direction: horizontal
595
+ gap: sm
596
+ type: stack
597
+ }, {
598
+ detail: {
599
+ children: []
600
+ gap: none
601
+ type: stack
602
+ }
603
+ emptyDetail: {
604
+ description: "Select a record from the list to see its details."
605
+ icon: mouse-pointer-click
606
+ title: "Nothing selected"
607
+ type: empty-state
608
+ }
609
+ hasSelection: false
610
+ master: {
611
+ children: [@trait.MasterListView]
612
+ className: "overflow-y-auto"
613
+ direction: vertical
614
+ gap: none
615
+ type: stack
616
+ }
617
+ type: master-detail-layout
618
+ }]
619
+ className: "rounded-lg border border-[var(--color-border)] bg-[var(--color-card)] shadow-sm overflow-hidden"
620
+ direction: vertical
621
+ gap: none
622
+ type: stack
623
+ }
624
+ @label "Master-detail look body (no selection)"
625
+ @description "Render-ui SExpr rendered after rows load when browseLook = master-detail: record-work split view — toolbar search, dense list left, empty-state pane right until a row is opened. Opening a row renders masterDetailRecordBodyContent. The toolbar is always present in this look (bodySearch does not apply)."
626
+ @tier "internal"
627
+ masterDetailRecordBodyContent : render-ui = {
628
+ children: [{
629
+ children: [{
630
+ className: "w-full max-w-md"
631
+ clearable: true
632
+ event: REFETCH_QUERY
633
+ placeholder: @config.searchPlaceholder
634
+ type: search-input
635
+ }]
636
+ className: "px-card-md py-card-sm border-b border-[var(--color-border)]"
637
+ direction: horizontal
638
+ gap: sm
639
+ type: stack
640
+ }, {
641
+ detail: {
642
+ actions: [{
643
+ event: CLOSE
644
+ label: Close
645
+ }]
646
+ fieldNames: @config.detailFields
647
+ initialData: ?row
648
+ showActions: false
649
+ type: detail-panel
650
+ }
651
+ hasSelection: true
652
+ master: {
653
+ children: [@trait.MasterListView]
654
+ className: "overflow-y-auto"
655
+ direction: vertical
656
+ gap: none
657
+ type: stack
658
+ }
659
+ type: master-detail-layout
660
+ }]
661
+ className: "rounded-lg border border-[var(--color-border)] bg-[var(--color-card)] shadow-sm overflow-hidden"
662
+ direction: vertical
663
+ gap: none
664
+ type: stack
665
+ }
666
+ @label "Master-detail look body (record open)"
667
+ @description "Render-ui SExpr rendered by the VIEW transition when browseLook = master-detail: the same split view with the clicked record's detail panel on the right, fed by the VIEW payload row and detailFields."
564
668
  @tier "internal"
565
669
  }
566
670
  }