Skip to main content

gpui_rsx/
lib.rs

1//! # gpui-rsx
2//!
3//! A procedural macro that brings JSX-like syntax to [GPUI], making UI code more concise
4//! and readable while generating **zero-overhead** native GPUI method chains at compile time.
5//!
6//! [GPUI]: https://www.gpui.rs/
7//!
8//! ## At a Glance
9//!
10//! ```ignore
11//! use gpui::*;
12//! use gpui_rsx::rsx;
13//!
14//! // Before — verbose GPUI method chain
15//! div()
16//!     .flex()
17//!     .flex_col()
18//!     .gap(px(16.0))
19//!     .p(px(16.0))
20//!     .bg(rgb(0x3b82f6))
21//!     .child(div().text_xl().font_bold().child("Hello GPUI"))
22//!     .child(
23//!         div()
24//!             .cursor_pointer()
25//!             .id("btn")
26//!             .on_click(cx.listener(|_, _, _window, cx| cx.notify()))
27//!             .child("Click me"),
28//!     )
29//!
30//! // After — concise RSX (~50% less code)
31//! rsx! {
32//!     <div class="flex flex-col gap-4 p-4 bg-blue-500">
33//!         <h1 class="text-xl font-bold">{"Hello GPUI"}</h1>
34//!         <button
35//!             cursor_pointer
36//!             onClick={cx.listener(|_, _, _window, cx| cx.notify())}
37//!         >
38//!             {"Click me"}
39//!         </button>
40//!     </div>
41//! }
42//! ```
43//!
44//! The two snippets produce **identical** compiled output.
45//!
46//! ## Features
47//!
48//! | Feature | Description |
49//! |---------|-------------|
50//! | HTML-like tags | `<div>`, `<span>`, `<button>`, … → all map to `div()` |
51//! | Boolean attributes | `<div flex flex_col />` → `.flex().flex_col()` |
52//! | Value attributes | `<div gap={px(4.0)} />` → `.gap(px(4.0))` |
53//! | `class` (static) | Tailwind-like string, parsed at compile time |
54//! | `class` (dynamic) | Runtime expression, full Tailwind palette + common utilities |
55//! | Full color palette | 242 Tailwind colors + arbitrary hex (`bg-[#ff0000]`) |
56//! | Fragments | `<>...</>` — returns `vec![...]` |
57//! | For-loop sugar | `{for item in iter { ... }}` |
58//! | Spread | `{...iterator}` |
59//! | Conditional | `when` / `whenSome` attributes |
60//! | Styled defaults | `<h1 styled>` injects sensible tag defaults |
61//! | camelCase mapping | `onClick` → `.on_click()`, `fontSize` → `.text_size()` |
62//! | `key` (loop ID) | Composite auto-ID for stateful elements in loops |
63//!
64//! ## Syntax Reference
65//!
66//! ### Basic Elements
67//!
68//! Any HTML tag maps to `div()`. Self-closing and pair tags are both valid:
69//!
70//! ```ignore
71//! rsx! { <div /> }                    // div()
72//! rsx! { <span></span> }              // div()
73//! rsx! { <button>{"OK"}</button> }    // div().child("OK")
74//! ```
75//!
76//! ### Fragment
77//!
78//! Return multiple root elements without a wrapper. Expands to `vec![...]`:
79//!
80//! ```ignore
81//! rsx! {
82//!     <>
83//!         <div>{"First"}</div>
84//!         <div>{"Second"}</div>
85//!     </>
86//! }
87//! // → vec![div().child("First"), div().child("Second")]
88//! ```
89//!
90//! ### Boolean Attributes (Flags)
91//!
92//! Bare attribute names become zero-argument method calls:
93//!
94//! ```ignore
95//! rsx! { <div flex flex_col items_center /> }
96//! // → div().flex().flex_col().items_center()
97//! ```
98//!
99//! ### Value Attributes
100//!
101//! `name={expr}` passes the expression as the method argument:
102//!
103//! ```ignore
104//! rsx! { <div gap={px(16.0)} bg={rgb(0x3b82f6)} opacity={0.8} /> }
105//! // → div().gap(px(16.0)).bg(rgb(0x3b82f6)).opacity(0.8)
106//! ```
107//!
108//! ### The `class` Attribute — Static (Compile-time, Recommended)
109//!
110//! A Tailwind-inspired string that expands to method calls at compile time.
111//! **All Tailwind utilities are supported; there is no runtime cost:**
112//!
113//! ```ignore
114//! rsx! { <div class="flex flex-col gap-4 p-4 bg-blue-500 text-white rounded-md" /> }
115//! // → div().flex().flex_col().gap(px(4.0)).p(px(4.0)).bg(rgb(0x3b82f6))
116//! //         .text_color(rgb(0xffffff)).rounded_md()
117//! ```
118//!
119//! #### Supported class patterns
120//!
121//! **Layout:** `flex`, `flex-col`, `flex-row`, `flex-1`, `flex-wrap`, `flex-none`,
122//! `block`, `grid`, `hidden`, `absolute`, `relative`
123//!
124//! **Alignment:** `items-center`, `items-start`, `items-end`, `justify-center`,
125//! `justify-between`, `justify-start`, `justify-end`, `justify-around`,
126//! `content-center`, `content-between`, …
127//!
128//! **Spacing** (numeric value → `px(n.0)`):
129//! `gap-4` → `.gap(px(4.0))`, `p-4`, `px-4`, `py-4`, `pt-4`, `pb-4`, `pl-4`, `pr-4`,
130//! `m-4`, `mx-4`, `my-4`, `mt-4`, `mb-4`, fractional `p-0.5` → `.p(px(0.5))`
131//!
132//! **Sizing:** `w-full`, `h-full`, `size-full`, `w-64`, `h-32`
133//!
134//! **Text:** `text-xs`, `text-sm`, `text-base`, `text-lg`, `text-xl`, `text-2xl`,
135//! `text-3xl`, `font-bold`, `italic`, `underline`, `truncate`, `text-left`, `text-center`
136//!
137//! **Border:** `border` → `.border_1()`, `border-2`, `rounded-sm`, `rounded-md`,
138//! `rounded-lg`, `rounded-xl`, `rounded-full`, `rounded-none`
139//!
140//! **Colors (full Tailwind palette):**
141//! `text-red-500` → `.text_color(rgb(0xef4444))`,
142//! `bg-blue-600` → `.bg(rgb(0x2563eb))`,
143//! `border-green-500` → `.border_color(rgb(0x22c55e))`
144//!
145//! Supported families: `slate`, `gray`, `zinc`, `neutral`, `stone`, `red`, `orange`,
146//! `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`,
147//! `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose` (shades 50–950) +
148//! `black`, `white`
149//!
150//! **Arbitrary hex (6-digit and 3-digit):**
151//! `bg-[#ff0000]` → `.bg(rgb(0xff0000))`,
152//! `text-[#f00]` → `.text_color(rgb(0xff0000))`
153//!
154//! ### The `class` Attribute — Dynamic (Runtime)
155//!
156//! When `class` receives an expression, a runtime matcher is generated.
157//! Supported classes: common layout/spacing/typography utilities, the full Tailwind
158//! color palette, arbitrary hex colors (`bg-[#ff0000]`, `text-[#f00]`), and arbitrary
159//! numeric values for spacing/sizing/opacity via prefix fallback. Truly
160//! unsupported classes (e.g. Tailwind variants, unknown utilities) are silently ignored
161//! in release and print a warning in debug builds.
162//!
163//! ```ignore
164//! let active = true;
165//! rsx! { <div class={if active { "flex gap-4" } else { "block" }} /> }
166//! ```
167//!
168//! Prefer static strings or the `when` attribute instead:
169//!
170//! ```ignore
171//! // ✅ static literal — compile-time, all classes work
172//! rsx! { <div class="flex gap-4" /> }
173//!
174//! // ✅ conditional literal — still static
175//! let cls = if active { "flex gap-4" } else { "block" };
176//! rsx! { <div class={cls} /> }
177//!
178//! // ✅ when attribute — compile-time, fully flexible
179//! rsx! { <div when={(active, |el| el.flex().gap(px(4.0)))} /> }
180//!
181//! // ⚠️ dynamic expression — runtime parser, narrower coverage than static literals
182//! rsx! { <div class={format!("gap-{} bg-[#f00]", spacing)} /> }
183//! ```
184//!
185//! ### Event Handling
186//!
187//! Event attributes (camelCase or snake_case) are mapped to GPUI listeners.
188//! Elements with event handlers automatically receive a deterministic `.id()`.
189//!
190//! ```ignore
191//! rsx! {
192//!     <button onClick={cx.listener(|view, _, _window, cx| {
193//!         view.count += 1;
194//!         cx.notify();
195//!     })}>
196//!         {"Increment"}
197//!     </button>
198//! }
199//! // → div().id("src/main.rs::__rsx_button_L42C8").on_click(cx.listener(...)).child("Increment")
200//! ```
201//!
202//! #### `key` — Unique IDs for stateful elements in loops
203//!
204//! `key={expr}` is a **macro-level** attribute consumed at compile time; it never
205//! becomes a `.key()` method call on the GPUI element.
206//!
207//! **`key` only takes effect when the element already needs an `.id()`** (i.e. it
208//! carries `onClick`, `onHover`, `onDrag`, `tooltip`, `focusable`, `overflowScroll`,
209//! `trackScroll`, or another attribute that requires a stateful element).
210//! On elements without any stateful attributes, `key` is silently ignored and no
211//! `.id()` is injected — the element stays a plain `Div`.
212//!
213//! | Situation | Result |
214//! |-----------|--------|
215//! | stateful attrs + `key` | composite ID: auto-prefix + key (runtime) |
216//! | stateful attrs, no `key` | pure source-location auto ID (compile-time) |
217//! | no stateful attrs + `key` | **`key` ignored**, no `.id()` injected |
218//! | explicit `id` | always used, `key` has no effect |
219//!
220//! **Loop safety:** Inside a `{for ...}` loop, every iteration shares the same source
221//! location. The macro **emits a compile error** when a stateful element is found inside
222//! a loop without an explicit `id` or `key`:
223//!
224//! ```ignore
225//! // ❌ compile error — all <li> would share the same auto ID
226//! {for item in &self.items { <li onClick={handler}>{item}</li> }}
227//!
228//! // ✅ key produces a unique ID per iteration
229//! rsx! {
230//!     <ul>
231//!         {for item in &self.items {
232//!             <li key={item.id} onClick={handler}>{item.name.clone()}</li>
233//!         }}
234//!     </ul>
235//! }
236//! // → div().id(format!("src/main.rs::__rsx_li_L42C8_{}", item.id)).on_click(handler)…
237//! //   e.g. "src/main.rs::__rsx_li_L42C8_1", "src/main.rs::__rsx_li_L42C8_2", …
238//! ```
239//!
240//! `key` accepts any type that implements `Display` (integers, `&str`, UUIDs, …).
241//! For a fully stable custom ID that survives refactors, use the `id` attribute instead.
242//!
243//! | Attribute | GPUI method |
244//! |-----------|-------------|
245//! | `onClick` / `on_click` | `.on_click(h)` |
246//! | `onMouseDown` / `on_mouse_down` | `.on_mouse_down(button, h)` |
247//! | `onMouseUp` / `on_mouse_up` | `.on_mouse_up(button, h)` |
248//! | `onMouseMove` / `on_mouse_move` | `.on_mouse_move(h)` |
249//! | `onMouseDownOut` / `on_mouse_down_out` | `.on_mouse_down_out(h)` |
250//! | `onMouseUpOut` / `on_mouse_up_out` | `.on_mouse_up_out(button, h)` |
251//! | `onAnyMouseDown` / `on_any_mouse_down` | `.on_any_mouse_down(h)` |
252//! | `onAnyMouseUp` / `on_any_mouse_up` | `.on_any_mouse_up(h)` |
253//! | `onKeyDown` / `on_key_down` | `.on_key_down(h)` |
254//! | `onKeyUp` / `on_key_up` | `.on_key_up(h)` |
255//! | `onModifiersChanged` / `on_modifiers_changed` | `.on_modifiers_changed(h)` |
256//! | `onHover` / `on_hover` | `.on_hover(h)` |
257//! | `onScrollWheel` / `on_scroll_wheel` | `.on_scroll_wheel(h)` |
258//! | `onDrag` / `on_drag` | `.on_drag(value, constructor)` |
259//! | `onDragMove` / `on_drag_move` | `.on_drag_move(h)` |
260//! | `onDrop` / `on_drop` | `.on_drop(h)` |
261//! | `onAction` / `on_action` | `.on_action(h)` |
262//! | `onBoxedAction` / `on_boxed_action` | `.on_boxed_action(action, h)` |
263//! | `captureAnyMouseDown` / `capture_any_mouse_down` | `.capture_any_mouse_down(h)` |
264//! | `captureAnyMouseUp` / `capture_any_mouse_up` | `.capture_any_mouse_up(h)` |
265//! | `captureKeyDown` / `capture_key_down` | `.capture_key_down(h)` |
266//! | `captureKeyUp` / `capture_key_up` | `.capture_key_up(h)` |
267//! | `captureAction` / `capture_action` | `.capture_action(h)` |
268//!
269//! Multi-argument GPUI methods use tuple syntax in RSX, e.g.
270//! `onMouseDown={(MouseButton::Left, handler)}`.
271//!
272//! ### Expressions and Children
273//!
274//! ```ignore
275//! rsx! {
276//!     <div>
277//!         {format!("Count: {}", self.count)}   // any expression
278//!         {self.render_child()}                 // method returning IntoElement
279//!         {if self.show {
280//!             rsx! { <span>{"Visible"}</span> }
281//!         } else {
282//!             rsx! { <span>{"Hidden"}</span> }
283//!         }}
284//!     </div>
285//! }
286//! ```
287//!
288//! Two or more consecutive expressions are batched into a single `.children([...])`
289//! call (stack-allocated array, zero heap allocation):
290//!
291//! ```ignore
292//! rsx! { <div>{"a"}{"b"}{"c"}</div> }
293//! // → div().children(["a", "b", "c"])
294//! ```
295//!
296//! ### For-loop Syntax Sugar
297//!
298//! ```ignore
299//! rsx! {
300//!     <ul>
301//!         {for item in &self.items {
302//!             <li>{item.name.clone()}</li>
303//!         }}
304//!     </ul>
305//! }
306//! // → div().children((&self.items).into_iter().map(|item| {
307//! //       div().child(item.name.clone())
308//! //   }))
309//! ```
310//!
311//! ### Spread Syntax
312//!
313//! ```ignore
314//! rsx! {
315//!     <div>
316//!         {...self.items.iter().map(|i| rsx! { <span>{i}</span> })}
317//!     </div>
318//! }
319//! ```
320//!
321//! ### Conditional Attributes: `when` and `whenSome`
322//!
323//! Apply style transformations based on a runtime condition without leaving compile-time
324//! safety:
325//!
326//! ```ignore
327//! rsx! {
328//!     <button
329//!         class="px-4 py-2 rounded-md"
330//!         when={(is_selected, |el| el.bg(rgb(0x3b82f6)).text_color(rgb(0xffffff)))}
331//!         when={(is_disabled, |el| el.opacity(0.5))}
332//!         whenSome={(custom_color, |el, c| el.bg(rgb(c)))}
333//!     >
334//!         {"Button"}
335//!     </button>
336//! }
337//! ```
338//!
339//! ### The `styled` Flag — Semantic Tag Defaults
340//!
341//! Adding `styled` injects sensible default classes for the tag name, applied before
342//! any user-provided attributes:
343//!
344//! ```ignore
345//! rsx! { <h1 styled>{"Title"}</h1> }
346//! // → div().text_3xl().font_bold().child("Title")
347//!
348//! rsx! { <button styled onClick={handler}>{"OK"}</button> }
349//! // → div().cursor_pointer().id("…").on_click(handler).child("OK")
350//! ```
351//!
352//! | Tag | Default classes |
353//! |-----|----------------|
354//! | `h1` | `text-3xl font-bold` |
355//! | `h2` | `text-2xl font-bold` |
356//! | `h3` | `text-xl font-bold` |
357//! | `h4` | `text-lg font-bold` |
358//! | `h5` | `text-base font-bold` |
359//! | `h6` | `text-sm font-bold` |
360//! | `button`, `a` | `cursor-pointer` |
361//! | `input`, `textarea` | `px-2 py-1` |
362//! | `ul`, `ol` | `flex flex-col` |
363//! | `li` | `flex items-center` |
364//! | `p` | `text-base` |
365//! | `label` | `text-sm` |
366//! | `form` | `flex flex-col gap-4` |
367//!
368//! ### Attribute Mapping Reference
369//!
370//! camelCase names are automatically converted to snake_case GPUI methods. Attributes
371//! not in this table are passed through unchanged (e.g., `bg={color}` → `.bg(color)`).
372//!
373//! | RSX attribute | GPUI method |
374//! |---------------|-------------|
375//! | `opacity` | `.opacity()` |
376//! | `visible` / `invisible` | `.visible()` / `.invisible()` |
377//! | `width` / `height` | `.w()` / `.h()` |
378//! | `minWidth` / `maxWidth` | `.min_w()` / `.max_w()` |
379//! | `minHeight` / `maxHeight` | `.min_h()` / `.max_h()` |
380//! | `gapX` / `gapY` | `.gap_x()` / `.gap_y()` |
381//! | `flexBasis` | `.flex_basis()` |
382//! | `flexGrow` / `flexShrink` (flags) | `.flex_grow()` / `.flex_shrink()` |
383//! | `fontSize` | `.text_size()` |
384//! | `lineHeight` | `.line_height()` |
385//! | `fontWeight` | `.font_weight()` |
386//! | `textAlign` | `.text_align()` |
387//! | `borderTop` / `borderBottom` | `.border_t(value)` / `.border_b(value)` |
388//! | `borderLeft` / `borderRight` | `.border_l(value)` / `.border_r(value)` |
389//! | `border_t` / `border_b` / `border_l` / `border_r` (flags) | `.border_t_1()` / `.border_b_1()` / `.border_l_1()` / `.border_r_1()` |
390//! | `roundedTop` / `roundedBottom` | `.rounded_t()` / `.rounded_b()` |
391//! | `roundedTopLeft` / `roundedTopRight` | `.rounded_tl()` / `.rounded_tr()` |
392//! | `roundedBottomLeft` / `roundedBottomRight` | `.rounded_bl()` / `.rounded_br()` |
393//! | `boxShadow` | `.shadow()` |
394//! | `inset` | `.inset()` |
395//!
396//! ## Auto ID Injection
397//!
398//! Elements that require a stateful identity (`onClick`, `onHover`, `onDrag`, `tooltip`,
399//! `focusable`, `overflowScroll`, `trackScroll`, and static `overflow-scroll` classes)
400//! automatically receive a deterministic `.id()` derived from the element's source
401//! location. The ID is chosen by the following priority:
402//!
403//! 1. **Explicit `id`** — always wins, `key` is ignored.
404//! 2. **`key` present** (and element is stateful) — composite ID at runtime:
405//!    `format!(concat!(file!(), "::{prefix}_{}"), key_expr)`
406//! 3. **No `key`** (and element is stateful) — pure compile-time source-location ID:
407//!    `concat!(file!(), "::", "__rsx_{tag}_L{line}C{col}")`
408//! 4. **Not stateful** — no `.id()` injected; `key` is silently ignored.
409//!
410//! ```text
411//! // Format (no key): concat!(file!(), "::", "__rsx_{tag}_L{line}C{col}")
412//! // Example:         "src/views/counter.rs::__rsx_button_L42C8"
413//! //
414//! // Format (key):    format!(concat!(file!(), "::__rsx_{tag}_L{line}C{col}_{}"), key)
415//! // Example:         "src/views/list.rs::__rsx_li_L55C12_42"
416//! ```
417//!
418//! The source-location ID is stable across incremental rebuilds as long as the
419//! element's position in the file does not change. For IDs that must survive
420//! refactors, use the explicit `id` attribute.
421//!
422//! > **Note on style attributes:** `hover`, `active`, `focus`, and `group` are
423//! > *`Styled` trait* methods and do **not** trigger auto ID injection.
424//! > Only `StatefulInteractiveElement` / `FocusableElement` attributes
425//! > (`onClick`, `onHover`, `onDrag`, `tooltip`, `focusable`, `overflowScroll`,
426//! > `trackScroll`, etc.) require an ID.
427//!
428//! ### Loop safety
429//!
430//! Elements inside `{for ...}` loops share the same source location and would receive
431//! identical auto IDs across iterations. **A compile error is emitted** in this case.
432//! Add `key={expr}` (any `Display` type) to produce a unique ID per iteration:
433//!
434//! ```ignore
435//! // ❌ compile error — all <li> would share the same auto ID
436//! {for item in &self.items { <li onClick={handler}>{item}</li> }}
437//!
438//! // ✅ key produces a unique ID per iteration
439//! {for item in &self.items { <li key={item.id} onClick={handler}>{item}</li> }}
440//! ```
441//!
442//! ## Performance
443//!
444//! - **Compile-time** — All class parsing, color lookup, and attribute mapping happen
445//!   during macro expansion. The generated code is identical to hand-written GPUI.
446//! - **O(1) lookups** — Colors, attributes, and spacing prefixes use `match` statements
447//!   that the compiler turns into jump tables.
448//! - **Zero allocation** — Static classes generate no heap allocation. Dynamic classes
449//!   use `AsRef<str>` (zero-copy for `&str` inputs).
450//! - **Binary size** — Dynamic class helpers use `#[inline(never)]` + LLVM ICF to avoid
451//!   code bloat when multiple `class={expr}` appear in the same component.
452//!
453//! ## Further Reading
454//!
455//! - [Architecture guide (ARCHITECTURE.md)](https://github.com/wsafight/gpui-rsx/blob/main/ARCHITECTURE.md) —
456//!   module design, data flow, extension points
457//! - [Getting started](https://github.com/wsafight/gpui-rsx/blob/main/docs/getting-started.md)
458//! - [API reference](https://github.com/wsafight/gpui-rsx/blob/main/docs/api-reference.md)
459//! - [Best practices](https://github.com/wsafight/gpui-rsx/blob/main/docs/best-practices.md)
460//! - [Changelog](https://github.com/wsafight/gpui-rsx/blob/main/CHANGELOG.md)
461
462use proc_macro::TokenStream;
463use syn::parse_macro_input;
464
465mod codegen;
466mod diagnostics;
467mod parser;
468
469use codegen::generate_body;
470use parser::RsxBody;
471
472/// Transforms JSX-like markup into GPUI method chains at compile time.
473///
474/// # Syntax
475///
476/// ```text
477/// rsx! { <tag attr1 attr2={expr} class="…"> {children} </tag> }
478/// rsx! { <tag /> }          — self-closing
479/// rsx! { <> … </> }         — fragment (returns vec![…])
480/// ```
481///
482/// # Examples
483///
484/// ## Basic element with static classes
485///
486/// ```ignore
487/// rsx! {
488///     <div class="flex flex-col gap-4 p-4">
489///         <h1 class="text-2xl font-bold">{"Hello"}</h1>
490///     </div>
491/// }
492/// ```
493///
494/// ## Event handling (auto ID injected)
495///
496/// ```ignore
497/// rsx! {
498///     <button
499///         class="bg-blue-500 text-white px-4 py-2 rounded-md"
500///         onClick={cx.listener(|view, _, _window, cx| {
501///             view.count += 1;
502///             cx.notify();
503///         })}
504///     >
505///         {"Click me"}
506///     </button>
507/// }
508/// ```
509///
510/// ## For-loop rendering
511///
512/// ```ignore
513/// rsx! {
514///     <ul>
515///         {for item in &self.items {
516///             <li>{item.name.clone()}</li>
517///         }}
518///     </ul>
519/// }
520/// ```
521///
522/// ## Fragment (multiple root elements)
523///
524/// ```ignore
525/// rsx! {
526///     <>
527///         <div>{"First"}</div>
528///         <div>{"Second"}</div>
529///     </>
530/// }
531/// ```
532///
533/// ## Conditional styling
534///
535/// ```ignore
536/// rsx! {
537///     <div
538///         class="px-4 py-2 rounded-md"
539///         when={(is_active, |el| el.bg(rgb(0x3b82f6)))}
540///         whenSome={(custom_width, |el, w| el.w(px(w)))}
541///     >
542///         {"Content"}
543///     </div>
544/// }
545/// ```
546///
547/// ## Styled defaults
548///
549/// ```ignore
550/// rsx! { <h1 styled>{"Title"}</h1> }
551/// // → div().text_3xl().font_bold().child("Title")
552/// ```
553///
554/// # Notes
555///
556/// - **Static `class`**: parsed entirely at compile time, supports all classes.
557/// - **Dynamic `class={expr}`**: runtime matcher; full Tailwind color palette,
558///   arbitrary hex colors, common layout/spacing/typography utilities, and arbitrary
559///   numeric values via prefix fallback. Truly unsupported classes are silently ignored
560///   in release builds. Prefer `when` or static strings for full coverage.
561/// - **Auto ID**: elements with stateful event handlers receive a source-location-based
562///   ID automatically. Provide an explicit `id` attribute for state-sensitive elements.
563#[proc_macro]
564pub fn rsx(input: TokenStream) -> TokenStream {
565    let body = parse_macro_input!(input as RsxBody);
566    let code = generate_body(&body);
567    TokenStream::from(code)
568}