aeon-ui-engine 1.3.1 → 1.3.5

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 (78) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/package.json +1 -1
  3. package/packages/react/src/combobox/index.tsx +13 -8
  4. package/packages/react/src/tooltip/index.tsx +48 -22
  5. package/packages/schemas/dist/index.d.ts +69 -24
  6. package/packages/schemas/dist/index.js +447 -49
  7. package/packages/schemas/json/appNav.json +146 -0
  8. package/packages/schemas/json/appShell.json +151 -0
  9. package/packages/schemas/json/avatar.json +130 -0
  10. package/packages/schemas/json/bar.json +126 -0
  11. package/packages/schemas/json/catalog.json +3163 -15
  12. package/packages/schemas/json/combobox.json +19 -7
  13. package/packages/schemas/json/composer.json +125 -0
  14. package/packages/schemas/json/content.json +167 -0
  15. package/packages/schemas/json/conversation.json +166 -0
  16. package/packages/schemas/json/entry.json +182 -0
  17. package/packages/schemas/json/identity.json +147 -0
  18. package/packages/schemas/json/listRow.json +131 -0
  19. package/packages/schemas/json/metricStrip.json +132 -0
  20. package/packages/schemas/json/nav.json +136 -0
  21. package/packages/schemas/json/panel.json +143 -0
  22. package/packages/schemas/json/pinInput.json +112 -0
  23. package/packages/schemas/json/profileHeader.json +138 -0
  24. package/packages/schemas/json/prompt.json +155 -0
  25. package/packages/schemas/json/radioGroup.json +130 -0
  26. package/packages/schemas/json/separator.json +105 -0
  27. package/packages/schemas/json/slider.json +147 -0
  28. package/packages/schemas/json/statusBanner.json +149 -0
  29. package/packages/schemas/json/stickyBar.json +153 -0
  30. package/packages/schemas/json/thread.json +129 -0
  31. package/packages/schemas/json/tooltip.json +21 -7
  32. package/packages/schemas/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  33. package/packages/schemas/prompts/cursorrules.md +6 -4
  34. package/packages/schemas/prompts/statecharts.md +23 -2
  35. package/packages/schemas/prompts/system.md +2 -2
  36. package/packages/schemas/registry/catalog.json +295 -4
  37. package/packages/schemas/registry/components/appNav.json +146 -0
  38. package/packages/schemas/registry/components/appShell.json +151 -0
  39. package/packages/schemas/registry/components/avatar.json +130 -0
  40. package/packages/schemas/registry/components/bar.json +126 -0
  41. package/packages/schemas/registry/components/combobox.json +19 -7
  42. package/packages/schemas/registry/components/composer.json +125 -0
  43. package/packages/schemas/registry/components/content.json +167 -0
  44. package/packages/schemas/registry/components/conversation.json +166 -0
  45. package/packages/schemas/registry/components/entry.json +182 -0
  46. package/packages/schemas/registry/components/identity.json +147 -0
  47. package/packages/schemas/registry/components/listRow.json +131 -0
  48. package/packages/schemas/registry/components/metricStrip.json +132 -0
  49. package/packages/schemas/registry/components/nav.json +136 -0
  50. package/packages/schemas/registry/components/panel.json +143 -0
  51. package/packages/schemas/registry/components/pinInput.json +112 -0
  52. package/packages/schemas/registry/components/profileHeader.json +138 -0
  53. package/packages/schemas/registry/components/prompt.json +155 -0
  54. package/packages/schemas/registry/components/radioGroup.json +130 -0
  55. package/packages/schemas/registry/components/separator.json +105 -0
  56. package/packages/schemas/registry/components/slider.json +147 -0
  57. package/packages/schemas/registry/components/statusBanner.json +149 -0
  58. package/packages/schemas/registry/components/stickyBar.json +153 -0
  59. package/packages/schemas/registry/components/thread.json +129 -0
  60. package/packages/schemas/registry/components/tooltip.json +21 -7
  61. package/packages/schemas/registry/machines/accordion.json +58 -0
  62. package/packages/schemas/registry/machines/appNav.json +108 -0
  63. package/packages/schemas/registry/machines/appShell.json +128 -0
  64. package/packages/schemas/registry/machines/contentRegion.json +314 -0
  65. package/packages/schemas/registry/machines/panel.json +131 -0
  66. package/packages/schemas/registry/machines/popover.json +143 -0
  67. package/packages/schemas/registry/machines/slider.json +151 -0
  68. package/packages/schemas/registry/machines/stickyBar.json +261 -0
  69. package/packages/schemas/registry/machines/tabs.json +58 -0
  70. package/packages/schemas/registry/machines/toast.json +89 -0
  71. package/packages/schemas/registry/machines/toggle.json +134 -0
  72. package/packages/schemas/scripts/emit.ts +23 -0
  73. package/packages/schemas/src/components.ts +429 -6
  74. package/packages/schemas/src/index.test.ts +83 -0
  75. package/packages/schemas/src/index.ts +1 -0
  76. package/packages/schemas/src/machine-id.ts +7 -0
  77. package/packages/schemas/src/prompts.ts +25 -4
  78. package/packages/schemas/src/registry.ts +28 -39
@@ -2,10 +2,21 @@ import { mkdirSync, writeFileSync, cpSync, existsSync } from 'node:fs'
2
2
  import { dirname, join } from 'node:path'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import {
5
+ accordionMachine,
6
+ appNavMachine,
7
+ appShellMachine,
5
8
  asyncMachine,
6
9
  buttonLifecycleMachine,
10
+ contentRegionMachine,
7
11
  dialogMachine,
8
12
  fieldMachine,
13
+ panelMachine,
14
+ popoverMachine,
15
+ sliderMachine,
16
+ stickyBarMachine,
17
+ tabsMachine,
18
+ toastMachine,
19
+ toggleMachine,
9
20
  } from '@aeon-ui/primitives'
10
21
  import { catalog, componentSchemas } from '../src/components.js'
11
22
  import { AEON_SYSTEM_PROMPT, CURSOR_RULES_SNIPPET, STATECHART_PROMPTS } from '../src/prompts.js'
@@ -30,11 +41,23 @@ for (const [id, schema] of Object.entries(componentSchemas)) {
30
41
  writeFileSync(join(registryDir, 'components', `${id}.json`), `${JSON.stringify(schema, null, 2)}\n`)
31
42
  }
32
43
 
44
+ /** Every primitives machine agents may fetch for validEvents / state trees. */
33
45
  const machines = {
34
46
  async: asyncMachine,
35
47
  buttonLifecycle: buttonLifecycleMachine,
36
48
  dialog: dialogMachine,
37
49
  field: fieldMachine,
50
+ popover: popoverMachine,
51
+ toggle: toggleMachine,
52
+ tabs: tabsMachine,
53
+ accordion: accordionMachine,
54
+ toast: toastMachine,
55
+ slider: sliderMachine,
56
+ panel: panelMachine,
57
+ appShell: appShellMachine,
58
+ appNav: appNavMachine,
59
+ stickyBar: stickyBarMachine,
60
+ contentRegion: contentRegionMachine,
38
61
  } as const
39
62
 
40
63
  for (const [id, machine] of Object.entries(machines)) {
@@ -222,24 +222,28 @@ export const componentSchemas = {
222
222
  combobox: componentSchema({
223
223
  id: 'https://aeon-ui.com/schemas/combobox.json',
224
224
  title: 'Combobox',
225
- description: 'Typeahead — React useState for open/query/highlight today. Do not add a duplicate floating machine without COMPONENT_CHECKLIST.',
225
+ description:
226
+ 'Open/closed via popoverMachine (same family as Select/Menu/Popover). Query, highlight, and item registry stay React-local for typeahead.',
226
227
  scope: 'combobox',
227
228
  parts: ['root', 'input', 'icon', 'content', 'item', 'empty'],
228
229
  states: ['open', 'closed'],
229
- machine: null,
230
- runtime: 'useState',
230
+ machine: 'popoverMachine',
231
+ runtime: 'useAeonMachine',
232
+ events: ['OPEN', 'CLOSE', 'TOGGLE', 'ESCAPE', 'POINTER_DOWN_OUTSIDE'],
231
233
  slots: { Root: 'combobox.root', Input: 'combobox.input', Content: 'combobox.content', Item: 'combobox.item' },
232
234
  }),
233
235
 
234
236
  tooltip: componentSchema({
235
237
  id: 'https://aeon-ui.com/schemas/tooltip.json',
236
238
  title: 'Tooltip',
237
- description: 'Open via React useState. Touch tap toggles.',
239
+ description:
240
+ 'Open/closed via popoverMachine (same family as Popover/Select/Combobox). openDelay / closeDelay / touchDuration stay React-local.',
238
241
  scope: 'tooltip',
239
242
  parts: ['root', 'trigger', 'positioner', 'content', 'arrow'],
240
243
  states: ['open', 'closed'],
241
- machine: null,
242
- runtime: 'useState',
244
+ machine: 'popoverMachine',
245
+ runtime: 'useAeonMachine',
246
+ events: ['OPEN', 'CLOSE', 'TOGGLE', 'SET_OPEN', 'ESCAPE', 'POINTER_DOWN_OUTSIDE'],
243
247
  slots: { Root: 'tooltip.root', Trigger: 'tooltip.trigger', Content: 'tooltip.content' },
244
248
  }),
245
249
 
@@ -289,6 +293,425 @@ export const componentSchemas = {
289
293
  },
290
294
  slots: { Root: 'progress.root', Track: 'progress.track', Range: 'progress.range', Label: 'progress.label' },
291
295
  }),
296
+
297
+ radioGroup: componentSchema({
298
+ id: 'https://aeon-ui.com/schemas/radioGroup.json',
299
+ title: 'RadioGroup',
300
+ description: 'Single selection via tabsMachine (same chart family as Tabs/Nav).',
301
+ scope: 'radioGroup',
302
+ parts: ['root', 'item', 'itemControl', 'itemIndicator', 'itemLabel'],
303
+ states: ['selected'],
304
+ machine: 'tabsMachine',
305
+ runtime: 'useAeonMachine',
306
+ events: ['SELECT'],
307
+ slots: {
308
+ Root: 'radioGroup.root',
309
+ Item: 'radioGroup.item',
310
+ ItemControl: 'radioGroup.itemControl',
311
+ ItemLabel: 'radioGroup.itemLabel',
312
+ },
313
+ }),
314
+
315
+ slider: componentSchema({
316
+ id: 'https://aeon-ui.com/schemas/slider.json',
317
+ title: 'Slider',
318
+ description: 'Range control via sliderMachine — idle | dragging plus numeric value in context.',
319
+ scope: 'slider',
320
+ parts: ['root', 'track', 'range', 'thumb', 'valueText'],
321
+ states: ['idle', 'dragging'],
322
+ machine: 'sliderMachine',
323
+ runtime: 'useAeonMachine',
324
+ events: ['SET_VALUE', 'STEP', 'POINTER_DOWN', 'POINTER_UP', 'HOME', 'END'],
325
+ slots: {
326
+ Root: 'slider.root',
327
+ Track: 'slider.track',
328
+ Range: 'slider.range',
329
+ Thumb: 'slider.thumb',
330
+ ValueText: 'slider.valueText',
331
+ },
332
+ }),
333
+
334
+ pinInput: componentSchema({
335
+ id: 'https://aeon-ui.com/schemas/pinInput.json',
336
+ title: 'PinInput',
337
+ description: 'OTP / PIN cells — React state for digit buffer; projects idle | complete on root.',
338
+ scope: 'pinInput',
339
+ parts: ['root', 'input'],
340
+ states: ['idle', 'complete'],
341
+ machine: null,
342
+ runtime: 'useState',
343
+ slots: { Root: 'pinInput.root', Input: 'pinInput.input' },
344
+ }),
345
+
346
+ avatar: componentSchema({
347
+ id: 'https://aeon-ui.com/schemas/avatar.json',
348
+ title: 'Avatar',
349
+ description: 'Image load lifecycle in React; badge presence is a prop projection.',
350
+ scope: 'avatar',
351
+ parts: ['root', 'image', 'fallback', 'badge'],
352
+ states: ['idle', 'loading', 'loaded', 'error'],
353
+ machine: null,
354
+ runtime: 'useState',
355
+ slots: {
356
+ Root: 'avatar.root',
357
+ Image: 'avatar.image',
358
+ Fallback: 'avatar.fallback',
359
+ Badge: 'avatar.badge',
360
+ },
361
+ }),
362
+
363
+ content: componentSchema({
364
+ id: 'https://aeon-ui.com/schemas/content.json',
365
+ title: 'Content',
366
+ description:
367
+ 'Status-slotted body via contentRegionMachine (or Root state prop projection). Pair with Async for fetch.',
368
+ scope: 'content',
369
+ parts: ['root', 'toolbar', 'body', 'pending', 'empty', 'error', 'success', 'sentinel'],
370
+ states: ['idle', 'pending', 'empty', 'error', 'ready', 'loadingMore', 'success'],
371
+ machine: 'contentRegionMachine',
372
+ runtime: 'useAeonMachine',
373
+ events: ['LOAD', 'RESOLVE', 'REJECT', 'RETRY', 'LOAD_MORE', 'MORE_DONE', 'SUCCEED', 'RESET'],
374
+ slots: {
375
+ Root: 'content.root',
376
+ Pending: 'content.pending',
377
+ Empty: 'content.empty',
378
+ Error: 'content.error',
379
+ Success: 'content.success',
380
+ },
381
+ }),
382
+
383
+ panel: componentSchema({
384
+ id: 'https://aeon-ui.com/schemas/panel.json',
385
+ title: 'Panel',
386
+ description: 'Collapsible layout region via panelMachine — expanded | collapsed.',
387
+ scope: 'panel',
388
+ parts: ['group', 'root', 'trigger', 'label', 'content'],
389
+ states: ['expanded', 'collapsed'],
390
+ machine: 'panelMachine',
391
+ runtime: 'useAeonMachine',
392
+ events: ['EXPAND', 'COLLAPSE', 'TOGGLE', 'SET_EXPANDED'],
393
+ slots: {
394
+ Group: 'panel.group',
395
+ Root: 'panel.root',
396
+ Trigger: 'panel.trigger',
397
+ Label: 'panel.label',
398
+ Content: 'panel.content',
399
+ },
400
+ }),
401
+
402
+ appShell: componentSchema({
403
+ id: 'https://aeon-ui.com/schemas/appShell.json',
404
+ title: 'AppShell',
405
+ description: 'Application chrome frame via appShellMachine — idle | overlayOpen (+ dockVisible).',
406
+ scope: 'appShell',
407
+ parts: ['root', 'header', 'subheader', 'content', 'aside', 'footer', 'dock', 'scrim'],
408
+ states: ['idle', 'overlayOpen'],
409
+ machine: 'appShellMachine',
410
+ runtime: 'useAeonMachine',
411
+ events: ['OPEN_OVERLAY', 'CLOSE_OVERLAY', 'SHOW_DOCK', 'HIDE_DOCK', 'SET_DOCK'],
412
+ slots: {
413
+ Root: 'appShell.root',
414
+ Header: 'appShell.header',
415
+ Content: 'appShell.content',
416
+ Dock: 'appShell.dock',
417
+ Scrim: 'appShell.scrim',
418
+ },
419
+ }),
420
+
421
+ stickyBar: componentSchema({
422
+ id: 'https://aeon-ui.com/schemas/stickyBar.json',
423
+ title: 'StickyBar',
424
+ description:
425
+ 'Sticky/fixed chrome band via stickyBarMachine. DOM scope is bar (reuses bar anatomy zones).',
426
+ scope: 'bar',
427
+ parts: ['root', 'leading', 'center', 'trailing', 'seam'],
428
+ states: ['floating', 'docked', 'collapsed', 'hidden'],
429
+ machine: 'stickyBarMachine',
430
+ runtime: 'useAeonMachine',
431
+ events: ['DOCK', 'FLOAT', 'COLLAPSE', 'EXPAND', 'HIDE', 'SHOW', 'SET_OFFSET'],
432
+ slots: {
433
+ Root: 'bar.root',
434
+ Leading: 'bar.leading',
435
+ Center: 'bar.center',
436
+ Trailing: 'bar.trailing',
437
+ Seam: 'bar.seam',
438
+ },
439
+ }),
440
+
441
+ bar: componentSchema({
442
+ id: 'https://aeon-ui.com/schemas/bar.json',
443
+ title: 'Bar',
444
+ description: 'Three-zone toolbar layout (leading / center / trailing / seam). Sticky lifecycle uses StickyBar + stickyBarMachine.',
445
+ scope: 'bar',
446
+ parts: ['root', 'leading', 'center', 'trailing', 'seam'],
447
+ states: ['idle'],
448
+ machine: null,
449
+ runtime: 'layout',
450
+ slots: {
451
+ Root: 'bar.root',
452
+ Leading: 'bar.leading',
453
+ Center: 'bar.center',
454
+ Trailing: 'bar.trailing',
455
+ },
456
+ }),
457
+
458
+ nav: componentSchema({
459
+ id: 'https://aeon-ui.com/schemas/nav.json',
460
+ title: 'Nav',
461
+ description: 'Navigational collection via tabsMachine; items project inactive | active | disabled.',
462
+ scope: 'nav',
463
+ parts: ['root', 'item', 'indicator', 'label', 'icon', 'badge'],
464
+ states: ['inactive', 'active', 'disabled'],
465
+ machine: 'tabsMachine',
466
+ runtime: 'useAeonMachine',
467
+ events: ['SELECT'],
468
+ slots: { Root: 'nav.root', Item: 'nav.item', Label: 'nav.label', Icon: 'nav.icon' },
469
+ }),
470
+
471
+ appNav: componentSchema({
472
+ id: 'https://aeon-ui.com/schemas/appNav.json',
473
+ title: 'AppNav',
474
+ description: 'Section + detail stack via appNavMachine — list→detail, settings drill-in.',
475
+ scope: 'appNav',
476
+ parts: ['root', 'rail', 'section', 'stage', 'breadcrumb', 'crumb', 'back'],
477
+ states: ['section'],
478
+ machine: 'appNavMachine',
479
+ runtime: 'useAeonMachine',
480
+ events: ['SET_SECTION', 'PUSH', 'REPLACE', 'POP', 'POP_TO', 'CLEAR', 'RESET'],
481
+ slots: {
482
+ Root: 'appNav.root',
483
+ Rail: 'appNav.rail',
484
+ Stage: 'appNav.stage',
485
+ Back: 'appNav.back',
486
+ },
487
+ }),
488
+
489
+ prompt: componentSchema({
490
+ id: 'https://aeon-ui.com/schemas/prompt.json',
491
+ title: 'Prompt',
492
+ description:
493
+ 'Confirm / permission dialog composed on Dialog. Open/closed via dialogMachine; face via status prop.',
494
+ scope: 'prompt',
495
+ parts: ['root', 'eyebrow', 'meta', 'amount', 'actions', 'primary', 'secondary'],
496
+ states: ['pending', 'confirming', 'dismissed', 'confirmed'],
497
+ machine: 'dialogMachine',
498
+ runtime: 'status prop',
499
+ events: ['OPEN', 'CLOSE', 'TOGGLE'],
500
+ props: {
501
+ status: {
502
+ type: 'string',
503
+ enum: ['pending', 'confirming', 'dismissed', 'confirmed'],
504
+ default: 'pending',
505
+ },
506
+ },
507
+ slots: {
508
+ Root: 'prompt.root',
509
+ Primary: 'prompt.primary',
510
+ Secondary: 'prompt.secondary',
511
+ Actions: 'prompt.actions',
512
+ },
513
+ }),
514
+
515
+ statusBanner: componentSchema({
516
+ id: 'https://aeon-ui.com/schemas/statusBanner.json',
517
+ title: 'StatusBanner',
518
+ description: 'Non-blocking top-of-app notice — tone + optional status props project to data-aeon-state.',
519
+ scope: 'statusBanner',
520
+ parts: ['root', 'copy', 'title', 'body', 'actions'],
521
+ states: ['info', 'success', 'warning', 'danger', 'neutral'],
522
+ machine: null,
523
+ runtime: 'props',
524
+ props: {
525
+ tone: { type: 'string', enum: ['info', 'success', 'warning', 'danger', 'neutral'] },
526
+ status: { type: 'string', description: 'Optional secondary token appended to data-aeon-state' },
527
+ },
528
+ slots: {
529
+ Root: 'statusBanner.root',
530
+ Title: 'statusBanner.title',
531
+ Body: 'statusBanner.body',
532
+ Actions: 'statusBanner.actions',
533
+ },
534
+ }),
535
+
536
+ separator: componentSchema({
537
+ id: 'https://aeon-ui.com/schemas/separator.json',
538
+ title: 'Separator',
539
+ description: 'Static divider — orientation prop only.',
540
+ scope: 'separator',
541
+ parts: ['root'],
542
+ states: ['horizontal', 'vertical'],
543
+ machine: null,
544
+ runtime: 'props',
545
+ slots: { Root: 'separator.root' },
546
+ }),
547
+
548
+ composer: componentSchema({
549
+ id: 'https://aeon-ui.com/schemas/composer.json',
550
+ title: 'Composer',
551
+ description: 'Compose chrome — root state prop idle | sending | disabled | error.',
552
+ scope: 'composer',
553
+ parts: ['root', 'input', 'actions', 'send'],
554
+ states: ['idle', 'sending', 'disabled', 'error'],
555
+ machine: null,
556
+ runtime: 'props',
557
+ slots: { Root: 'composer.root', Input: 'composer.input', Send: 'composer.send' },
558
+ }),
559
+
560
+ thread: componentSchema({
561
+ id: 'https://aeon-ui.com/schemas/thread.json',
562
+ title: 'Thread',
563
+ description: 'Message stream — item state prop mine | theirs | pending | failed.',
564
+ scope: 'thread',
565
+ parts: ['root', 'list', 'item', 'bubble', 'meta', 'day'],
566
+ states: ['mine', 'theirs', 'pending', 'failed'],
567
+ machine: null,
568
+ runtime: 'props',
569
+ slots: { Root: 'thread.root', Item: 'thread.item', Bubble: 'thread.bubble' },
570
+ }),
571
+
572
+ identity: componentSchema({
573
+ id: 'https://aeon-ui.com/schemas/identity.json',
574
+ title: 'Identity',
575
+ description: 'Avatar + title + subtitle strip — presentational layout, not a niche product scope.',
576
+ scope: 'identity',
577
+ parts: ['root', 'avatar', 'title', 'subtitle', 'meta', 'trailing'],
578
+ states: ['idle'],
579
+ machine: null,
580
+ runtime: 'layout',
581
+ props: {
582
+ size: { type: 'string', enum: ['sm', 'md', 'lg'] },
583
+ },
584
+ slots: {
585
+ Root: 'identity.root',
586
+ Avatar: 'identity.avatar',
587
+ Title: 'identity.title',
588
+ Subtitle: 'identity.subtitle',
589
+ Meta: 'identity.meta',
590
+ Trailing: 'identity.trailing',
591
+ },
592
+ }),
593
+
594
+ profileHeader: componentSchema({
595
+ id: 'https://aeon-ui.com/schemas/profileHeader.json',
596
+ title: 'ProfileHeader',
597
+ description: 'Dense account header — identity · metrics · actions. Universal people chrome.',
598
+ scope: 'profileHeader',
599
+ parts: ['root', 'media', 'identity', 'metrics', 'actions', 'body'],
600
+ states: ['idle'],
601
+ machine: null,
602
+ runtime: 'layout',
603
+ slots: {
604
+ Root: 'profileHeader.root',
605
+ Media: 'profileHeader.media',
606
+ Identity: 'profileHeader.identity',
607
+ Metrics: 'profileHeader.metrics',
608
+ Actions: 'profileHeader.actions',
609
+ Body: 'profileHeader.body',
610
+ },
611
+ }),
612
+
613
+ metricStrip: componentSchema({
614
+ id: 'https://aeon-ui.com/schemas/metricStrip.json',
615
+ title: 'MetricStrip',
616
+ description: 'Dense value|label chips — cluster or loose density. Generic stats only.',
617
+ scope: 'metricStrip',
618
+ parts: ['root', 'chip', 'value', 'label'],
619
+ states: ['idle'],
620
+ machine: null,
621
+ runtime: 'layout',
622
+ props: {
623
+ density: { type: 'string', enum: ['cluster', 'loose'] },
624
+ },
625
+ slots: {
626
+ Root: 'metricStrip.root',
627
+ Chip: 'metricStrip.chip',
628
+ Value: 'metricStrip.value',
629
+ Label: 'metricStrip.label',
630
+ },
631
+ }),
632
+
633
+ listRow: componentSchema({
634
+ id: 'https://aeon-ui.com/schemas/listRow.json',
635
+ title: 'ListRow',
636
+ description: 'Settings / menu row — leading · label · trailing. Layout only.',
637
+ scope: 'listRow',
638
+ parts: ['root', 'leading', 'label', 'description', 'trailing'],
639
+ states: ['idle'],
640
+ machine: null,
641
+ runtime: 'layout',
642
+ slots: {
643
+ Root: 'listRow.root',
644
+ Leading: 'listRow.leading',
645
+ Label: 'listRow.label',
646
+ Description: 'listRow.description',
647
+ Trailing: 'listRow.trailing',
648
+ },
649
+ }),
650
+
651
+ entry: componentSchema({
652
+ id: 'https://aeon-ui.com/schemas/entry.json',
653
+ title: 'Entry',
654
+ description:
655
+ 'Universal multi-value card (feed stack or listing split). Root state idle | selected | muted — never invent niche scopes.',
656
+ scope: 'entry',
657
+ parts: [
658
+ 'list',
659
+ 'root',
660
+ 'header',
661
+ 'leading',
662
+ 'heading',
663
+ 'title',
664
+ 'subtitle',
665
+ 'meta',
666
+ 'media',
667
+ 'body',
668
+ 'values',
669
+ 'value',
670
+ 'actions',
671
+ 'footer',
672
+ ],
673
+ states: ['idle', 'selected', 'muted'],
674
+ machine: null,
675
+ runtime: 'props',
676
+ props: {
677
+ state: { type: 'string', enum: ['idle', 'selected', 'muted'], default: 'idle' },
678
+ density: { type: 'string', enum: ['compact', 'cozy'] },
679
+ layout: { type: 'string', enum: ['stack', 'split'] },
680
+ },
681
+ slots: {
682
+ List: 'entry.list',
683
+ Root: 'entry.root',
684
+ Header: 'entry.header',
685
+ Body: 'entry.body',
686
+ Actions: 'entry.actions',
687
+ Footer: 'entry.footer',
688
+ },
689
+ }),
690
+
691
+ conversation: componentSchema({
692
+ id: 'https://aeon-ui.com/schemas/conversation.json',
693
+ title: 'Conversation',
694
+ description:
695
+ 'Inbox / DM list — item state prop idle | unread | selected. Universal messaging chrome.',
696
+ scope: 'conversation',
697
+ parts: ['root', 'item', 'leading', 'body', 'title', 'preview', 'meta', 'badge'],
698
+ states: ['idle', 'unread', 'selected'],
699
+ machine: null,
700
+ runtime: 'props',
701
+ props: {
702
+ state: { type: 'string', enum: ['idle', 'unread', 'selected'], default: 'idle' },
703
+ },
704
+ slots: {
705
+ Root: 'conversation.root',
706
+ Item: 'conversation.item',
707
+ Leading: 'conversation.leading',
708
+ Body: 'conversation.body',
709
+ Title: 'conversation.title',
710
+ Preview: 'conversation.preview',
711
+ Meta: 'conversation.meta',
712
+ Badge: 'conversation.badge',
713
+ },
714
+ }),
292
715
  } as const satisfies Record<string, JsonSchema>
293
716
 
294
717
  export type ComponentSchemaId = keyof typeof componentSchemas
@@ -1,6 +1,44 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import { catalog, componentSchemas } from './components.js'
3
+ import { machineRegistryId } from './machine-id.js'
3
4
  import { AEON_SYSTEM_PROMPT } from './prompts.js'
5
+ import { registryCatalog } from './registry.js'
6
+
7
+ /** Machine-backed COMPONENTS / MACHINES rows that must have a schema entry. */
8
+ const MACHINE_BACKED_SCHEMA_IDS = [
9
+ 'async',
10
+ 'field',
11
+ 'button',
12
+ 'switch',
13
+ 'checkbox',
14
+ 'dialog',
15
+ 'tabs',
16
+ 'accordion',
17
+ 'menu',
18
+ 'popover',
19
+ 'select',
20
+ 'combobox',
21
+ 'toast',
22
+ 'radioGroup',
23
+ 'slider',
24
+ 'stickyBar',
25
+ 'appShell',
26
+ 'content',
27
+ 'nav',
28
+ 'panel',
29
+ 'appNav',
30
+ 'prompt',
31
+ 'tooltip',
32
+ ] as const
33
+
34
+ const LAYOUT_SCHEMA_IDS = [
35
+ 'identity',
36
+ 'profileHeader',
37
+ 'metricStrip',
38
+ 'listRow',
39
+ 'entry',
40
+ 'conversation',
41
+ ] as const
4
42
 
5
43
  describe('@aeon-ui/schemas', () => {
6
44
  it('exposes a catalog of every component schema', () => {
@@ -22,4 +60,49 @@ describe('@aeon-ui/schemas', () => {
22
60
  expect(AEON_SYSTEM_PROMPT).toContain('Build the state machines first')
23
61
  expect(AEON_SYSTEM_PROMPT).toContain('data-aeon-state')
24
62
  })
63
+
64
+ it('derives registry catalog from componentSchemas (no hand list drift)', () => {
65
+ const schemaIds = Object.keys(componentSchemas).sort()
66
+ const registryIds = registryCatalog.components.map((c) => c.id).sort()
67
+ expect(registryIds).toEqual(schemaIds)
68
+ expect(catalog.components.sort()).toEqual(schemaIds)
69
+ })
70
+
71
+ it('covers machine-backed components in the schema catalog', () => {
72
+ for (const id of MACHINE_BACKED_SCHEMA_IDS) {
73
+ expect(componentSchemas).toHaveProperty(id)
74
+ }
75
+ expect(componentSchemas.combobox['x-aeon']).toMatchObject({
76
+ machine: 'popoverMachine',
77
+ runtime: 'useAeonMachine',
78
+ })
79
+ expect(componentSchemas.tooltip['x-aeon']).toMatchObject({
80
+ machine: 'popoverMachine',
81
+ runtime: 'useAeonMachine',
82
+ })
83
+ })
84
+
85
+ it('covers Instant-critical layout schemas', () => {
86
+ for (const id of LAYOUT_SCHEMA_IDS) {
87
+ expect(componentSchemas).toHaveProperty(id)
88
+ }
89
+ expect(componentSchemas.entry['x-aeon']).toMatchObject({
90
+ scope: 'entry',
91
+ runtime: 'props',
92
+ })
93
+ })
94
+
95
+ it('maps machine export names to registry machine file ids', () => {
96
+ expect(machineRegistryId('popoverMachine')).toBe('popover')
97
+ expect(machineRegistryId('createAsyncMachine')).toBe('async')
98
+ expect(machineRegistryId('buttonLifecycleMachine')).toBe('buttonLifecycle')
99
+ expect(machineRegistryId(null)).toBeUndefined()
100
+ })
101
+
102
+ it('points registry machineUrl at the mapped machine id when present', () => {
103
+ const select = registryCatalog.components.find((c) => c.id === 'select')
104
+ expect(select?.machineUrl).toBe('https://aeon-ui.com/registry/machines/popover.json')
105
+ const scroll = registryCatalog.components.find((c) => c.id === 'scroll')
106
+ expect(scroll?.machineUrl).toBeUndefined()
107
+ })
25
108
  })
@@ -23,3 +23,4 @@ export {
23
23
  type AgentEventBridge,
24
24
  } from './agent-bridge.js'
25
25
  export { registryCatalog, REGISTRY_BASE, type RegistryEntry } from './registry.js'
26
+ export { machineRegistryId } from './machine-id.js'
@@ -0,0 +1,7 @@
1
+ /** Map component schema `machine` export names → registry/machines/{id}.json ids. */
2
+ export function machineRegistryId(machine: string | null | undefined): string | undefined {
3
+ if (machine == null) return undefined
4
+ if (machine === 'createAsyncMachine') return 'async'
5
+ if (machine.endsWith('Machine')) return machine.slice(0, -'Machine'.length)
6
+ return machine
7
+ }
@@ -15,8 +15,8 @@ You are building UI with **Aeon UI**, a React component library powered by finit
15
15
  ## Non-negotiable rules
16
16
  1. Do not duplicate open/checked/lifecycle in \`useState\` when a machine exists in \`@aeon-ui/primitives\`.
17
17
  2. **Button \`status\` is a prop** (\`idle|pending|success|failure|disabled\`). Optional: drive it from \`buttonLifecycleMachine\` via \`useAeonMachine\` in app code.
18
- 3. **Select / Menu / Popover** share \`popoverMachine\` — never fork another open/closed chart.
19
- 4. **Combobox** and **Tooltip** use React state today — do not add floating machines without the component checklist.
18
+ 3. **Select / Menu / Popover / Combobox / Tooltip** share \`popoverMachine\` for open/closed — never fork another open/closed chart. Combobox keeps query/highlight in React; Tooltip keeps openDelay/closeDelay/touchDuration in React.
19
+ 4. **PinInput** digit buffer is React-local today — do not invent a pin machine without the component checklist.
20
20
  5. **Scroll** uses \`getScrollSnapshot()\` — no XState machine.
21
21
  6. If a machine lists N stable states, the UI must represent all N (totality).
22
22
  7. Prefer \`@aeon-ui/react\` (headless) or \`@aeon-ui/ui\` (styled). Relative TS imports use \`.js\` extensions inside the Aeon monorepo only.
@@ -58,13 +58,34 @@ Rule: Pending must set aria-busy. Do not use disabled alone to mean "saving".`,
58
58
  States: open | closed. Events: OPEN, CLOSE, TOGGLE.
59
59
  Rule: Backdrop click and Escape are CLOSE transitions. Title/Description are required for a11y when open.`,
60
60
 
61
- popoverFamily: `## Popover family (Popover, Menu, Select open state)
61
+ popoverFamily: `## Popover family (Popover, Menu, Select, Combobox, Tooltip open state)
62
62
  Machine: popoverMachine — states open | closed.
63
- Rule: Reuse this chart. Never create selectOpenMachine / menuOpenMachine forks.`,
63
+ Rule: Reuse this chart. Never create selectOpenMachine / menuOpenMachine / comboboxOpenMachine / tooltipOpenMachine forks.
64
+ Combobox: query + highlight stay React-local; open/closed must still be popoverMachine.
65
+ Tooltip: openDelay / closeDelay / touchDuration stay React-local; open/closed must still be popoverMachine.`,
64
66
 
65
67
  toggle: `## Switch / Checkbox
66
68
  Machine: toggleMachine — checked | unchecked. Events: TOGGLE, CHECK, UNCHECK.
67
69
  Rule: Hidden input stays in sync with snapshot for forms.`,
70
+
71
+ slider: `## Slider statechart
72
+ Machine: sliderMachine — idle | dragging; value/min/max/step in context.
73
+ Events: SET_VALUE, STEP, POINTER_DOWN, POINTER_UP, HOME, END.
74
+ Rule: Disabled blocks SET_VALUE / drag; project idle vs dragging on data-aeon-state.`,
75
+
76
+ contentRegion: `## Content region statechart
77
+ Machine: contentRegionMachine — idle → pending → (empty | error | ready) → loadingMore | success.
78
+ Events: LOAD, RESOLVE, REJECT, RETRY, LOAD_MORE, MORE_DONE, SUCCEED, RESET.
79
+ Rule: Every stable state has a slot. Pair with Async for fetch; do not hide empty/error behind a spinner.`,
80
+
81
+ toast: `## Toast item statechart
82
+ Machine: toastMachine — hidden | visible. Events: SHOW, HIDE, DISMISS.
83
+ Rule: Provider list orchestration may use React; each toast item uses the machine.`,
84
+
85
+ stickyBar: `## StickyBar statechart
86
+ Machine: stickyBarMachine — floating | docked | collapsed | hidden.
87
+ Events: DOCK, FLOAT, COLLAPSE, EXPAND, HIDE, SHOW, SET_OFFSET.
88
+ Rule: DOM scope is bar (bar anatomy zones). Do not invent a parallel sticky useState.`,
68
89
  } as const
69
90
 
70
91
  /** Checkout-form example used on the landing "AI playground" proof. */