@almadar/std 16.218.0 → 16.219.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 (75) hide show
  1. package/behaviors/lolo/infra/atoms/std-data-import.lolo +56 -5
  2. package/behaviors/lolo/infra/atoms/std-notification-center.lolo +18 -18
  3. package/behaviors/lolo/infra/atoms/std-notification-preferences.lolo +5 -5
  4. package/behaviors/lolo/infra/atoms/std-notify-on-event.lolo +17 -18
  5. package/behaviors/lolo/ui/core/atoms/std-gantt-schedule.lolo +215 -0
  6. package/behaviors/lolo/ui/core/atoms/ui-textarea.lolo +2 -2
  7. package/behaviors/lolo/ui/core/molecules/ui-gantt.lolo +169 -0
  8. package/behaviors/lolo/ui/game/atoms/ui-canvas-2d.lolo +2 -2
  9. package/behaviors/lolo/ui/game/atoms/ui-canvas.lolo +2 -2
  10. package/behaviors/lolo/ui/learning/atoms/ui-math-canvas.lolo +2 -2
  11. package/behaviors/registry/infra/atoms/std-data-import.orb +329 -6
  12. package/behaviors/registry/infra/atoms/std-notification-center.orb +41 -33
  13. package/behaviors/registry/infra/atoms/std-notification-preferences.orb +10 -17
  14. package/behaviors/registry/infra/atoms/std-notify-on-event.orb +198 -195
  15. package/behaviors/registry/ui/core/atoms/std-gantt-schedule.orb +723 -0
  16. package/behaviors/registry/ui/core/molecules/ui-gantt.orb +508 -0
  17. package/canonical-operators.json +46 -1
  18. package/dist/behaviors/exports-reader.js +3212 -2818
  19. package/dist/behaviors/functions/index.d.ts +238 -5
  20. package/dist/behaviors/functions/index.js +3207 -2819
  21. package/dist/behaviors/index.d.ts +1 -1
  22. package/dist/behaviors/index.js +3213 -2819
  23. package/dist/behaviors/query.js +3212 -2818
  24. package/dist/behaviors-embeddings.hash.json +3 -3
  25. package/dist/behaviors-embeddings.json +19 -7
  26. package/dist/behaviors-registry.json +1772 -127
  27. package/dist/index.d.ts +3 -3
  28. package/dist/index.js +3249 -2819
  29. package/dist/knob-embeddings.hash.json +3 -3
  30. package/dist/knob-embeddings.json +1 -1
  31. package/dist/modules/anim.d.ts +1 -1
  32. package/dist/modules/array.d.ts +1 -1
  33. package/dist/modules/async.d.ts +1 -1
  34. package/dist/modules/behavior.d.ts +1 -1
  35. package/dist/modules/browser.d.ts +1 -1
  36. package/dist/modules/composition.d.ts +1 -1
  37. package/dist/modules/contract.d.ts +1 -1
  38. package/dist/modules/core.d.ts +1 -1
  39. package/dist/modules/data.d.ts +1 -1
  40. package/dist/modules/ease.d.ts +1 -1
  41. package/dist/modules/format.d.ts +1 -1
  42. package/dist/modules/geo.d.ts +1 -1
  43. package/dist/modules/graph.d.ts +1 -1
  44. package/dist/modules/grid.d.ts +1 -1
  45. package/dist/modules/index.d.ts +2 -1
  46. package/dist/modules/index.js +30 -1
  47. package/dist/modules/integration.d.ts +1 -1
  48. package/dist/modules/json.d.ts +20 -0
  49. package/dist/modules/json.js +30 -0
  50. package/dist/modules/llm.d.ts +1 -1
  51. package/dist/modules/math.d.ts +1 -1
  52. package/dist/modules/memory.d.ts +1 -1
  53. package/dist/modules/nn.d.ts +1 -1
  54. package/dist/modules/noise.d.ts +1 -1
  55. package/dist/modules/object.d.ts +1 -1
  56. package/dist/modules/os.d.ts +1 -1
  57. package/dist/modules/path.d.ts +1 -1
  58. package/dist/modules/prob.d.ts +1 -1
  59. package/dist/modules/session.d.ts +1 -1
  60. package/dist/modules/str.d.ts +1 -1
  61. package/dist/modules/tensor.d.ts +1 -1
  62. package/dist/modules/time.d.ts +1 -1
  63. package/dist/modules/trace.d.ts +1 -1
  64. package/dist/modules/train.d.ts +1 -1
  65. package/dist/modules/validate.d.ts +1 -1
  66. package/dist/modules/vector.d.ts +1 -1
  67. package/dist/modules/workspace.d.ts +1 -1
  68. package/dist/registry/factory-signatures.json +1 -1
  69. package/dist/registry.d.ts +1 -1
  70. package/dist/registry.js +28 -0
  71. package/dist/{types-C-Yerb4-.d.ts → types-DpbKBJu8.d.ts} +3 -3
  72. package/i18n/ar.json +3 -1
  73. package/i18n/en.json +2 -0
  74. package/i18n/sl.json +3 -1
  75. package/package.json +2 -2
@@ -1,5 +1,5 @@
1
1
  app std-data-import "1.0.0"
2
- "std-data-import — staged CSV import run, source-agnostic: upload → staged review → confirm → handoff. UPLOAD_CSV opens a run and persists an ImportBatch row (status uploaded, fieldMappingJson recorded verbatim for provenance); the host's file UI parses the CSV and answers with STAGE_ROWS, one JSON string per raw row, which the atom bulk-persists into import_rows against the batch id and presents as a staged review (row/error counts + Confirm/Discard). CONFIRM_IMPORT marks the batch confirmed and emits IMPORT_CONFIRMED { batchId, sourceName, rowCount } — the mapping+persist half is the CONSUMER organism's: it listens for IMPORT_CONFIRMED, fetches import_rows where batchId matches, maps each rowJson through its own field mapping, and persists into its own target entity (cross-atom persist into a consumer's entity is not expressible from here). This is deliberately the CSV-file path: the atom never calls a source service — sourceName/fieldMappingJson/targetEntity/historyWindowDays are declarative consumer configs, and any persist failure marks the batch row failed and fires IMPORT_FAILED, never a silent swallow. Compose with std-import for the file-picker menu."
2
+ "std-data-import — staged CSV import run, source-agnostic: upload → staged review → confirm → parse+map → handoff. UPLOAD_CSV opens a run and persists an ImportBatch row (status uploaded, fieldMappingJson recorded verbatim for provenance); the host's file UI parses the CSV and answers with STAGE_ROWS, one JSON string per raw row, which the atom bulk-persists into import_rows against the batch id and presents as a staged review (row/error counts + Confirm/Discard). CONFIRM_IMPORT marks the batch confirmed, emits IMPORT_CONFIRMED { batchId, sourceName, rowCount }, and fetches the batch's staged rows; on fetch success the atom parses each rowJson via json/parse, remaps keys through the batch's mappingJson, and emits IMPORT_ROWS_PARSED { batchId, rows, failed } — the persist half is the CONSUMER organism's: it listens for IMPORT_ROWS_PARSED and persists the mapped rows into its own target entity (cross-atom persist into a consumer's entity is not expressible from here). This is deliberately the CSV-file path: the atom never calls a source service — sourceName/fieldMappingJson/targetEntity/historyWindowDays are declarative consumer configs, and any persist failure marks the batch row failed and fires IMPORT_FAILED, never a silent swallow. Compose with std-import for the file-picker menu."
3
3
  orbital DataImportOrbital {
4
4
 
5
5
  entity ImportBatch [persistent: import_batches] {
@@ -8,7 +8,7 @@ orbital DataImportOrbital {
8
8
  status : "uploaded" | "staged" | "confirmed" | "failed" @description "Lifecycle position of the import run." = uploaded
9
9
  rowCount : number @description "Number of rows staged for review." @synonyms "rows, staged count, record count" = 0
10
10
  errorCount : number @description "Number of rows the host parser flagged with errors during staging." @synonyms "errors, failed rows, parse errors" = 0
11
- mappingJson : string @description "Source-column → target-field mapping (JSON string), recorded verbatim from config for provenance; the consumer's mapping layer applies it." @synonyms "field mapping, column mapping, mapping" = "{}"
11
+ mappingJson : string @description "Source-column → target-field mapping (JSON string), recorded verbatim from config for provenance; the atom applies it when parsing staged rows on confirm." @synonyms "field mapping, column mapping, mapping" = "{}"
12
12
  createdAt : number @description "Run start timestamp." = 0
13
13
  }
14
14
 
@@ -145,6 +145,9 @@ orbital DataImportOrbital {
145
145
  variant: success
146
146
  })
147
147
  (emit IMPORT_CONFIRMED { batchId: @entity.id, rowCount: @entity.rowCount, sourceName: @entity.sourceName })
148
+ (fetch ImportRow {
149
+ emit: { failure: IMPORT_PERSIST_FAILED, success: IMPORT_ROWS_FETCHED }
150
+ })
148
151
  ;; Same transient-failure self-loop as in `uploaded`.
149
152
  IMPORT_PERSIST_FAILED -> staged
150
153
  (set @entity.status failed)
@@ -166,6 +169,40 @@ orbital DataImportOrbital {
166
169
  }
167
170
 
168
171
  state confirmed {
172
+ ;; The parse half: fetch success delivers every staged row flat (?data);
173
+ ;; filter to this batch, keep rows whose rowJson parses to an object,
174
+ ;; remap keys through the batch's mappingJson, and hand the consumer
175
+ ;; mapped rows + a failed count (failed rows surface, never drop
176
+ ;; silently). json/parse yields null on invalid JSON, so the guard-level
177
+ ;; branch is validate/object on the parse result.
178
+ IMPORT_ROWS_FETCHED -> confirmed
179
+ (emit IMPORT_ROWS_PARSED {
180
+ batchId: @entity.id
181
+ failed: (array/len (array/filter ?data (fn row (and
182
+ ["=", (object/get @row "batchId"), @entity.id]
183
+ (not (validate/object (json/parse (object/get @row "rowJson"))))))))
184
+ rows: (array/map
185
+ (array/filter ?data (fn row (and
186
+ ["=", (object/get @row "batchId"), @entity.id]
187
+ (validate/object (json/parse (object/get @row "rowJson"))))))
188
+ (fn row (object/mapKeys
189
+ (json/parse (object/get @row "rowJson"))
190
+ (fn k (if (object/has (json/parse @entity.mappingJson) @k)
191
+ (object/get (json/parse @entity.mappingJson) @k)
192
+ @k)))))
193
+ })
194
+ ;; Same transient-failure self-loop as in `staged` — the row fetch can
195
+ ;; fail after the batch row was already persisted as confirmed.
196
+ IMPORT_PERSIST_FAILED -> confirmed
197
+ (set @entity.status failed)
198
+ (set @entity.errorCount 1)
199
+ (persist update ImportBatch { errorCount: 1, id: @entity.id, status: failed })
200
+ (emit IMPORT_FAILED { batchId: @entity.id, error: ?error })
201
+ (render-ui main {
202
+ message: ?error
203
+ type: alert
204
+ variant: error
205
+ })
169
206
  RESET -> idle
170
207
  (set @entity.status uploaded)
171
208
  (set @entity.rowCount 0)
@@ -174,6 +211,7 @@ orbital DataImportOrbital {
174
211
  ;; Self-loop so the validator sees an in-atom listener for the
175
212
  ;; externally-consumed confirmation event.
176
213
  IMPORT_CONFIRMED -> confirmed
214
+ IMPORT_ROWS_PARSED -> confirmed
177
215
  }
178
216
 
179
217
  state failed {
@@ -206,9 +244,22 @@ orbital DataImportOrbital {
206
244
  sourceName : string
207
245
  rowCount : number
208
246
  }
209
- @description "The user confirmed the staged review and the batch row is persisted as confirmed. Consumers listen, fetch import_rows where batchId matches, map each rowJson through their field mapping, and persist into their own target entity."
247
+ @description "The user confirmed the staged review and the batch row is persisted as confirmed. The atom then fetches the batch's staged rows, parses and key-maps them, and emits IMPORT_ROWS_PARSED for the consumer's persist half."
210
248
  @synonyms "import confirmed, commit import, accept staged rows, approve import"
211
249
  @tier "domain"
250
+ IMPORT_ROWS_FETCHED {
251
+ data : [ImportRow]
252
+ }
253
+ @description "import_rows collection-fetch success on confirm — payload carries every staged row flat; the atom filters to @entity.id by batchId in the IMPORT_ROWS_PARSED arm (self-loop only)"
254
+ @tier "secondary"
255
+ IMPORT_ROWS_PARSED -> external {
256
+ batchId : string!
257
+ failed : number
258
+ rows : [Map string scalar]
259
+ }
260
+ @description "Confirmed rows parsed and key-mapped: each staged row's rowJson was parsed via json/parse and remapped through the batch's mappingJson; failed counts rows whose rowJson did not parse to an object (surfaced, never dropped). The consumer persists rows into its own target entity and marks them imported."
261
+ @synonyms "rows parsed, rows mapped, import rows ready"
262
+ @tier "domain"
212
263
  IMPORT_FAILED -> external {
213
264
  batchId : string
214
265
  error : string!
@@ -233,7 +284,7 @@ orbital DataImportOrbital {
233
284
  @description "Host-side CSV parsing finished: rows carries one JSON string per parsed row (raw source shape, pre-mapping); errorCount counts rows the parser flagged. The atom bulk-persists them into import_rows and opens the staged review."
234
285
  @tier "internal"
235
286
  CONFIRM_IMPORT
236
- @description "The user approved the staged review; the batch is marked confirmed and IMPORT_CONFIRMED fires for the consumer's mapping+persist half."
287
+ @description "The user approved the staged review; the batch is marked confirmed, IMPORT_CONFIRMED fires, and the atom parses+maps the staged rows into IMPORT_ROWS_PARSED for the consumer's persist half."
237
288
  @synonyms "confirm import, approve staged rows, accept import"
238
289
  @tier "domain"
239
290
  DISCARD_IMPORT
@@ -252,7 +303,7 @@ orbital DataImportOrbital {
252
303
  @tier "domain"
253
304
  fieldMappingJson : string = "{}"
254
305
  @label "Field mapping (JSON)"
255
- @description "JSON object mapping source CSV column names to target field names. Stored verbatim on the batch row (mappingJson) so the consumer's mapping layer applies the same mapping when it persists staged rows into the target entity."
306
+ @description "JSON object mapping source CSV column names to target field names. Stored verbatim on the batch row (mappingJson); the atom applies it when it parses staged rows into IMPORT_ROWS_PARSED, so the consumer persists already-mapped rows into the target entity."
256
307
  @synonyms "field mapping, column mapping, map columns, remap fields, csv columns"
257
308
  @tier "domain"
258
309
  targetEntity : string = ""
@@ -9,12 +9,13 @@ orbital NotificationCenterOrbital {
9
9
 
10
10
  entity NotificationPreference [persistent: notificationPreferences] {
11
11
  id : string!
12
- userId : string @description "Owner of this preference row — rows are meant to be private to their owner. Access predicates need an [identity] roster, so the binding organism (which declares one) owns the @read/@update scoping; the generic atom cannot." @synonyms "user, owner, account" = ""
13
- eventType : string @description "Event key this preference applies to (matches a listener's eventKey); blank = all events." @synonyms "event, trigger, subscription, topic" = ""
12
+ personId : string! @description "Owner of this preference row — rows are meant to be private to their owner. Access predicates need an [identity] roster, so the binding organism (which declares one) owns the @read/@update scoping; the generic atom cannot." @synonyms "user, owner, account"
13
+ eventKey : string @description "Event key this preference applies to (matches a listener's eventKey); blank = all events." @synonyms "event, trigger, subscription, topic" = ""
14
+ channel : "in_app" | "email" | "push" | "slack" | "sms" | "webhook" @description "Channel override for this user." @synonyms "preferred channel, delivery method" = in_app
14
15
  mode : "instant" | "digest" | "off" @description "Delivery mode: instant (default), digest (deferred to the drain cycle), or off (muted)." @synonyms "delivery mode, frequency, cadence, mute, defer" = instant
15
- channel : string @description "Channel override for this user (in-app/email/sms/push/webhook); blank = the listener's configured channel." @synonyms "preferred channel, delivery method" = ""
16
- quietStartHour : number @description "Per-user quiet-hours start (hour 0-23); equal to quietEndHour = disabled." @synonyms "quiet start, do not disturb from" = 0
17
- quietEndHour : number @description "Per-user quiet-hours end (hour 0-23); equal to quietStartHour = disabled." @synonyms "quiet end, do not disturb until" = 0
16
+ quietHours : string @description "Per-user quiet-hours window as \"HH:MM-HH:MM\" (e.g. 22:00-07:00); blank = disabled." @synonyms "do not disturb, quiet window, night mode" = ""
17
+ createdAt : number @description "Timestamp indicating when this preference row was created." @synonyms "creation time, created" = 0
18
+ updatedAt : number @description "Timestamp indicating when this preference row was last updated." @synonyms "modified, last updated" = 0
18
19
  }
19
20
 
20
21
  entity PushSubscription [persistent: pushSubscriptions] {
@@ -126,20 +127,21 @@ orbital NotificationCenterOrbital {
126
127
  SAVE_PREFERENCE -> idle
127
128
  (persist create NotificationPreference {
128
129
  channel: ?channel
129
- eventType: ?eventType
130
+ createdAt: @now
131
+ eventKey: ?eventKey
130
132
  mode: ?mode
131
- quietEndHour: ?quietEndHour
132
- quietStartHour: ?quietStartHour
133
- userId: @user.id
133
+ personId: @user.id
134
+ quietHours: ?quietHours
135
+ updatedAt: @now
134
136
  } { emit: { failure: PREFERENCE_SAVE_FAILED, success: PREFERENCE_SAVED } })
135
137
  UPDATE_PREFERENCE -> idle
136
138
  (persist update NotificationPreference {
137
139
  id: ?id
138
140
  channel: ?channel
139
- eventType: ?eventType
141
+ eventKey: ?eventKey
140
142
  mode: ?mode
141
- quietEndHour: ?quietEndHour
142
- quietStartHour: ?quietStartHour
143
+ quietHours: ?quietHours
144
+ updatedAt: @now
143
145
  } { emit: { failure: PREFERENCE_SAVE_FAILED, success: PREFERENCE_SAVED } })
144
146
  DELETE_PREFERENCE -> idle
145
147
  (persist delete NotificationPreference ?id { emit: { failure: PREFERENCE_SAVE_FAILED, success: PREFERENCE_DELETED } })
@@ -171,19 +173,17 @@ orbital NotificationCenterOrbital {
171
173
 
172
174
  listens {
173
175
  SAVE_PREFERENCE {
174
- eventType : string
176
+ eventKey : string
175
177
  mode : string
176
178
  channel : string
177
- quietStartHour : number
178
- quietEndHour : number
179
+ quietHours : string
179
180
  }
180
181
  UPDATE_PREFERENCE {
181
182
  id : string
182
- eventType : string
183
+ eventKey : string
183
184
  mode : string
184
185
  channel : string
185
- quietStartHour : number
186
- quietEndHour : number
186
+ quietHours : string
187
187
  }
188
188
  DELETE_PREFERENCE {
189
189
  id : string
@@ -1,5 +1,5 @@
1
1
  app std-notification-preferences "1.0.0"
2
- "std-notification-preferences — per-person notification preference store plus a consultable gate. Owns NotificationPreference rows (one per person + event key: channel override in_app/email/push/slack, mode instant/digest, quiet-hours window 'HH:MM-HH:MM' such as 22:00-07:00). The PreferencesManage trait is standard CRUD over the store (list / create / update / delete, persisted, success/failure cascade arms): a data-grid browse plus ModalRecordModal create/edit forms whose SAVE carries the full row payload (std-list daisy-chain idiom). The PreferenceGate trait is the consult half other notify traits call: emit CHECK_PREFERENCE { personId, eventKey } and the gate fetches the matching row (a row whose eventKey is blank applies to every event) and answers PREFERENCE_RESOLVED { personId, eventKey, channel, mode, suppressed } — suppressed is true only when the row's quietHours window covers the current hour; mode 'digest' rides on the verdict for the caller to defer. Default policy: no matching row — or a failed lookup — resolves fail-open to { channel: in_app, mode: instant, suppressed: false }; the absence of a preference never silences a notification."
2
+ "std-notification-preferences — per-person notification preference store plus a consultable gate. Owns NotificationPreference rows (one per person + event key: channel override in_app/email/push/slack/sms/webhook, mode instant/digest/off, quiet-hours window 'HH:MM-HH:MM' such as 22:00-07:00). The PreferencesManage trait is standard CRUD over the store (list / create / update / delete, persisted, success/failure cascade arms): a data-grid browse plus ModalRecordModal create/edit forms whose SAVE carries the full row payload (std-list daisy-chain idiom). The PreferenceGate trait is the consult half other notify traits call: emit CHECK_PREFERENCE { personId, eventKey } and the gate fetches the matching row (a row whose eventKey is blank applies to every event) and answers PREFERENCE_RESOLVED { personId, eventKey, channel, mode, suppressed } — suppressed is true only when the row's quietHours window covers the current hour; mode 'digest' rides on the verdict for the caller to defer. Default policy: no matching row — or a failed lookup — resolves fail-open to { channel: in_app, mode: instant, suppressed: false }; the absence of a preference never silences a notification."
3
3
  orbital NotificationPreferencesOrbital {
4
4
  uses Modal from "std/behaviors/std-modal"
5
5
 
@@ -7,12 +7,12 @@ orbital NotificationPreferencesOrbital {
7
7
 
8
8
  type PREFERENCE_RESOLVED = Event { personId : string, eventKey : string, channel : string, mode : string, suppressed : boolean } "The gate's verdict. Fail-open defaults: no matching row or a failed lookup resolves { channel: in_app, mode: instant, suppressed: false } — absence of a preference never silences a notification."
9
9
 
10
- entity NotificationPreference [persistent: notification_preferences] {
10
+ entity NotificationPreference [persistent: notificationPreferences] {
11
11
  id : string!
12
12
  personId : string! @description "The person this preference row belongs to." @synonyms "user, recipient, owner, account"
13
- eventKey : string! @description "Event key this preference applies to (matches a notify listener's eventKey); blank = all events." @synonyms "event, trigger, topic, subscription"
14
- channel : "in_app" | "email" | "push" | "slack" @description "Delivery channel override for this person." @synonyms "preferred channel, delivery method" = in_app
15
- mode : "instant" | "digest" @description "Delivery mode: instant (default) or digest (deferred to a batch cycle)." @synonyms "delivery mode, frequency, cadence, defer" = instant
13
+ eventKey : string @description "Event key this preference applies to (matches a notify listener's eventKey); blank = all events." @synonyms "event, trigger, topic, subscription" = ""
14
+ channel : "in_app" | "email" | "push" | "slack" | "sms" | "webhook" @description "Delivery channel override for this person." @synonyms "preferred channel, delivery method" = in_app
15
+ mode : "instant" | "digest" | "off" @description "Delivery mode: instant (default), digest (deferred to a batch cycle), or off (muted)." @synonyms "delivery mode, frequency, cadence, mute, defer" = instant
16
16
  quietHours : string @description "Quiet-hours window as \"HH:MM-HH:MM\" (e.g. 22:00-07:00); blank or equal start/end = disabled." @synonyms "do not disturb, quiet window, night mode" = ""
17
17
  createdAt : number @description "Timestamp indicating when this preference row was created." @synonyms "creation time, created" = 0
18
18
  updatedAt : number @description "Timestamp indicating when this preference row was last updated." @synonyms "modified, last updated" = 0
@@ -1,6 +1,6 @@
1
1
  app std-notify-on-event "2.0.0"
2
- "std-notify-on-event — cross-orbital wiring atom. Subscribes to events from another orbital (e.g. CheckoutOrbital emits OrderPlaced, OrderRecordOrbital reacts via this atom) and dispatches a notification. Use when one orbital needs to react to or be notified about events that happen in another orbital — the wiring goes through this atom's `EventOccurred` listener which the molecule renames via traitOverrides.events to the upstream event name. Dispatch gates on `guardOp`: `eq` (default) does string-equality on `triggerStatus` against the upstream event's `status` field; `gt`/`lt`/`gte`/`lte` instead numerically compare `guardField` (read from the event's `data` payload) against `guardValue`, for threshold-crossing asks such as low-stock alerts. Resolves recipient from `recipients` (first entry; multi-recipient fan-out is a future extension), falling back to `fallbackRecipient` when set, or `@user.id` otherwise. Skips dispatch when the recipient is in `suppressionList`. Delivery consults std-notification-center data before dispatching: per-user NotificationPreference rows (matched on `userId` = recipient and `eventType` = this listener's `eventKey`, blank = all) can mute (`mode: off`), defer to the digest drain (`mode: digest`), override the channel, or impose per-user quiet hours; the atom-level `quietStartHour`/`quietEndHour` window and the `frequencyCapPerWeek` rolling cap (counted against dispatched NotificationRecord audit rows for the same recipient + channel) suppress likewise. Every suppression persists an honest audit row (`dispatched: false` with the reason in `error`: quiet-hours / digest-deferred / muted-by-preference / frequency-cap / no-push-subscription) which std-notification-center's digest cycle drains later. Channels: `in-app` emits the `notify` toast; `email`/`sms`/`webhook` dispatch through call-service (email.send / twilio.sendSMS / webhook.send); `push` looks up the recipient's PushSubscription row (owned by std-notification-center) and dispatches web-push via push.send (`pushTitle` falls back to `template`; `pushDeepLink`/`pushIcon` optional). The audit row's `dispatched` flag is honest — true only once the service call succeeds."
3
- type PrefRow = { userId : string, eventType : string, mode : string, channel : string, quietStartHour : number, quietEndHour : number }
2
+ "std-notify-on-event — cross-orbital wiring atom. Subscribes to events from another orbital (e.g. CheckoutOrbital emits OrderPlaced, OrderRecordOrbital reacts via this atom) and dispatches a notification. Use when one orbital needs to react to or be notified about events that happen in another orbital — the wiring goes through this atom's `EventOccurred` listener which the molecule renames via traitOverrides.events to the upstream event name. Dispatch gates on `guardOp`: `eq` (default) does string-equality on `triggerStatus` against the upstream event's `status` field; `gt`/`lt`/`gte`/`lte` instead numerically compare `guardField` (read from the event's `data` payload) against `guardValue`, for threshold-crossing asks such as low-stock alerts. Resolves recipient from `recipients` (first entry; multi-recipient fan-out is a future extension), falling back to `fallbackRecipient` when set, or `@user.id` otherwise. Skips dispatch when the recipient is in `suppressionList`. Delivery consults std-notification-center data before dispatching: per-person NotificationPreference rows (matched on `personId` = recipient and `eventKey` = this listener's `eventKey`, blank = all) can mute (`mode: off`), defer to the digest drain (`mode: digest`), override the channel, or impose per-user quiet hours (a 'HH:MM-HH:MM' window, blank = none); the atom-level `quietStartHour`/`quietEndHour` window and the `frequencyCapPerWeek` rolling cap (counted against dispatched NotificationRecord audit rows for the same recipient + channel) suppress likewise. Every suppression persists an honest audit row (`dispatched: false` with the reason in `error`: quiet-hours / digest-deferred / muted-by-preference / frequency-cap / no-push-subscription) which std-notification-center's digest cycle drains later. Channels: `in-app` emits the `notify` toast; `email`/`sms`/`webhook` dispatch through call-service (email.send / twilio.sendSMS / webhook.send); `push` looks up the recipient's PushSubscription row (owned by std-notification-center) and dispatches web-push via push.send (`pushTitle` falls back to `template`; `pushDeepLink`/`pushIcon` optional). The audit row's `dispatched` flag is honest — true only once the service call succeeds."
3
+ type PrefRow = { personId : string, eventKey : string, mode : string, channel : string, quietHours : string }
4
4
  type AuditRow = { recipient : string, channel : string, dispatched : boolean, recordedAt : number }
5
5
  type SubRow = { userId : string, endpoint : string, p256dh : string, auth : string }
6
6
  orbital NotifyOnEventOrbital {
@@ -19,8 +19,7 @@ orbital NotifyOnEventOrbital {
19
19
  recordedAt : number @description "Timestamp indicating when the notification record was created." @synonyms "creation time, timestamp, date, when" = 0
20
20
  prefMode : string @description "Scratch: effective preference mode resolved for the current dispatch (instant/digest/off)." @synonyms "resolved mode" = ""
21
21
  prefChannel : string @description "Scratch: channel override from the matched preference row; blank = no override." @synonyms "channel override" = ""
22
- prefQuietStart : number @description "Scratch: per-user quiet-hours start (hour 0-23) from the matched preference row." @synonyms "user quiet start" = 0
23
- prefQuietEnd : number @description "Scratch: per-user quiet-hours end (hour 0-23) from the matched preference row." @synonyms "user quiet end" = 0
22
+ prefQuietHours : string @description "Scratch: per-user quiet-hours window \"HH:MM-HH:MM\" from the matched preference row; blank = none." @synonyms "user quiet hours" = ""
24
23
  pushEndpoint : string @description "Scratch: push endpoint of the recipient's subscription for the current dispatch." @synonyms "subscription endpoint" = ""
25
24
  pushP256dh : string @description "Scratch: p256dh key of the recipient's push subscription." @synonyms "push key" = ""
26
25
  pushAuth : string @description "Scratch: auth secret of the recipient's push subscription." @synonyms "push auth" = ""
@@ -28,12 +27,13 @@ orbital NotifyOnEventOrbital {
28
27
 
29
28
  entity NotificationPreference [persistent: notificationPreferences] {
30
29
  id : string!
31
- userId : string @description "Owner of this preference row." @synonyms "user, owner, account" = ""
32
- eventType : string @description "Event key this preference applies to (matches the listener's eventKey); blank = all events." @synonyms "event, trigger, subscription" = ""
33
- mode : string @description "Delivery mode: instant, digest, or off." @synonyms "delivery mode, frequency, cadence" = instant
34
- channel : string @description "Channel override for this user (in-app/email/sms/push/webhook); blank = the listener's configured channel." @synonyms "preferred channel, delivery method" = ""
35
- quietStartHour : number @description "Per-user quiet-hours start (hour 0-23); equal to quietEndHour = disabled." @synonyms "quiet start, do not disturb from" = 0
36
- quietEndHour : number @description "Per-user quiet-hours end (hour 0-23); equal to quietStartHour = disabled." @synonyms "quiet end, do not disturb until" = 0
30
+ personId : string! @description "Owner of this preference row." @synonyms "user, owner, account"
31
+ eventKey : string @description "Event key this preference applies to (matches the listener's eventKey); blank = all events." @synonyms "event, trigger, subscription" = ""
32
+ channel : "in_app" | "email" | "push" | "slack" | "sms" | "webhook" @description "Channel override for this user." @synonyms "preferred channel, delivery method" = in_app
33
+ mode : "instant" | "digest" | "off" @description "Delivery mode: instant, digest, or off." @synonyms "delivery mode, frequency, cadence" = instant
34
+ quietHours : string @description "Per-user quiet-hours window as \"HH:MM-HH:MM\" (e.g. 22:00-07:00); blank = disabled." @synonyms "do not disturb, quiet window, night mode" = ""
35
+ createdAt : number @description "Timestamp indicating when this preference row was created." @synonyms "creation time, created" = 0
36
+ updatedAt : number @description "Timestamp indicating when this preference row was last updated." @synonyms "modified, last updated" = 0
37
37
  }
38
38
 
39
39
  entity PushSubscription [persistent: pushSubscriptions] {
@@ -72,10 +72,9 @@ orbital NotifyOnEventOrbital {
72
72
  severity: @entity.severity
73
73
  })
74
74
  NOTIFY_PREFS_LOADED -> idle
75
- (set @entity.prefMode (if ["=", (array/len (array/filter ?data (fn p (and ["=", (object/get @p userId), @entity.recipient] (or ["=", (object/get @p eventType), ""] ["=", (object/get @p eventType), @config.eventKey]))))), 0] "instant" (object/get (array/first (array/filter ?data (fn p (and ["=", (object/get @p userId), @entity.recipient] (or ["=", (object/get @p eventType), ""] ["=", (object/get @p eventType), @config.eventKey]))))) mode)))
76
- (set @entity.prefChannel (if ["=", (array/len (array/filter ?data (fn p (and ["=", (object/get @p userId), @entity.recipient] (or ["=", (object/get @p eventType), ""] ["=", (object/get @p eventType), @config.eventKey]))))), 0] "" (object/get (array/first (array/filter ?data (fn p (and ["=", (object/get @p userId), @entity.recipient] (or ["=", (object/get @p eventType), ""] ["=", (object/get @p eventType), @config.eventKey]))))) channel)))
77
- (set @entity.prefQuietStart (if ["=", (array/len (array/filter ?data (fn p (and ["=", (object/get @p userId), @entity.recipient] (or ["=", (object/get @p eventType), ""] ["=", (object/get @p eventType), @config.eventKey]))))), 0] 0 (object/get (array/first (array/filter ?data (fn p (and ["=", (object/get @p userId), @entity.recipient] (or ["=", (object/get @p eventType), ""] ["=", (object/get @p eventType), @config.eventKey]))))) quietStartHour)))
78
- (set @entity.prefQuietEnd (if ["=", (array/len (array/filter ?data (fn p (and ["=", (object/get @p userId), @entity.recipient] (or ["=", (object/get @p eventType), ""] ["=", (object/get @p eventType), @config.eventKey]))))), 0] 0 (object/get (array/first (array/filter ?data (fn p (and ["=", (object/get @p userId), @entity.recipient] (or ["=", (object/get @p eventType), ""] ["=", (object/get @p eventType), @config.eventKey]))))) quietEndHour)))
75
+ (set @entity.prefMode (if ["=", (array/len (array/filter ?data (fn p (and ["=", (object/get @p personId), @entity.recipient] (or ["=", (object/get @p eventKey), ""] ["=", (object/get @p eventKey), @config.eventKey]))))), 0] "instant" (object/get (array/first (array/filter ?data (fn p (and ["=", (object/get @p personId), @entity.recipient] (or ["=", (object/get @p eventKey), ""] ["=", (object/get @p eventKey), @config.eventKey]))))) mode)))
76
+ (set @entity.prefChannel (if ["=", (array/len (array/filter ?data (fn p (and ["=", (object/get @p personId), @entity.recipient] (or ["=", (object/get @p eventKey), ""] ["=", (object/get @p eventKey), @config.eventKey]))))), 0] "" (object/get (array/first (array/filter ?data (fn p (and ["=", (object/get @p personId), @entity.recipient] (or ["=", (object/get @p eventKey), ""] ["=", (object/get @p eventKey), @config.eventKey]))))) channel)))
77
+ (set @entity.prefQuietHours (if ["=", (array/len (array/filter ?data (fn p (and ["=", (object/get @p personId), @entity.recipient] (or ["=", (object/get @p eventKey), ""] ["=", (object/get @p eventKey), @config.eventKey]))))), 0] "" (object/get (array/first (array/filter ?data (fn p (and ["=", (object/get @p personId), @entity.recipient] (or ["=", (object/get @p eventKey), ""] ["=", (object/get @p eventKey), @config.eventKey]))))) quietHours)))
79
78
  (set @entity.channel (if ["=", @entity.prefChannel, ""] @entity.channel @entity.prefChannel))
80
79
  (if ["=", @entity.prefMode, "off"] (persist create NotificationRecord {
81
80
  channel: @entity.channel
@@ -93,7 +92,7 @@ orbital NotifyOnEventOrbital {
93
92
  recipient: @entity.recipient
94
93
  recordedAt: @now
95
94
  severity: @entity.severity
96
- }) (if (and (not ["=", @entity.prefQuietStart, @entity.prefQuietEnd]) (if ["<", @entity.prefQuietStart, @entity.prefQuietEnd] (and [">=", (time/hour @now), @entity.prefQuietStart] ["<", (time/hour @now), @entity.prefQuietEnd]) (or [">=", (time/hour @now), @entity.prefQuietStart] ["<", (time/hour @now), @entity.prefQuietEnd]))) (persist create NotificationRecord {
95
+ }) (if (and (str/includes @entity.prefQuietHours "-") ["!=", (str/slice (array/first (str/split @entity.prefQuietHours "-")) 0 2), (str/slice (array/nth (str/split @entity.prefQuietHours "-") 1) 0 2)] (if ["<", (str/slice (array/first (str/split @entity.prefQuietHours "-")) 0 2), (str/slice (array/nth (str/split @entity.prefQuietHours "-") 1) 0 2)] (and [">=", (time/hour @now), (str/slice (array/first (str/split @entity.prefQuietHours "-")) 0 2)] ["<", (time/hour @now), (str/slice (array/nth (str/split @entity.prefQuietHours "-") 1) 0 2)]) (or [">=", (time/hour @now), (str/slice (array/first (str/split @entity.prefQuietHours "-")) 0 2)] ["<", (time/hour @now), (str/slice (array/nth (str/split @entity.prefQuietHours "-") 1) 0 2)]))) (persist create NotificationRecord {
97
96
  channel: @entity.channel
98
97
  dispatched: false
99
98
  error: "quiet-hours"
@@ -101,7 +100,7 @@ orbital NotifyOnEventOrbital {
101
100
  recipient: @entity.recipient
102
101
  recordedAt: @now
103
102
  severity: @entity.severity
104
- }) (if ["=", @entity.channel, in-app] (do (set @entity.dispatched true) (notify @config.severity @config.template)) (fetch NotificationRecord {
103
+ }) (if (or ["=", @entity.channel, in-app] ["=", @entity.channel, in_app]) (do (set @entity.dispatched true) (notify @config.severity @config.template)) (fetch NotificationRecord {
105
104
  emit: { failure: NOTIFY_LOOKUP_FAILED, success: NOTIFY_CAP_LOADED }
106
105
  filter: [">", (object/get @entity recordedAt), ["-", @now, 604800000]]
107
106
  })))))
@@ -380,12 +379,12 @@ orbital NotifyOnEventOrbital {
380
379
  @tier "internal"
381
380
  eventKey : string = ""
382
381
  @label "Event key for preferences"
383
- @description "Stable key naming the upstream event this listener is bound to (e.g. 'OrderPlaced'). NotificationPreference rows (std-notification-center) whose eventType equals this key — or is blank — apply to this listener's dispatches."
382
+ @description "Stable key naming the upstream event this listener is bound to (e.g. 'OrderPlaced'). NotificationPreference rows (std-notification-center) whose eventKey equals this key — or is blank — apply to this listener's dispatches."
384
383
  @synonyms "preference key, event name, subscription key, notification topic"
385
384
  @tier "policy"
386
385
  notifyChannel : "in-app" | "email" | "sms" | "push" | "webhook" = in-app
387
386
  @label "Channel"
388
- @description "Delivery channel. `in-app` (default) emits the in-app `notify` toast. `email`/`sms`/`webhook` dispatch through call-service at event time (email.send / twilio.sendSMS / webhook.send) and persist a NotificationRecord audit row with an honest `dispatched` flag: true only once the service call succeeds, false plus the failure detail in `error` otherwise. `push` looks up the recipient's PushSubscription row (captured by std-notification-center's subscribe flow) and dispatches web-push via push.send — a recipient with no subscription records the audit row undispatched (`no-push-subscription`). `webhook` requires `webhookUrl` to actually dispatch; a blank `webhookUrl` still records the audit row undispatched rather than failing. A per-user NotificationPreference row with a non-blank `channel` overrides this value for that recipient."
387
+ @description "Delivery channel. `in-app` (default) emits the in-app `notify` toast. `email`/`sms`/`webhook` dispatch through call-service at event time (email.send / twilio.sendSMS / webhook.send) and persist a NotificationRecord audit row with an honest `dispatched` flag: true only once the service call succeeds, false plus the failure detail in `error` otherwise. `push` looks up the recipient's PushSubscription row (captured by std-notification-center's subscribe flow) and dispatches web-push via push.send — a recipient with no subscription records the audit row undispatched (`no-push-subscription`). `webhook` requires `webhookUrl` to actually dispatch; a blank `webhookUrl` still records the audit row undispatched rather than failing. A per-user NotificationPreference row's `channel` overrides this value for that recipient."
389
388
  @synonyms "delivery method, notification channel, send via, dispatch through, push notification, web push"
390
389
  @tier "policy"
391
390
  emailSubject : string = ""
@@ -0,0 +1,215 @@
1
+ app std-gantt-schedule "1.0.0"
2
+ "std-gantt-schedule — GENERIC view-only Gantt/timeline atom: fetches a task collection on INIT and renders it as bars on a day-scale axis via the gantt pattern, with group headers, dependency arrows, and a today marker. Fully rebindable: a host overrides the linked entity and points the field-mapping configs (labelField/startField/endField/durationField/statusField/groupField) at its own columns; clicking a bar emits the config-named onBarClick event with the row id. No drag-edit, no zoom — schedule viewing only."
3
+ type GanttLinkSpec = { from : string!, to : string! }
4
+ orbital GanttScheduleOrbital {
5
+ type TaskLoaded = Event { data : [Task] } "Fired when Task finishes loading; payload.data holds the tasks"
6
+
7
+ type TaskLoadFailed = Event { error : string, code : string } "Fired when Task fails to load"
8
+
9
+ entity Task [runtime] {
10
+ id : string!
11
+ title : string @description "task label shown on the bar" @synonyms "name, label, summary, subject" = ""
12
+ start : datetime @description "task start timestamp the bar is placed from" @synonyms "starts, begins, start date, from"
13
+ end : datetime @description "task end timestamp the bar is placed to; when absent, duration days are used" @synonyms "finish, due, end date, until"
14
+ status : "pending" | "active" | "complete" | "blocked" @description "task lifecycle status, drives bar colour" @synonyms "state, stage, phase" = pending
15
+ group : string @description "group/section the task renders under a header row for" @synonyms "team, swimlane, section, project" = ""
16
+ }
17
+
18
+ trait TaskGanttSchedule -> @rebindable Task [interaction, collection] {
19
+ initial: loading
20
+
21
+ state loading {
22
+ INIT -> loading
23
+ (fetch Task {
24
+ emit: { failure: TaskLoadFailed, success: TaskLoaded }
25
+ include: @config.include
26
+ })
27
+ (render-ui main {
28
+ align: center
29
+ children: [{ type: spinner }, {
30
+ color: muted
31
+ content: "Loading schedule…"
32
+ type: typography
33
+ variant: caption
34
+ }]
35
+ className: py-12
36
+ direction: vertical
37
+ gap: md
38
+ type: stack
39
+ })
40
+ TaskLoaded -> chart
41
+ (render-ui main {
42
+ children: [{
43
+ align: center
44
+ children: [{ name: gantt-chart, size: xl, type: icon }, {
45
+ content: @config.title
46
+ type: typography
47
+ variant: h2
48
+ }]
49
+ direction: horizontal
50
+ gap: sm
51
+ type: stack
52
+ }, {
53
+ barClickEvent: @config.onBarClick
54
+ dayWidth: @config.dayWidth
55
+ durationField: @config.durationField
56
+ endField: @config.endField
57
+ groupField: @config.groupField
58
+ links: @config.links
59
+ rangeEnd: @config.rangeEnd
60
+ rangeStart: @config.rangeStart
61
+ showToday: @config.showToday
62
+ startField: @config.startField
63
+ statusField: @config.statusField
64
+ tasks: ?data
65
+ titleField: @config.labelField
66
+ type: gantt
67
+ }]
68
+ direction: vertical
69
+ gap: lg
70
+ type: stack
71
+ })
72
+ TaskLoadFailed -> error
73
+ (render-ui main {
74
+ align: center
75
+ children: [{
76
+ color: error
77
+ name: alert-triangle
78
+ size: lg
79
+ type: icon
80
+ }, {
81
+ content: "Failed to load"
82
+ type: typography
83
+ variant: h3
84
+ }, {
85
+ color: muted
86
+ content: ?error
87
+ type: typography
88
+ variant: body
89
+ }, {
90
+ action: RETRY
91
+ icon: rotate-ccw
92
+ label: Retry
93
+ type: button
94
+ variant: primary
95
+ }]
96
+ className: py-12
97
+ direction: vertical
98
+ gap: md
99
+ type: stack
100
+ })
101
+ }
102
+
103
+ state chart {
104
+ INIT -> loading
105
+ (fetch Task {
106
+ emit: { failure: TaskLoadFailed, success: TaskLoaded }
107
+ include: @config.include
108
+ })
109
+ (render-ui main { type: spinner })
110
+ }
111
+
112
+ state error {
113
+ INIT -> loading
114
+ (fetch Task {
115
+ emit: { failure: TaskLoadFailed, success: TaskLoaded }
116
+ include: @config.include
117
+ })
118
+ (render-ui main { type: spinner })
119
+ RETRY -> loading
120
+ (fetch Task {
121
+ emit: { failure: TaskLoadFailed, success: TaskLoaded }
122
+ include: @config.include
123
+ })
124
+ (render-ui main { type: spinner })
125
+ }
126
+
127
+ emits {
128
+ TaskLoaded
129
+ @description "Indicates that schedule tasks have been successfully retrieved."
130
+ @synonyms "loaded, fetched, retrieved, populated"
131
+ @tier "domain"
132
+ TaskLoadFailed
133
+ @description "Indicates a failure to load schedule tasks."
134
+ @synonyms "error, failed, problem, loading issue"
135
+ @tier "internal"
136
+ @config.onBarClick -> external {
137
+ id : string
138
+ }
139
+ @description "Emitted when a gantt bar is clicked; { id } carries the clicked task row's id."
140
+ @tier "presentation"
141
+ }
142
+
143
+ config {
144
+ title : string = "Schedule"
145
+ @label "Title"
146
+ @description "Header text shown above the gantt chart."
147
+ @tier "presentation"
148
+ labelField : string = "title"
149
+ @label "Label field"
150
+ @description "Row column holding the bar label. Rebound hosts point this at their own column (e.g. name, summary)."
151
+ @synonyms "title field, label column, name field"
152
+ @tier "presentation"
153
+ startField : string = "start"
154
+ @label "Start field"
155
+ @description "Row column holding the start timestamp each bar is placed from (e.g. startTime, startsAt, beginsAt)."
156
+ @synonyms "start column, from field, begin field"
157
+ @tier "presentation"
158
+ endField : string = "end"
159
+ @label "End field"
160
+ @description "Row column holding the end timestamp each bar is placed to. When a row has no end, durationField (days) is used instead."
161
+ @synonyms "end column, due field, finish field"
162
+ @tier "presentation"
163
+ durationField : string = ""
164
+ @label "Duration field"
165
+ @description "Row column holding the task length in days, used when the row has no end value. Empty = bars always read endField."
166
+ @synonyms "duration column, length field, days field"
167
+ @tier "presentation"
168
+ statusField : string = "status"
169
+ @label "Status field"
170
+ @description "Row column holding the status value that drives bar colour."
171
+ @synonyms "status column, state field, stage field"
172
+ @tier "presentation"
173
+ groupField : string = ""
174
+ @label "Group field"
175
+ @description "Row column rows are grouped under header rows by (e.g. team, project). Empty (default) = flat list, no group headers."
176
+ @synonyms "group column, swimlane field, section field"
177
+ @tier "presentation"
178
+ rangeStart : string = ""
179
+ @label "Range start"
180
+ @description "First visible day (ISO date). Empty (default) = 2 days before the earliest task."
181
+ @synonyms "window start, axis start, from date"
182
+ @tier "presentation"
183
+ rangeEnd : string = ""
184
+ @label "Range end"
185
+ @description "Last visible day (ISO date). Empty (default) = 2 days after the latest task end."
186
+ @synonyms "window end, axis end, to date"
187
+ @tier "presentation"
188
+ dayWidth : number = 28
189
+ @label "Day width"
190
+ @description "Pixels per day on the time axis."
191
+ @tier "presentation"
192
+ showToday : boolean = true
193
+ @label "Show today marker"
194
+ @description "Paint the today marker line when today falls inside the visible range."
195
+ @tier "presentation"
196
+ links : [GanttLinkSpec] = []
197
+ @label "Dependency links"
198
+ @description "Dependency arrows between task ids ({ from, to }); the host derives them from its own relation field and passes them here."
199
+ @synonyms "dependencies, dependency arrows, precedences"
200
+ @tier "presentation"
201
+ include : [string] = []
202
+ @label "Which relation fields to hydrate?"
203
+ @description "Entity relation field names to hydrate on fetch, so field mappings can use dot-paths like assignee.name. Empty = no hydration, the default."
204
+ @synonyms "relation hydration, populate relations, joined fields"
205
+ @tier "internal"
206
+ onBarClick : event = ""
207
+ @label "On bar click"
208
+ @description "Event emitted when a bar is clicked: UI:{onBarClick} with { id } carrying the clicked row's id. Empty means bars are not clickable."
209
+ @synonyms "bar click, task click, select task, open task"
210
+ @tier "presentation"
211
+ }
212
+ }
213
+
214
+ page "/tasks/gantt" as TaskGanttSchedulePage -> TaskGanttSchedule
215
+ }
@@ -23,7 +23,7 @@ orbital TextareaOrbital {
23
23
 
24
24
  emits {
25
25
  @config.action -> external { }
26
- @description "Declarative event name for trait dispatch"
26
+ @description "Declarative event: fires on ⌘/Ctrl+Enter with `{ value }`."
27
27
  @tier "essential"
28
28
  @config.onChange -> external {
29
29
  value : string!
@@ -47,7 +47,7 @@ orbital TextareaOrbital {
47
47
  @tier "presentation"
48
48
  action : event = "ACTION"
49
49
  @label "Action"
50
- @description "Declarative event name for trait dispatch"
50
+ @description "Declarative event: fires on ⌘/Ctrl+Enter with `{ value }`."
51
51
  @tier "presentation"
52
52
  error : string
53
53
  @label "Error"