social components inlay-proto.up.railway.app/
atproto components sdui
at main 187 lines 4.8 kB view raw
1// Minimal helper to create elements from generated lexicon modules 2// 3// Usage: 4// import { Stack } from "@/generated/org/design"; 5// import { Post } from "@/generated/com/example"; 6// import { $, serializeTree, deserializeTree } from "@inlay/core"; 7// 8// $(Stack, { gap: "medium" }, 9// $(Post, { uri: 'at://...' }), 10// $(Post, { uri: 'at://...' }) 11// ) 12// 13// Serialization: 14// serializeTree(tree) 15// deserializeTree(tree) 16// 17// Element types are NSIDs (e.g., "com.example.Post") 18 19import type { l } from "@atproto/lex"; 20 21import { ensureValidNsid } from "@atproto/syntax"; 22import { 23 BRAND, 24 type Element, 25 type LexiconComponent, 26 createElement, 27 keyStaticChildren, 28} from "./element.ts"; 29 30export { type Element, isValidElement } from "./element.ts"; 31export { BRAND } from "./element.ts"; 32 33// --- Base types --- 34 35type LexiconModule = LexiconComponent & { 36 readonly main: l.Procedure; 37}; 38 39type Child = Element | string | number | boolean | null | undefined | Child[]; 40 41// --- JSX support --- 42 43type Props<M extends LexiconModule> = Omit< 44 l.InferMethodInputBody<M["main"]>, 45 "children" 46> & { 47 children?: Child; 48}; 49 50export type Component<M extends LexiconModule> = M & 51 ((props: Props<M>) => Element); 52 53export function jslex<T>(namespace: T): { 54 [K in keyof T]: T[K] extends LexiconModule ? Component<T[K]> : T[K]; 55} { 56 return namespace as any; 57} 58 59// --- $ function --- 60 61type Config<M extends LexiconModule> = Omit< 62 l.InferMethodInputBody<M["main"]>, 63 "children" 64> & { key?: string }; 65 66// Overload: typed lexicon module 67export function $<M extends LexiconModule>( 68 mod: M, 69 config?: Config<M>, 70 ...children: Child[] 71): Element; 72 73// Overload: string NSID (untyped) 74export function $( 75 nsid: string, 76 config?: l.LexMap & { key?: string }, 77 ...children: Child[] 78): Element; 79 80// Implementation 81export function $( 82 modOrNsid: LexiconModule | string, 83 config?: l.LexMap & { key?: string }, 84 ...children: Child[] 85): Element { 86 const nsid = 87 typeof modOrNsid === "string" 88 ? (ensureValidNsid(modOrNsid), modOrNsid) 89 : modOrNsid.$nsid; 90 const { key, ...rest } = config ?? {}; 91 const props = 92 children.length > 0 93 ? { ...rest, children: keyStaticChildren(children) } 94 : rest; 95 return createElement(nsid, key, props); 96} 97 98export function walkTree<T>( 99 tree: T, 100 mapObject: ( 101 obj: Record<string, unknown>, 102 walk: (v: unknown) => unknown 103 ) => unknown 104): T { 105 return walk(tree) as T; 106 107 function walk(value: unknown): unknown { 108 if (value == null || typeof value !== "object") { 109 return value; 110 } 111 if (Array.isArray(value)) { 112 return value.map(walk); 113 } 114 return mapObject(value as Record<string, unknown>, walk); 115 } 116} 117 118export type ElementVisitor = (element: Element) => unknown; 119 120export function serializeTree<T>(tree: T, visitor?: ElementVisitor): T { 121 return walkTree(tree, (obj, walk) => { 122 if (visitor && obj.$ === BRAND) { 123 const result = visitor(obj as Element); 124 if (result !== obj) { 125 return walk(result); 126 } 127 } 128 const out: Record<string, unknown> = {}; 129 for (const [k, v] of Object.entries(obj)) { 130 if (v === BRAND) { 131 out[k] = "$"; 132 } else if (typeof v === "string" && v.startsWith("$")) { 133 out[k] = "$" + v; 134 } else { 135 out[k] = walk(v); 136 } 137 } 138 return out; 139 }); 140} 141 142export function deserializeTree<T>(tree: T, visitor?: ElementVisitor): T { 143 return walkTree(tree, (obj, walk) => { 144 const out: Record<string, unknown> = {}; 145 for (const [k, v] of Object.entries(obj)) { 146 if (v === "$") { 147 out[k] = BRAND; 148 } else if (typeof v === "string" && v.startsWith("$$")) { 149 out[k] = v.slice(1); 150 } else { 151 out[k] = walk(v); 152 } 153 } 154 if (visitor && out.$ === BRAND) { 155 const result = visitor(out as Element); 156 if (result !== out) { 157 return result; 158 } 159 } 160 return out; 161 }); 162} 163 164/** 165 * Walk a value tree resolving Binding elements via a caller-provided resolver. 166 * Recurses into plain objects and arrays. Non-Binding elements are opaque — 167 * they are NOT recursed into (their props are rendered separately by renderNode). 168 */ 169export function resolveBindings<T>( 170 tree: T, 171 resolve: (path: string[]) => unknown 172): T { 173 return walkTree(tree, (obj, walk) => { 174 if (obj.$ === BRAND) { 175 const el = obj as Element; 176 if (el.type === "at.inlay.Binding") { 177 const path = (el.props as Record<string, unknown> | undefined)?.path; 178 if (!Array.isArray(path)) throw new Error("Binding missing path"); 179 return resolve(path as string[]); 180 } 181 return obj; // non-Binding element: opaque 182 } 183 const out: Record<string, unknown> = {}; 184 for (const [k, v] of Object.entries(obj)) out[k] = walk(v); 185 return out; 186 }); 187}