social components
inlay-proto.up.railway.app/
atproto
components
sdui
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}