@almadar/std 16.186.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.
Files changed (83) hide show
  1. package/behaviors/lolo/ui/core/.hand-authored.json +4 -0
  2. package/behaviors/lolo/ui/core/atoms/std-board.lolo +138 -4
  3. package/behaviors/lolo/ui/core/atoms/std-browse.lolo +148 -8
  4. package/behaviors/lolo/ui/core/atoms/std-calendar.lolo +141 -3
  5. package/behaviors/lolo/ui/core/atoms/std-event-log.lolo +255 -2
  6. package/behaviors/lolo/ui/core/atoms/std-gallery.lolo +148 -1
  7. package/behaviors/lolo/ui/core/atoms/std-list.lolo +193 -1
  8. package/behaviors/lolo/ui/core/atoms/std-record-detail.lolo +329 -0
  9. package/behaviors/lolo/ui/core/atoms/std-selection.lolo +182 -1
  10. package/behaviors/lolo/ui/core/atoms/std-tree.lolo +113 -3
  11. package/behaviors/lolo/ui/game/atoms/std-area-fill.lolo +93 -0
  12. package/behaviors/lolo/ui/game/atoms/std-loop-lock.lolo +79 -0
  13. package/behaviors/lolo/ui/game/atoms/std-pad-lock.lolo +93 -0
  14. package/behaviors/lolo/ui/game/atoms/std-patrol-hazard.lolo +14 -7
  15. package/behaviors/lolo/ui/game/atoms/std-platformer-body.lolo +21 -4
  16. package/behaviors/lolo/ui/game/atoms/std-velocity-gate.lolo +101 -0
  17. package/behaviors/registry/infra/atoms/std-approval-gate.orb +16 -8
  18. package/behaviors/registry/infra/atoms/std-cache-aside.orb +62 -31
  19. package/behaviors/registry/infra/atoms/std-circuit-breaker.orb +32 -16
  20. package/behaviors/registry/infra/atoms/std-data-erasure.orb +16 -8
  21. package/behaviors/registry/infra/atoms/std-mod-queue.orb +16 -8
  22. package/behaviors/registry/infra/atoms/std-rate-limiter.orb +28 -14
  23. package/behaviors/registry/infra/organisms/std-status-lifecycle-control.orb +2 -1
  24. package/behaviors/registry/ml/organisms/std-escalating-decision.orb +10 -5
  25. package/behaviors/registry/ml/organisms/std-knowledge-tracing.orb +4 -2
  26. package/behaviors/registry/ui/core/atoms/std-billable-hour.orb +2 -1
  27. package/behaviors/registry/ui/core/atoms/std-board.orb +355 -2
  28. package/behaviors/registry/ui/core/atoms/std-browse.orb +304 -31
  29. package/behaviors/registry/ui/core/atoms/std-calendar.orb +344 -0
  30. package/behaviors/registry/ui/core/atoms/std-dashboard-grid-embedded.orb +2 -1
  31. package/behaviors/registry/ui/core/atoms/std-esign-flow.orb +4 -2
  32. package/behaviors/registry/ui/core/atoms/std-esign-request.orb +6 -3
  33. package/behaviors/registry/ui/core/atoms/std-event-log.orb +773 -0
  34. package/behaviors/registry/ui/core/atoms/std-export.orb +4 -2
  35. package/behaviors/registry/ui/core/atoms/std-gallery.orb +390 -0
  36. package/behaviors/registry/ui/core/atoms/std-import.orb +4 -2
  37. package/behaviors/registry/ui/core/atoms/std-list.orb +451 -0
  38. package/behaviors/registry/ui/core/atoms/std-record-detail.orb +1141 -0
  39. package/behaviors/registry/ui/core/atoms/std-selection.orb +436 -0
  40. package/behaviors/registry/ui/core/atoms/std-tree.orb +292 -1
  41. package/behaviors/registry/ui/game/atoms/std-area-fill.orb +955 -0
  42. package/behaviors/registry/ui/game/atoms/std-fx-trail.orb +1 -1
  43. package/behaviors/registry/ui/game/atoms/std-gate-lock.orb +1 -1
  44. package/behaviors/registry/ui/game/atoms/std-lives.orb +1 -1
  45. package/behaviors/registry/ui/game/atoms/std-loop-lock.orb +785 -0
  46. package/behaviors/registry/ui/game/atoms/std-pad-lock.orb +903 -0
  47. package/behaviors/registry/ui/game/atoms/std-patrol-hazard.orb +190 -44
  48. package/behaviors/registry/ui/game/atoms/std-platformer-body.orb +868 -637
  49. package/behaviors/registry/ui/game/atoms/std-velocity-gate.orb +1040 -0
  50. package/behaviors/registry/ui/game/organisms/std-arcade-board-3d.orb +8 -4
  51. package/behaviors/registry/ui/game/organisms/std-crew-board-2d.orb +6 -3
  52. package/behaviors/registry/ui/game/organisms/std-dungeon-board-2d.orb +10 -5
  53. package/behaviors/registry/ui/game/organisms/std-market-board-3d.orb +8 -4
  54. package/behaviors/registry/ui/game/organisms/std-objective-board-3d.orb +8 -4
  55. package/behaviors/registry/ui/game/organisms/std-platformer-board-2d.orb +7 -4
  56. package/behaviors/registry/ui/game/organisms/std-survival-board-3d.orb +8 -4
  57. package/behaviors/registry/ui/game/organisms/std-tactics-board-2d.orb +12 -6
  58. package/behaviors/registry/ui/game/organisms/std-tactics-board-3d.orb +8 -4
  59. package/behaviors/registry/ui/game/organisms/std-worldmap-board-2d.orb +6 -3
  60. package/behaviors/registry/ui/learning/atoms/std-learn-slope-probe.orb +1 -1
  61. package/behaviors/registry/ui/marketing/atoms/std-marketing-article.orb +2 -1
  62. package/behaviors/registry/ui/marketing/atoms/std-marketing-footer.orb +2 -1
  63. package/behaviors/registry/ui/marketing/atoms/std-marketing-split.orb +2 -1
  64. package/behaviors/registry/ui/marketing/atoms/std-marketing-stats.orb +2 -1
  65. package/behaviors/registry/ui/marketing/atoms/std-marketing-steps.orb +2 -1
  66. package/behaviors/registry/ui/marketing/atoms/std-marketing-team.orb +2 -1
  67. package/dist/behaviors/exports-reader.js +7822 -3996
  68. package/dist/behaviors/functions/index.d.ts +598 -270
  69. package/dist/behaviors/functions/index.js +7805 -3989
  70. package/dist/behaviors/index.d.ts +1 -1
  71. package/dist/behaviors/index.js +7823 -3997
  72. package/dist/behaviors/query.js +7822 -3996
  73. package/dist/behaviors-embeddings.hash.json +3 -3
  74. package/dist/behaviors-embeddings.json +535 -519
  75. package/dist/behaviors-registry.json +2877 -2268
  76. package/dist/index.d.ts +1 -1
  77. package/dist/index.js +7823 -3997
  78. package/dist/knob-embeddings.hash.json +3 -3
  79. package/dist/knob-embeddings.json +1 -1
  80. package/dist/registry/factory-signatures.json +1 -1
  81. package/package.json +1 -1
  82. package/behaviors/lolo/ui/core/organisms/ui-ui-slot-renderer.lolo +0 -71
  83. package/behaviors/registry/ui/core/organisms/ui-ui-slot-renderer.orb +0 -234
@@ -1,5 +1,6 @@
1
1
  app std-selection "1.0.0"
2
2
  "std-selection as a Function"
3
+ type FilterSpec = { field : string!, label : string, options : [string] }
3
4
  orbital SelectableItemOrbital {
4
5
  type SelectableItemLoaded = Event { data : [SelectableItem] } "Fired when SelectableItem finishes loading; payload.data holds the list"
5
6
 
@@ -46,7 +47,37 @@ orbital SelectableItemOrbital {
46
47
  direction: horizontal
47
48
  gap: sm
48
49
  type: stack
49
- }, { type: divider }, {
50
+ }, (if @config.bodySearch {
51
+ children: [{
52
+ className: "w-full max-w-md"
53
+ clearable: true
54
+ event: REFETCH_QUERY
55
+ placeholder: @config.searchPlaceholder
56
+ type: search-input
57
+ }]
58
+ direction: horizontal
59
+ gap: sm
60
+ type: stack
61
+ } {
62
+ children: []
63
+ gap: none
64
+ type: stack
65
+ }), (if [">", (array/len @config.filters), 0] {
66
+ children: [{
67
+ entity: SelectableItem
68
+ event: REFETCH_FILTER
69
+ filters: @config.filters
70
+ look: @config.filterBarLook
71
+ type: filter-group
72
+ }]
73
+ direction: horizontal
74
+ gap: sm
75
+ type: stack
76
+ } {
77
+ children: []
78
+ gap: none
79
+ type: stack
80
+ }), { type: divider }, {
50
81
  color: muted
51
82
  content: "Choose a selectableitem to continue."
52
83
  type: typography
@@ -61,6 +92,7 @@ orbital SelectableItemOrbital {
61
92
  label: Select
62
93
  }]
63
94
  look: @config.tableLook
95
+ pageSize: @config.displayPageSize
64
96
  renderItem: (fn item {
65
97
  align: center
66
98
  children: [{ label: @item.name, type: checkbox }]
@@ -105,6 +137,106 @@ orbital SelectableItemOrbital {
105
137
  }
106
138
 
107
139
  state idle {
140
+ ;; REFETCH_* stays in `idle` and DOES NOT replace the pick-list with a
141
+ ;; spinner — same reasoning as std-browse's REFETCH_* arms: a skeleton
142
+ ;; on refetch unmounts the list and flickers every keystroke. The fetch
143
+ ;; fires in the background; the SelectableItemLoaded arm below
144
+ ;; re-renders in place once it lands.
145
+ REFETCH_QUERY -> idle
146
+ (fetch SelectableItem {
147
+ emit: { failure: SelectableItemLoadFailed, success: SelectableItemLoaded }
148
+ filter: (or ["=", ?searchTerm, ""] (str/includes (object/get @entity name) ?searchTerm))
149
+ })
150
+ REFETCH_FILTER -> idle
151
+ (fetch SelectableItem {
152
+ emit: { failure: SelectableItemLoadFailed, success: SelectableItemLoaded }
153
+ filter: (or ["=", ?value, ""] ["=", (object/get @entity ?field), ?value])
154
+ })
155
+ ;; No filter carried over on REFETCH_PAGE, mirroring std-browse exactly.
156
+ ;; Paging is safe here: this atom's selection is a single transient id
157
+ ;; carried only in the SELECT/CONFIRM_SELECTION payload (`?id`), never
158
+ ;; accumulated into a persisted @entity array, so turning a page cannot
159
+ ;; strand an "already selected" item — there is no multi-item selection
160
+ ;; state to strand. A future multi-select variant that DOES accumulate
161
+ ;; picks into an @entity field would need to carry that array across
162
+ ;; REFETCH_PAGE explicitly; this single-pick atom does not need to.
163
+ REFETCH_PAGE -> idle
164
+ (fetch SelectableItem {
165
+ emit: { failure: SelectableItemLoadFailed, success: SelectableItemLoaded }
166
+ limit: @config.pageSize
167
+ offset: ["*", ["-", ?page, 1], @config.pageSize]
168
+ })
169
+ SelectableItemLoaded -> idle
170
+ (render-ui main {
171
+ children: [{
172
+ align: center
173
+ children: [{ name: check-square, type: icon }, {
174
+ content: SelectableItems
175
+ type: typography
176
+ variant: h2
177
+ }]
178
+ direction: horizontal
179
+ gap: sm
180
+ type: stack
181
+ }, (if @config.bodySearch {
182
+ children: [{
183
+ className: "w-full max-w-md"
184
+ clearable: true
185
+ event: REFETCH_QUERY
186
+ placeholder: @config.searchPlaceholder
187
+ type: search-input
188
+ }]
189
+ direction: horizontal
190
+ gap: sm
191
+ type: stack
192
+ } {
193
+ children: []
194
+ gap: none
195
+ type: stack
196
+ }), (if [">", (array/len @config.filters), 0] {
197
+ children: [{
198
+ entity: SelectableItem
199
+ event: REFETCH_FILTER
200
+ filters: @config.filters
201
+ look: @config.filterBarLook
202
+ type: filter-group
203
+ }]
204
+ direction: horizontal
205
+ gap: sm
206
+ type: stack
207
+ } {
208
+ children: []
209
+ gap: none
210
+ type: stack
211
+ }), { type: divider }, {
212
+ color: muted
213
+ content: "Choose a selectableitem to continue."
214
+ type: typography
215
+ variant: caption
216
+ }, {
217
+ className: "transition-shadow hover:shadow-md cursor-pointer"
218
+ entity: ?data
219
+ fields: []
220
+ itemActions: [{
221
+ event: SELECT
222
+ icon: check
223
+ label: Select
224
+ }]
225
+ look: @config.tableLook
226
+ pageSize: @config.displayPageSize
227
+ renderItem: (fn item {
228
+ align: center
229
+ children: [{ label: @item.name, type: checkbox }]
230
+ direction: horizontal
231
+ gap: sm
232
+ type: stack
233
+ })
234
+ type: data-grid
235
+ }]
236
+ direction: vertical
237
+ gap: lg
238
+ type: stack
239
+ })
108
240
  SELECT -> selecting
109
241
  (render-ui main {
110
242
  children: [{
@@ -269,6 +401,25 @@ orbital SelectableItemOrbital {
269
401
  @description "Indicates a selectable item failed to load."
270
402
  @synonyms "error, failure, problem, load error"
271
403
  @tier "internal"
404
+ REFETCH_QUERY {
405
+ searchTerm : string!
406
+ }
407
+ @description "Triggers a pick-list refresh based on the current search term."
408
+ @synonyms "refresh, reload, update, re-query, search"
409
+ @tier "domain"
410
+ REFETCH_FILTER {
411
+ field : string!
412
+ value : string!
413
+ }
414
+ @description "Triggers a pick-list refresh based on current filter criteria."
415
+ @synonyms "refresh, filter, update, reload"
416
+ @tier "domain"
417
+ REFETCH_PAGE {
418
+ page : number!
419
+ }
420
+ @description "Triggers a pick-list page refresh."
421
+ @synonyms "reload, refresh, update, paginate"
422
+ @tier "domain"
272
423
  }
273
424
 
274
425
  config {
@@ -276,6 +427,36 @@ orbital SelectableItemOrbital {
276
427
  @label "Table look"
277
428
  @description "Layer 2 visual treatment for the data table rendered by this atom."
278
429
  @tier "presentation"
430
+ bodySearch : boolean = false
431
+ @label "Show the pick-list's own search box?"
432
+ @description "Embeds a search box above the pick-list. 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)."
433
+ @synonyms "inline search, list search, built-in search, show search"
434
+ @tier "presentation"
435
+ searchPlaceholder : string = "Search…"
436
+ @label "Search box placeholder text"
437
+ @description "Hint text inside the pick-list search box."
438
+ @synonyms "search hint, placeholder, search box text"
439
+ @tier "presentation"
440
+ filters : [FilterSpec] = []
441
+ @label "Which filters should appear above the pick-list?"
442
+ @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 pick-list with no page-level filter affordance."
443
+ @synonyms "filter bar, faceted filters, field filters, dropdowns"
444
+ @tier "presentation"
445
+ filterBarLook : "toolbar" | "chips" | "pills" | "popover-trigger" | "inline-column-header" = toolbar
446
+ @label "Filter bar look"
447
+ @description "Visual treatment for the embedded filter bar, mirroring std-filter's own enum. Only applies when `filters` is non-empty."
448
+ @synonyms "filter bar style, facet style, chips, pills"
449
+ @tier "presentation"
450
+ pageSize : number = 20
451
+ @label "How many candidates per page when paging?"
452
+ @description "Records fetched from the server per REFETCH_PAGE request. The initial load and search/filter refetches remain unpaginated (load every candidate) unless a consumer wires REFETCH_PAGE, so the default pick-list surface is unchanged. Safe to page: this atom's selection is a single transient id (never an accumulated array), so no already-picked item can be stranded on another page."
453
+ @synonyms "fetch limit, records per page, page limit"
454
+ @tier "presentation"
455
+ displayPageSize : number = 20
456
+ @label "How many candidates to show at once?"
457
+ @description "Rows visible per page before the data-grid's own pagination control appears — 20 matches the pick-list density sweet spot (Miller's law, 10-25 rows)."
458
+ @synonyms "visible rows, items per page, display limit, show per page"
459
+ @tier "presentation"
279
460
  }
280
461
  }
281
462
 
@@ -1,5 +1,6 @@
1
1
  app std-tree "1.0.0"
2
- "std-tree — generic hierarchical node browser. Fetches a flat TreeNode collection and renders it through file-tree's flat `items` mode (parentId-nested at render time by the component, not the .lolo); OPEN_NODE is the external handoff for a selected node's id. Rebindable: pass `fields` at the call site to retarget it at any parent-pointer entity (categories, org units, comment threads)."
2
+ "std-tree — generic hierarchical node browser. Fetches a flat TreeNode collection and renders it through file-tree's flat `items` mode (parentId-nested at render time by the component, not the .lolo); OPEN_NODE is the external handoff for a selected node's id. Rebindable: pass `fields` at the call site to retarget it at any parent-pointer entity (categories, org units, comment threads). Search/filter are wired (REFETCH_QUERY/REFETCH_FILTER re-fetch the flat node set server-side); pagination is deliberately NOT offered — a hierarchy is not a paged list, and paging a flat node set would sever children from parents that fell on another page."
3
+ type FilterSpec = { field : string!, label : string, options : [string] }
3
4
  orbital TreeNodeOrbital {
4
5
  type TreeNodeLoaded = Event { data : [TreeNode] } "Fired when the tree node collection finishes loading"
5
6
 
@@ -51,7 +52,37 @@ orbital TreeNodeOrbital {
51
52
  direction: horizontal
52
53
  gap: sm
53
54
  type: stack
54
- }, { type: divider }, {
55
+ }, { type: divider }, (if @config.bodySearch {
56
+ children: [{
57
+ className: "w-full max-w-md"
58
+ clearable: true
59
+ event: REFETCH_QUERY
60
+ placeholder: @config.searchPlaceholder
61
+ type: search-input
62
+ }]
63
+ direction: horizontal
64
+ gap: sm
65
+ type: stack
66
+ } {
67
+ children: []
68
+ gap: none
69
+ type: stack
70
+ }), (if [">", (array/len @config.filters), 0] {
71
+ children: [{
72
+ entity: TreeNode
73
+ event: REFETCH_FILTER
74
+ filters: @config.filters
75
+ look: @config.filterBarLook
76
+ type: filter-group
77
+ }]
78
+ direction: horizontal
79
+ gap: sm
80
+ type: stack
81
+ } {
82
+ children: []
83
+ gap: none
84
+ type: stack
85
+ }), {
55
86
  items: (array/map ?data (fn row {
56
87
  icon: @row.icon
57
88
  id: @row.id
@@ -113,6 +144,22 @@ orbital TreeNodeOrbital {
113
144
  gap: md
114
145
  type: stack
115
146
  })
147
+ ;; REFETCH_* stays in `browsing` and DOES NOT replace the tree with a
148
+ ;; spinner — search-as-you-type would otherwise unmount file-tree every
149
+ ;; keystroke and flicker. The fetch fires in the background; when
150
+ ;; TreeNodeLoaded arrives (handler below) React reconciles the tree in
151
+ ;; place. No REFETCH_PAGE: a hierarchy is not a paged list — paging the
152
+ ;; flat node set would sever children from parents on another page.
153
+ REFETCH_QUERY -> browsing
154
+ (fetch TreeNode {
155
+ emit: { failure: TreeNodeLoadFailed, success: TreeNodeLoaded }
156
+ filter: (or ["=", ?searchTerm, ""] (str/includes (object/get @entity name) ?searchTerm))
157
+ })
158
+ REFETCH_FILTER -> browsing
159
+ (fetch TreeNode {
160
+ emit: { failure: TreeNodeLoadFailed, success: TreeNodeLoaded }
161
+ filter: (or ["=", ?value, ""] ["=", (object/get @entity ?field), ?value])
162
+ })
116
163
  OPEN_NODE -> browsing
117
164
  (set @entity.id ?id)
118
165
  TreeNodeLoaded -> browsing
@@ -132,7 +179,37 @@ orbital TreeNodeOrbital {
132
179
  direction: horizontal
133
180
  gap: sm
134
181
  type: stack
135
- }, { type: divider }, {
182
+ }, { type: divider }, (if @config.bodySearch {
183
+ children: [{
184
+ className: "w-full max-w-md"
185
+ clearable: true
186
+ event: REFETCH_QUERY
187
+ placeholder: @config.searchPlaceholder
188
+ type: search-input
189
+ }]
190
+ direction: horizontal
191
+ gap: sm
192
+ type: stack
193
+ } {
194
+ children: []
195
+ gap: none
196
+ type: stack
197
+ }), (if [">", (array/len @config.filters), 0] {
198
+ children: [{
199
+ entity: TreeNode
200
+ event: REFETCH_FILTER
201
+ filters: @config.filters
202
+ look: @config.filterBarLook
203
+ type: filter-group
204
+ }]
205
+ direction: horizontal
206
+ gap: sm
207
+ type: stack
208
+ } {
209
+ children: []
210
+ gap: none
211
+ type: stack
212
+ }), {
136
213
  items: (array/map ?data (fn row {
137
214
  icon: @row.icon
138
215
  id: @row.id
@@ -214,6 +291,19 @@ orbital TreeNodeOrbital {
214
291
  }
215
292
 
216
293
  listens {
294
+ REFETCH_QUERY {
295
+ searchTerm : string!
296
+ }
297
+ @description "Triggers a tree refresh based on the current search query."
298
+ @synonyms "refresh, reload, update, re-query"
299
+ @tier "domain"
300
+ REFETCH_FILTER {
301
+ field : string!
302
+ value : string!
303
+ }
304
+ @description "Triggers a tree refresh based on current filter criteria."
305
+ @synonyms "refresh, filter, update, reload"
306
+ @tier "domain"
217
307
  OPEN_NODE {
218
308
  id : string!
219
309
  }
@@ -240,6 +330,26 @@ orbital TreeNodeOrbital {
240
330
  @label "Section title"
241
331
  @description "Heading shown above the tree"
242
332
  @tier "presentation"
333
+ bodySearch : boolean = false
334
+ @label "Show the tree's own search box?"
335
+ @description "Embeds a search box above the tree that re-fetches the flat node set server-side. Set false (the default) when the page already provides a search affordance (a toolbar Search molecule), so the surface shows one search box instead of two."
336
+ @synonyms "inline search, list search, built-in search, show search, duplicate search"
337
+ @tier "presentation"
338
+ searchPlaceholder : string = "Search…"
339
+ @label "Search box placeholder text"
340
+ @description "Hint text inside the tree search box (e.g. 'Search nodes…')."
341
+ @synonyms "search hint, placeholder, search box text, search label"
342
+ @tier "presentation"
343
+ filters : [FilterSpec] = []
344
+ @label "Which filters should appear above the tree?"
345
+ @description "Dropdown filter facets by field, rendered as an embedded filter bar above the tree; each selection fires REFETCH_FILTER on this trait, re-fetching the flat node set. Empty (the default) = no filter bar, which is the right setting whenever the page already composes std-filter beside this tree and routes `XFilter.FILTER -> REFETCH_FILTER` — that is the wired owner and remains the primary path."
346
+ @synonyms "filter bar, faceted filters, field filters, dropdowns"
347
+ @tier "presentation"
348
+ filterBarLook : "toolbar" | "chips" | "pills" | "popover-trigger" | "inline-column-header" = toolbar
349
+ @label "Filter bar look"
350
+ @description "Visual treatment for the embedded filter bar, mirroring std-filter's own enum. Only applies when `filters` is non-empty."
351
+ @synonyms "filter bar style, facet style, chips, pills"
352
+ @tier "presentation"
243
353
  }
244
354
  }
245
355
 
@@ -0,0 +1,93 @@
1
+ app std-area-fill "1.0.0"
2
+ "Headless area-fill mechanic: the player paints area under a curve by moving through a fill region. Listens BODY_MOVED and, per fill, accumulates height(x) × |dx| while the player is inside the fill's x-range (the per-tick step is clamped to 0.5 world units — the physical per-tick maximum — so teleports and respawns paint nothing) — height is linear interpolation of the config curve samples above the fill's baseline (the platformer body's leftSamp/rightSamp idiom). rateMode picks the rate: \"area\" (height × |dx|, default — fills fastest where the curve is tallest), \"speed\" (|dx| only, flat rate), or \"ride\" (|dx| only while the move reports skating — walking the same distance paints nothing). When a fill's filled reaches its target it flips reported and emits AREA_FILLED once (the shared entity carries the rest); each fill also stamps a frontier (x0 + span × min(1, filled/target)) so boards can shade the painted region left-to-right as the gauge grows. RESTART resets every fill. Renders nothing; boards shade the painted region and read filled/frontier from the shared entity (the render authority)."
3
+ orbital AreaFillOrbital {
4
+ type AreaFillSample = {
5
+ x : number!
6
+ y : number!
7
+ }
8
+ type AreaFill = {
9
+ id : string!
10
+ x0 : number!
11
+ x1 : number!
12
+ baseline : number!
13
+ target : number!
14
+ rateMode : string
15
+ hint : string
16
+ samples : [AreaFillSample]!
17
+ filled : number
18
+ frontier : number
19
+ reported : boolean
20
+ }
21
+
22
+ entity AreaFillData [runtime, shared] {
23
+ id : string!
24
+ fills : [AreaFill] = []
25
+ lastX : number = 0
26
+ }
27
+
28
+ trait AreaFill -> @rebindable AreaFillData [lifecycle] {
29
+ initial: active
30
+
31
+ state active {
32
+ INIT -> active
33
+ (set @entity.fills (array/map @config.fills (fn f (object/merge @f { filled: 0, frontier: (object/get @f x0), reported: false }))))
34
+ (set @entity.lastX 0)
35
+
36
+ BODY_MOVED -> active when @config.running
37
+ (let ((adx (math/abs (- @payload.x @entity.lastX))))
38
+ (set @entity.fills (array/map @entity.fills (fn f
39
+ (let ((inside (and (>= @payload.x (object/get @f x0)) (<= @payload.x (object/get @f x1)))))
40
+ (let ((leftSamp (array/last (array/filter (object/get @f samples) (fn s (<= (object/get @s x) @payload.x))))))
41
+ (let ((rightSamp (array/first (array/filter (object/get @f samples) (fn s (> (object/get @s x) @payload.x))))))
42
+ (let ((h (if (or (== @leftSamp null) (== @rightSamp null)) 0
43
+ (- (+ (object/get @leftSamp y) (* (/ (- (object/get @rightSamp y) (object/get @leftSamp y)) (- (object/get @rightSamp x) (object/get @leftSamp x))) (- @payload.x (object/get @leftSamp x)))) (object/get @f baseline)))))
44
+ (let ((step (math/min @adx 0.5)))
45
+ (let ((rate (if @inside
46
+ (if (== (object/get @f rateMode) "ride") (if @payload.skating @step 0)
47
+ (if (== (object/get @f rateMode) "speed") @step
48
+ (* @step @h)))
49
+ 0)))
50
+ (let ((newFilled (+ (object/get @f filled) @rate)))
51
+ (object/merge @f { filled: @newFilled, frontier: (+ (object/get @f x0) (* (- (object/get @f x1) (object/get @f x0)) (math/min 1 (/ @newFilled (object/get @f target))))) }))))))))))))
52
+ (set @entity.lastX @payload.x)
53
+ (let ((newly (array/filter @entity.fills (fn f (and (>= (object/get @f filled) (object/get @f target)) (== (object/get @f reported) false))))))
54
+ (if (> (array/len @newly) 0)
55
+ (do
56
+ (set @entity.fills (array/map @entity.fills (fn f (if (== (object/get @f id) (object/get (array/first @newly) id)) (object/merge @f { reported: true }) @f))))
57
+ (emit AREA_FILLED { id: (object/get (array/first @newly) id), x: @payload.x, y: @payload.y }))
58
+ null))
59
+
60
+ RESTART -> active
61
+ (set @entity.fills (array/map @config.fills (fn f (object/merge @f { filled: 0, frontier: (object/get @f x0), reported: false }))))
62
+ (set @entity.lastX 0)
63
+ }
64
+
65
+ emits {
66
+ AREA_FILLED -> external {
67
+ id : string!
68
+ x : number!
69
+ y : number!
70
+ }
71
+ @description "A fill's painted area reached its target (x, y = the player's position at completion, for burst placement). When several fills complete on one move, the first reports and the shared entity carries the rest."
72
+ @tier "domain"
73
+ }
74
+
75
+ listens {
76
+ BODY_MOVED { x : number!, y : number!, vx : number!, vy : number!, grounded : boolean!, skating : boolean!, speed : number! }
77
+ RESTART { id : string }
78
+ }
79
+
80
+ config {
81
+ running : boolean = true
82
+ @label "Running"
83
+ @description "When true, BODY_MOVED paints every fill the player moves through; toggle to freeze every fill as-is."
84
+ @tier "domain"
85
+ fills : [AreaFill] = []
86
+ @label "Fills"
87
+ @description "One row per fill region: id, x-range (x0..x1 in world units), baseline the height is measured from, target area, rateMode (area | speed | ride — default area), optional hint, and the curve samples to interpolate. filled/frontier/reported are stamped at runtime."
88
+ @tier "domain"
89
+ }
90
+ }
91
+
92
+ page "/area-fill" as AreaFillPage -> AreaFill
93
+ }
@@ -0,0 +1,79 @@
1
+ app std-loop-lock "1.0.0"
2
+ "Headless winding-lock mechanic: locks that open only after the player travels a full revolution around their anchor. Listens BODY_MOVED and, while the player stays within a radial band of a lock's ring, accumulates the wrapped angular delta of their position into swept (radians, signed — either direction counts); when |swept| reaches turns × 2π the lock flips unlocked and emits LOOP_UNLOCKED once. Entering the band reseeds the angle so leaving and returning never jumps the sweep; progress is kept across exits, so a revolution can be completed in stages. RESTART re-locks every lock. Renders nothing; boards draw the ring/chest and read swept from the shared entity (the render authority)."
3
+ orbital LoopLockOrbital {
4
+ type LoopLock = {
5
+ id : string!
6
+ x : number!
7
+ y : number!
8
+ radius : number!
9
+ band : number!
10
+ turns : number!
11
+ swept : number
12
+ lastAngle : number
13
+ inside : boolean
14
+ unlocked : boolean
15
+ reported : boolean
16
+ }
17
+
18
+ entity LoopLockData [runtime, shared] {
19
+ id : string!
20
+ locks : [LoopLock] = []
21
+ }
22
+
23
+ trait LoopLock -> @rebindable LoopLockData [lifecycle] {
24
+ initial: active
25
+
26
+ state active {
27
+ INIT -> active
28
+ (set @entity.locks (array/map @config.locks (fn l (object/merge @l { swept: 0, lastAngle: 0, inside: false, unlocked: false, reported: false }))))
29
+
30
+ BODY_MOVED -> active when @config.running
31
+ (set @entity.locks (array/map @entity.locks (fn l
32
+ (let ((dx (- @payload.x (object/get @l x))) (dy (- @payload.y (object/get @l y))))
33
+ (let ((dist (math/hypot @dx @dy)) (ang (math/atan2 @dy @dx)))
34
+ (let ((in (<= (math/abs (- @dist (object/get @l radius))) (object/get @l band))))
35
+ (let ((d (if (and @in (== (object/get @l inside) true))
36
+ (math/atan2 (math/sin (- @ang (object/get @l lastAngle))) (math/cos (- @ang (object/get @l lastAngle))))
37
+ 0)))
38
+ (let ((swept (+ (object/get @l swept) @d)))
39
+ (object/merge @l { inside: @in, lastAngle: (if @in @ang (object/get @l lastAngle)), swept: @swept, unlocked: (or (object/get @l unlocked) (>= (math/abs @swept) (* (object/get @l turns) 6.283185307179586))) })))))))))
40
+ (let ((newly (array/filter @entity.locks (fn l (and (== (object/get @l unlocked) true) (== (object/get @l reported) false))))))
41
+ (if (> (array/len @newly) 0)
42
+ (do
43
+ (set @entity.locks (array/map @entity.locks (fn l (if (== (object/get @l id) (object/get (array/first @newly) id)) (object/merge @l { reported: true }) @l))))
44
+ (emit LOOP_UNLOCKED { id: (object/get (array/first @newly) id), x: (object/get (array/first @newly) x), y: (object/get (array/first @newly) y) }))
45
+ null))
46
+
47
+ RESTART -> active
48
+ (set @entity.locks (array/map @config.locks (fn l (object/merge @l { swept: 0, lastAngle: 0, inside: false, unlocked: false, reported: false }))))
49
+ }
50
+
51
+ emits {
52
+ LOOP_UNLOCKED -> external {
53
+ id : string!
54
+ x : number!
55
+ y : number!
56
+ }
57
+ @description "A lock's angular sweep reached turns × 2π and it flipped unlocked; boards layer treasure fx and rewards. When several locks open on one move, the first reports and the shared entity carries the rest."
58
+ @tier "domain"
59
+ }
60
+
61
+ listens {
62
+ BODY_MOVED { x : number!, y : number!, vx : number!, vy : number!, grounded : boolean!, skating : boolean!, speed : number! }
63
+ RESTART { id : string }
64
+ }
65
+
66
+ config {
67
+ running : boolean = true
68
+ @label "Running"
69
+ @description "When true, BODY_MOVED accumulates angular sweep and unlocks locks; toggle to freeze every lock as-is."
70
+ @tier "domain"
71
+ locks : [LoopLock] = []
72
+ @label "Locks"
73
+ @description "One row per winding lock: id, anchor (x, y in world units), ring radius, radial tolerance band, and revolutions required. swept/inside/unlocked are stamped at runtime."
74
+ @tier "domain"
75
+ }
76
+ }
77
+
78
+ page "/loop-lock" as LoopLockPage -> LoopLock
79
+ }
@@ -0,0 +1,93 @@
1
+ app std-pad-lock "1.0.0"
2
+ "Headless pad-lock mechanic: floor pads that light when the player steps on them, completing a set when every pad is lit. Listens BODY_MOVED and lights any unlit live pad the player's rect touches (|dx| <= reachX, |dy| <= reachY of the pad center); a lit pad stays lit. Pads may carry a group number (grp 0 = ungrouped, always live): when groups are used, only pads of the lowest uncompleted group are live — pads of later groups refuse to light until the current group finishes, so one sum must be finished before the next. When a group completes the trait emits PAD_GROUP_COMPLETED once for it (first newly completed group reports; the shared entity carries the rest); when every pad in the set is lit it stamps completed and emits PAD_SET_COMPLETED once. RESTART unlights every pad and resets the set. Renders nothing; boards draw the pads and read lit from the shared entity (the render authority)."
3
+ orbital PadLockOrbital {
4
+ type PadLock = {
5
+ id : string!
6
+ x : number!
7
+ y : number!
8
+ grp : number!
9
+ lit : boolean
10
+ }
11
+
12
+ entity PadLockData [runtime, shared] {
13
+ id : string!
14
+ pads : [PadLock] = []
15
+ groupsDone : [number] = []
16
+ completed : boolean = false
17
+ }
18
+
19
+ trait PadLock -> @rebindable PadLockData [lifecycle] {
20
+ initial: active
21
+
22
+ state active {
23
+ INIT -> active
24
+ (set @entity.pads (array/map @config.pads (fn p (object/merge @p { lit: false }))))
25
+ (set @entity.groupsDone [])
26
+ (set @entity.completed false)
27
+
28
+ BODY_MOVED -> active when @config.running
29
+ (let ((grpIds (array/reduce (array/map (array/filter @entity.pads (fn p (> (object/get @p grp) 0))) (fn p (object/get @p grp))) [] (fn acc g (if (> (array/len (array/filter @acc (fn x (== @x @g)))) 0) @acc (array/append @acc @g))))))
30
+ (let ((openGrps (array/filter @grpIds (fn g (< (array/len (array/filter @entity.pads (fn p (and (== (object/get @p grp) @g) (== (object/get @p lit) true))))) (array/len (array/filter @entity.pads (fn p (== (object/get @p grp) @g)))))))))
31
+ (let ((currentGrp (array/reduce @openGrps 9999 (fn acc g (math/min @acc @g)))))
32
+ (let ((newPads (array/map @entity.pads (fn p
33
+ (object/merge @p { lit: (or (object/get @p lit) (and (or (== (object/get @p grp) 0) (== (object/get @p grp) @currentGrp)) (and (<= (math/abs (- @payload.x (object/get @p x))) @config.reachX) (<= (math/abs (- @payload.y (object/get @p y))) @config.reachY)))) })))))
34
+ (let ((doneNow (array/filter @grpIds (fn g (== (array/len (array/filter @newPads (fn p (and (== (object/get @p grp) @g) (== (object/get @p lit) true))))) (array/len (array/filter @newPads (fn p (== (object/get @p grp) @g)))))))))
35
+ (let ((newlyGrp (array/filter @doneNow (fn g (== (array/len (array/filter @entity.groupsDone (fn d (== @d @g)))) 0)))))
36
+ (do
37
+ (set @entity.pads @newPads)
38
+ (if (> (array/len @newlyGrp) 0)
39
+ (do
40
+ (set @entity.groupsDone @doneNow)
41
+ (emit PAD_GROUP_COMPLETED { grp: (array/first @newlyGrp) }))
42
+ null)
43
+ (if (and (== @entity.completed false) (== (array/len (array/filter @newPads (fn p (== (object/get @p lit) false)))) 0))
44
+ (do
45
+ (set @entity.completed true)
46
+ (emit PAD_SET_COMPLETED {}))
47
+ null))))))))
48
+
49
+ RESTART -> active
50
+ (set @entity.pads (array/map @config.pads (fn p (object/merge @p { lit: false }))))
51
+ (set @entity.groupsDone [])
52
+ (set @entity.completed false)
53
+ }
54
+
55
+ emits {
56
+ PAD_GROUP_COMPLETED -> external {
57
+ grp : number!
58
+ }
59
+ @description "Every pad of one group is lit; fires once per group, in group order — later groups stay dead until the lowest uncompleted group finishes. Boards layer a small fx and unlock the next group's pads."
60
+ @tier "domain"
61
+ PAD_SET_COMPLETED -> external {
62
+ }
63
+ @description "Every pad in the set is lit; fires once per run. Boards layer the treasure fx and spill the bonus reward."
64
+ @tier "domain"
65
+ }
66
+
67
+ listens {
68
+ BODY_MOVED { x : number!, y : number!, vx : number!, vy : number!, grounded : boolean!, skating : boolean!, speed : number! }
69
+ RESTART { id : string }
70
+ }
71
+
72
+ config {
73
+ running : boolean = true
74
+ @label "Running"
75
+ @description "When true, BODY_MOVED lights touched live pads and reports group/set completion; toggle to freeze every pad as-is."
76
+ @tier "domain"
77
+ pads : [PadLock] = []
78
+ @label "Pads"
79
+ @description "One row per floor pad: id, center (x, y in world units), and grp (0 = ungrouped, always live; N = live only while it is the lowest uncompleted group). lit is stamped at runtime."
80
+ @tier "domain"
81
+ reachX : number = 0.7
82
+ @label "Reach X"
83
+ @description "Horizontal tolerance (world units) around a pad's center when testing player contact (approximates the body's half-width plus a step)."
84
+ @tier "domain"
85
+ reachY : number = 1.2
86
+ @label "Reach Y"
87
+ @description "Vertical tolerance (world units) around a pad's center when testing player contact (covers skating over the surface the pad sits on)."
88
+ @tier "domain"
89
+ }
90
+ }
91
+
92
+ page "/pad-lock" as PadLockPage -> PadLock
93
+ }