schema.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import { defineSchema } from "@json-render/core";
  2. /**
  3. * The schema for @json-render/react-native
  4. *
  5. * Defines:
  6. * - Spec: A flat tree of elements with keys, types, props, and children references
  7. * - Catalog: Components with props schemas, and optional actions
  8. */
  9. export const schema = defineSchema(
  10. (s) => ({
  11. // What the AI-generated SPEC looks like
  12. spec: s.object({
  13. /** Root element key */
  14. root: s.string(),
  15. /** Flat map of elements by key */
  16. elements: s.record(
  17. s.object({
  18. /** Component type from catalog */
  19. type: s.ref("catalog.components"),
  20. /** Component props */
  21. props: s.propsOf("catalog.components"),
  22. /** Child element keys (flat reference) */
  23. children: s.array(s.string()),
  24. /** Visibility condition */
  25. visible: s.any(),
  26. }),
  27. ),
  28. }),
  29. // What the CATALOG must provide
  30. catalog: s.object({
  31. /** Component definitions */
  32. components: s.map({
  33. /** Zod schema for component props */
  34. props: s.zod(),
  35. /** Slots for this component. Use ['default'] for children, or named slots like ['header', 'footer'] */
  36. slots: s.array(s.string()),
  37. /** Description for AI generation hints */
  38. description: s.string(),
  39. /** Example prop values used in prompt examples (auto-generated from Zod schema if omitted) */
  40. example: s.any(),
  41. }),
  42. /** Action definitions (optional) */
  43. actions: s.map({
  44. /** Zod schema for action params */
  45. params: s.zod(),
  46. /** Description for AI generation hints */
  47. description: s.string(),
  48. }),
  49. }),
  50. }),
  51. {
  52. defaultRules: [
  53. // Layout patterns
  54. "FIXED BOTTOM BAR PATTERN: When building a screen with a fixed header and/or fixed bottom tab bar, the outermost vertical layout component must have flex:1 so it fills the screen. The scrollable content area must also have flex:1. Structure: screen wrapper > vertical layout(flex:1, gap:0) > [header, content wrapper(flex:1) > [scroll container(...)], bottom-tabs]. Both the outer layout AND the content wrapper need flex:1. ONLY use components from the AVAILABLE COMPONENTS list.",
  55. "NEVER place a bottom tab bar or fixed footer inside a scroll container. It must be a sibling AFTER the flex:1 container that holds the scroll content.",
  56. // Element integrity
  57. "CRITICAL INTEGRITY CHECK: Before outputting ANY element that references children, you MUST have already output (or will output) each child as its own element. If an element has children: ['a', 'b'], then elements 'a' and 'b' MUST exist. A missing child element causes that entire branch of the UI to be invisible.",
  58. "SELF-CHECK: After generating all elements, mentally walk the tree from root. Every key in every children array must resolve to a defined element. If you find a gap, output the missing element immediately.",
  59. 'When building repeating content backed by a state array (e.g. todos, posts, cart items), use the "repeat" field on a container element from the AVAILABLE COMPONENTS list. Example: { "type": "<ContainerComponent>", "props": { "gap": 8 }, "repeat": { "statePath": "/todos", "key": "id" }, "children": ["todo-item"] }. Inside repeated children, use { "$item": "field" } to read a field from the current item, and { "$index": true } for the current array index. For two-way binding to an item field use { "$bindItem": "completed" }. Do NOT hardcode individual elements for each array item.',
  60. // Visible field placement
  61. 'CRITICAL: The "visible" field goes on the ELEMENT object, NOT inside "props". Correct: {"type":"<ComponentName>","props":{},"visible":{"$state":"/activeTab","eq":"home"},"children":[...]}. WRONG: {"type":"<ComponentName>","props":{},"visible":{...},"children":[...]} with visible inside props.',
  62. // Tab navigation pattern
  63. "TAB NAVIGATION PATTERN: When building a UI with multiple tabs, use a pressable/tappable component + setState action + visible conditions to make tabs functional. ONLY use components from the AVAILABLE COMPONENTS list.",
  64. 'Each tab button should be a pressable component wrapping its icon/label children, with action "setState" and actionParams { "statePath": "/activeTab", "value": "tabName" }.',
  65. 'Each tab\'s content section should have a visible condition: { "$state": "/activeTab", "eq": "tabName" }.',
  66. "The first tab's content should NOT have a visible condition (so it shows by default when no tab is selected yet). All other tabs MUST have a visible condition.",
  67. // Tab active state highlighting (using dynamic props)
  68. "TAB ACTIVE STYLING: Use $cond dynamic props on icon elements inside each tab button so a single icon changes appearance based on the active tab.",
  69. ' - For the icon name: { "$cond": { "$state": "/activeTab", "eq": "thisTabName" }, "$then": "home", "$else": "home-outline" }',
  70. ' - For the icon color: { "$cond": { "$state": "/activeTab", "eq": "thisTabName" }, "$then": "#007AFF", "$else": "#8E8E93" }',
  71. " - For labels, use $cond on the color prop similarly.",
  72. ' - For the FIRST/DEFAULT tab, use { "$cond": [{ "$state": "/activeTab", "eq": "thisTabName" }], "$then": "#007AFF", "$else": "#8E8E93" } so it appears active before any tab is tapped. When no tab is selected yet, include a default tab with no visible condition.',
  73. // Push/pop screen navigation (all screens in one spec)
  74. 'SCREEN NAVIGATION: Use a pressable component with action "push" and actionParams { "screen": "screenName" } to navigate to a new screen. Use action "pop" to go back. All screens must be defined in the SAME spec. ONLY use components from the AVAILABLE COMPONENTS list.',
  75. 'Each screen section uses a visible condition on /currentScreen: { "$state": "/currentScreen", "eq": "screenName" }. The default/home screen should show by default (no visible condition) and all other screens should have the appropriate visible condition.',
  76. "push automatically maintains a /navStack in the state model so pop always returns to the previous screen.",
  77. 'Include a back button on pushed screens using action "pop". Example: pressable(action:"pop") > row layout > back icon + back label. ONLY use components from the AVAILABLE COMPONENTS list.',
  78. "Use push/pop for drill-down flows: tapping a list item to see details, opening a profile, etc. Use setState + visible conditions for tab switching within a screen.",
  79. 'Example: A list screen with items that push to detail: a pressable component with action:"push" and actionParams:{screen:"detail"} wrapping each list item. The detail screen section has visible:{"$state":"/currentScreen","eq":"detail"} and contains a back button with action:"pop". ONLY use components from the AVAILABLE COMPONENTS list.',
  80. ],
  81. },
  82. );
  83. /**
  84. * Type for the React Native schema
  85. */
  86. export type ReactNativeSchema = typeof schema;
  87. /**
  88. * Infer the spec type from a catalog
  89. */
  90. export type ReactNativeSpec<TCatalog> = typeof schema extends {
  91. createCatalog: (catalog: TCatalog) => { _specType: infer S };
  92. }
  93. ? S
  94. : never;
  95. // Backward compatibility aliases
  96. /** @deprecated Use `schema` instead */
  97. export const elementTreeSchema = schema;
  98. /** @deprecated Use `ReactNativeSchema` instead */
  99. export type ElementTreeSchema = ReactNativeSchema;
  100. /** @deprecated Use `ReactNativeSpec` instead */
  101. export type ElementTreeSpec<T> = ReactNativeSpec<T>;