Skip to main content

teksilo_widgets/
preview_catalog.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! `WidgetCatalog` impls for `teksilo-widgets`.
5//!
6//! Gated behind the `preview` Cargo feature so production builds and
7//! headless tests don't pull in the catalog data or the `inventory`
8//! submission machinery.
9//!
10//! Per-widget impls are grouped by file section. Each one declares an
11//! `id`, a `group`, a `display_name`, a `KnobSpec` of tweakable
12//! properties, a `Vec<PreviewVariant>` of canonical scenarios, and a
13//! `build` closure that constructs a fresh widget instance from the
14//! `KnobValues` runtime view. The `register_widget_catalog!` macro
15//! wires each impl into the global `inventory` so the previewer's
16//! navigator surfaces it.
17//!
18//! Coverage in v1:
19//! - Tier A (flat knob surface): Button, Checkbox, RadioButton, Toggle,
20//!   Slider, ProgressBar, Badge, Link, ComboBox, SegmentedControl,
21//!   IconWidget, Divider.
22//! - Tier B (composites with fixture variants): Card, Panel, GroupBox,
23//!   GroupHeader, IconButton, Snackbar, Breadcrumb, Toolbar,
24//!   StatusBar, Accordion, RadioGroup, SplitButton.
25//! - Tier C (data-driven / structural): ListView, TreeView, MenuList,
26//!   ScrollArea, Splitter, TabWidget, ToolBox, Repeater.
27//! - Skipped (modal / event-heavy / overlay-driven): Dialog,
28//!   MessageBox, Popover, Wizard, MenuBar, MenuContext, TitleBar,
29//!   ShortcutSettings, ImageWidget. These need additional context
30//!   (intent registry, modal manager, raster resources) the catalog
31//!   does not provide.
32
33mod icons;
34
35use teksilo_core::signal::Signal;
36use teksilo_core::styles::{CheckboxVariant, TextInputVariant};
37use teksilo_core::widget::Widget;
38use teksilo_i18n::lit;
39use teksilo_preview::{
40    KnobOverrides, KnobSpec, KnobValues, PreviewVariant, SlottedChild, WidgetCatalog,
41    WidgetCategory, register_widget_catalog_at,
42};
43use teksilo_tokens::{BorderRole, SurfaceRole, TextRole, TextStyleRole};
44
45use crate::primitives::{
46    Center, Expand, FixedSize, Grid, HStack, IconWidget, Padding, Spacer, TextWidget, TrackSize,
47    VStack, ZStack,
48};
49// MaxSize / RectWidget are available for catalog impls in the section
50// below; not every impl needs them, so the import carries
51// `unused_imports` allow rather than gating each impl behind a feature.
52#[allow(unused_imports)]
53use crate::primitives::{MaxSize, RectWidget};
54use crate::{
55    Accordion, Avatar, AvatarPresence, AvatarShape, AvatarSize, Badge, Breadcrumb, BreadcrumbItem,
56    Button, ButtonVariant, Card, Checkbox, ComboBox, FontPicker, GridSizing, GridView, GroupBox,
57    GroupHeader, IconButton, IconButtonSize, LanguageSwitcher, Link, ListView, MenuItem, MenuList,
58    Orientation, PaneDescriptor, Panel, ProgressBar, RadioButton, RadioGroup, RadioTile,
59    RadioTileGroup, ScrollArea, SegmentDisplay, SegmentOverflow, SegmentedControl, Slider,
60    Snackbar, SplitButton, Splitter, SplitterModel, StandardListItem, StandardTreeItem, StatusBar,
61    TabWidget, TextInput, TextScaleControl, ThemeSwitcher, TileLayout, Toggle, ToolBox, Toolbar,
62    TreeView,
63};
64
65// ---------------------------------------------------------------------------
66// Button
67// ---------------------------------------------------------------------------
68
69/// Maps a `variant` enum-knob index to `ButtonVariant` (declaration order).
70fn button_variant(idx: usize) -> ButtonVariant {
71    match idx {
72        0 => ButtonVariant::Filled,
73        1 => ButtonVariant::Tinted,
74        2 => ButtonVariant::Outlined,
75        4 => ButtonVariant::Ghost,
76        5 => ButtonVariant::Link,
77        6 => ButtonVariant::Destructive,
78        _ => ButtonVariant::Plain,
79    }
80}
81
82/// Maps a `variant` enum-knob index to `CheckboxVariant` (declaration order).
83fn checkbox_variant(idx: usize) -> CheckboxVariant {
84    match idx {
85        1 => CheckboxVariant::Rounded,
86        2 => CheckboxVariant::Circle,
87        _ => CheckboxVariant::Square,
88    }
89}
90
91/// Maps a `variant` enum-knob index to `TextInputVariant` (declaration order).
92fn text_input_variant(idx: usize) -> TextInputVariant {
93    match idx {
94        1 => TextInputVariant::Filled,
95        2 => TextInputVariant::Underline,
96        3 => TextInputVariant::Bare,
97        _ => TextInputVariant::Outlined,
98    }
99}
100
101impl WidgetCatalog for Button {
102    fn id() -> &'static str {
103        "button"
104    }
105    fn group() -> &'static str {
106        "Controls"
107    }
108    fn display_name() -> &'static str {
109        "Button"
110    }
111    fn knobs() -> KnobSpec {
112        KnobSpec::new()
113            .text("label", "Label", "Click me")
114            .ctor(0)
115            .enum_(
116                "variant",
117                "Variant",
118                "ButtonVariant",
119                &[
120                    "Filled",
121                    "Tinted",
122                    "Outlined",
123                    "Plain",
124                    "Ghost",
125                    "Link",
126                    "Destructive",
127                ],
128                3,
129            )
130            .bool_("enabled", "Enabled", true)
131            .opt_text("tooltip", "Tooltip", None)
132    }
133    fn variants() -> Vec<PreviewVariant> {
134        vec![
135            PreviewVariant::defaults("default"),
136            PreviewVariant::knobs(
137                "primary",
138                KnobOverrides::new()
139                    .enum_("variant", 0)
140                    .text("label", "Save"),
141            ),
142            PreviewVariant::knobs(
143                "flat",
144                KnobOverrides::new()
145                    .enum_("variant", 4)
146                    .text("label", "More…"),
147            ),
148            PreviewVariant::knobs("disabled", KnobOverrides::new().bool_("enabled", false)),
149            PreviewVariant::knobs(
150                "with-tooltip",
151                KnobOverrides::new()
152                    .text("label", "Help")
153                    .opt_text("tooltip", Some("Open the help documentation")),
154            ),
155        ]
156    }
157    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
158        let label = knobs.text("label").get();
159        let variant = button_variant(knobs.enum_("variant").get());
160        let enabled = knobs.bool_("enabled").get();
161        let tooltip = knobs.opt_text("tooltip").get();
162        let mut b = Button::new(lit!(label)).variant(variant).enabled(enabled);
163        if let Some(t) = tooltip {
164            b = b.tooltip(lit!(t));
165        }
166        Box::new(b)
167    }
168    fn icon() -> Option<Box<dyn Widget>> {
169        Some(icons::button())
170    }
171}
172register_widget_catalog_at!("crates/teksilo-widgets/src/button.rs", Button);
173
174// =========================================================================
175// Layout primitives (designer-facing — ContainerA / Leaf, with runtime
176// `build_with_children` so the designer's interpreted canvas can nest them)
177// =========================================================================
178
179impl WidgetCatalog for VStack {
180    fn id() -> &'static str {
181        "vstack"
182    }
183    fn group() -> &'static str {
184        "Layout"
185    }
186    fn display_name() -> &'static str {
187        "VStack"
188    }
189    fn knobs() -> KnobSpec {
190        KnobSpec::new().f32_("spacing", "Spacing", 8.0, 0.0, 48.0)
191    }
192    fn variants() -> Vec<PreviewVariant> {
193        vec![PreviewVariant::defaults("default")]
194    }
195    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
196        Box::new(
197            VStack::new()
198                .spacing(knobs.f32_("spacing").get())
199                .child(sample_text("Item 1"))
200                .child(sample_text("Item 2"))
201                .child(sample_text("Item 3")),
202        )
203    }
204    fn icon() -> Option<Box<dyn Widget>> {
205        Some(icons::vstack())
206    }
207    fn category() -> WidgetCategory {
208        WidgetCategory::ContainerA
209    }
210    fn build_with_children(
211        _variant: &str,
212        knobs: &KnobValues,
213        children: Vec<SlottedChild>,
214    ) -> Box<dyn Widget> {
215        let mut s = VStack::new().spacing(knobs.f32_("spacing").get());
216        for c in children {
217            s = s.add_child(c.id);
218        }
219        Box::new(s)
220    }
221}
222register_widget_catalog_at!("crates/teksilo-widgets/src/primitives/vstack.rs", VStack);
223
224impl WidgetCatalog for HStack {
225    fn id() -> &'static str {
226        "hstack"
227    }
228    fn group() -> &'static str {
229        "Layout"
230    }
231    fn display_name() -> &'static str {
232        "HStack"
233    }
234    fn knobs() -> KnobSpec {
235        KnobSpec::new().f32_("spacing", "Spacing", 8.0, 0.0, 48.0)
236    }
237    fn variants() -> Vec<PreviewVariant> {
238        vec![PreviewVariant::defaults("default")]
239    }
240    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
241        Box::new(
242            HStack::new()
243                .spacing(knobs.f32_("spacing").get())
244                .child(sample_text("A"))
245                .child(sample_text("B"))
246                .child(sample_text("C")),
247        )
248    }
249    fn icon() -> Option<Box<dyn Widget>> {
250        Some(icons::hstack())
251    }
252    fn category() -> WidgetCategory {
253        WidgetCategory::ContainerA
254    }
255    fn build_with_children(
256        _variant: &str,
257        knobs: &KnobValues,
258        children: Vec<SlottedChild>,
259    ) -> Box<dyn Widget> {
260        let mut s = HStack::new().spacing(knobs.f32_("spacing").get());
261        for c in children {
262            s = s.add_child(c.id);
263        }
264        Box::new(s)
265    }
266}
267register_widget_catalog_at!("crates/teksilo-widgets/src/primitives/hstack.rs", HStack);
268
269impl WidgetCatalog for ZStack {
270    fn id() -> &'static str {
271        "zstack"
272    }
273    fn group() -> &'static str {
274        "Layout"
275    }
276    fn display_name() -> &'static str {
277        "ZStack"
278    }
279    fn variants() -> Vec<PreviewVariant> {
280        vec![PreviewVariant::defaults("default")]
281    }
282    fn build(_variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
283        Box::new(
284            ZStack::new()
285                .child(RectWidget::new().background(SurfaceRole::Raised))
286                .child(Center::new().child(sample_text("ZStack"))),
287        )
288    }
289    fn icon() -> Option<Box<dyn Widget>> {
290        Some(icons::zstack())
291    }
292    fn category() -> WidgetCategory {
293        WidgetCategory::ContainerA
294    }
295    fn build_with_children(
296        _variant: &str,
297        _knobs: &KnobValues,
298        children: Vec<SlottedChild>,
299    ) -> Box<dyn Widget> {
300        let mut s = ZStack::new();
301        for c in children {
302            s = s.add_child(c.id);
303        }
304        Box::new(s)
305    }
306}
307register_widget_catalog_at!("crates/teksilo-widgets/src/primitives/zstack.rs", ZStack);
308
309impl WidgetCatalog for Grid {
310    fn id() -> &'static str {
311        "grid"
312    }
313    fn group() -> &'static str {
314        "Layout"
315    }
316    fn display_name() -> &'static str {
317        "Grid"
318    }
319    fn knobs() -> KnobSpec {
320        KnobSpec::new()
321            .choice("columns", "Columns", &["1", "2", "3", "4"], 1)
322            .f32_("gap", "Gap", 8.0, 0.0, 32.0)
323    }
324    fn variants() -> Vec<PreviewVariant> {
325        vec![PreviewVariant::defaults("default")]
326    }
327    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
328        let cols = knobs.choice("columns").get() + 1;
329        let gap = knobs.f32_("gap").get();
330        let mut g = Grid::new()
331            .columns(vec![TrackSize::Fractional(1.0); cols])
332            .column_gap(gap)
333            .row_gap(gap);
334        for i in 1..=6 {
335            g = g.child(sample_text(&format!("{i}")));
336        }
337        Box::new(g)
338    }
339    fn icon() -> Option<Box<dyn Widget>> {
340        Some(icons::grid())
341    }
342    fn category() -> WidgetCategory {
343        WidgetCategory::ContainerA
344    }
345    fn build_with_children(
346        _variant: &str,
347        knobs: &KnobValues,
348        children: Vec<SlottedChild>,
349    ) -> Box<dyn Widget> {
350        let cols = knobs.choice("columns").get() + 1;
351        let gap = knobs.f32_("gap").get();
352        let mut g = Grid::new()
353            .columns(vec![TrackSize::Fractional(1.0); cols])
354            .column_gap(gap)
355            .row_gap(gap);
356        for c in children {
357            g = g.add_child(c.id);
358        }
359        Box::new(g)
360    }
361}
362register_widget_catalog_at!("crates/teksilo-widgets/src/primitives/grid.rs", Grid);
363
364impl WidgetCatalog for Padding {
365    fn id() -> &'static str {
366        "padding"
367    }
368    fn group() -> &'static str {
369        "Layout"
370    }
371    fn display_name() -> &'static str {
372        "Padding"
373    }
374    fn knobs() -> KnobSpec {
375        KnobSpec::new().f32_("amount", "Amount", 16.0, 0.0, 48.0)
376    }
377    fn variants() -> Vec<PreviewVariant> {
378        vec![PreviewVariant::defaults("default")]
379    }
380    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
381        Box::new(Padding::uniform(knobs.f32_("amount").get()).child(sample_text("Padded content")))
382    }
383    fn icon() -> Option<Box<dyn Widget>> {
384        Some(icons::padding())
385    }
386    fn category() -> WidgetCategory {
387        WidgetCategory::ContainerA
388    }
389    fn build_with_children(
390        _variant: &str,
391        knobs: &KnobValues,
392        children: Vec<SlottedChild>,
393    ) -> Box<dyn Widget> {
394        let mut p = Padding::uniform(knobs.f32_("amount").get());
395        if let Some(c) = children.into_iter().next() {
396            p = p.child_id(c.id);
397        }
398        Box::new(p)
399    }
400}
401register_widget_catalog_at!("crates/teksilo-widgets/src/primitives/padding.rs", Padding);
402
403impl WidgetCatalog for Expand {
404    fn id() -> &'static str {
405        "expand"
406    }
407    fn group() -> &'static str {
408        "Layout"
409    }
410    fn display_name() -> &'static str {
411        "Expand"
412    }
413    fn knobs() -> KnobSpec {
414        KnobSpec::new().f32_("flex", "Flex", 1.0, 0.0, 4.0)
415    }
416    fn variants() -> Vec<PreviewVariant> {
417        vec![PreviewVariant::defaults("default")]
418    }
419    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
420        Box::new(
421            FixedSize::new().width(220.0_f32).height(60.0_f32).child(
422                Expand::new()
423                    .flex(knobs.f32_("flex").get())
424                    .child(RectWidget::new().background(SurfaceRole::AccentSubtle)),
425            ),
426        )
427    }
428    fn icon() -> Option<Box<dyn Widget>> {
429        Some(icons::expand())
430    }
431    fn category() -> WidgetCategory {
432        WidgetCategory::ContainerA
433    }
434    fn build_with_children(
435        _variant: &str,
436        knobs: &KnobValues,
437        children: Vec<SlottedChild>,
438    ) -> Box<dyn Widget> {
439        let mut e = Expand::new().flex(knobs.f32_("flex").get());
440        if let Some(c) = children.into_iter().next() {
441            e = e.child_id(c.id);
442        }
443        Box::new(e)
444    }
445}
446register_widget_catalog_at!("crates/teksilo-widgets/src/primitives/expand.rs", Expand);
447
448impl WidgetCatalog for Center {
449    fn id() -> &'static str {
450        "center"
451    }
452    fn group() -> &'static str {
453        "Layout"
454    }
455    fn display_name() -> &'static str {
456        "Center"
457    }
458    fn variants() -> Vec<PreviewVariant> {
459        vec![PreviewVariant::defaults("default")]
460    }
461    fn build(_variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
462        Box::new(
463            FixedSize::new()
464                .width(200.0_f32)
465                .height(80.0_f32)
466                .child(Center::new().child(sample_text("Centered"))),
467        )
468    }
469    fn icon() -> Option<Box<dyn Widget>> {
470        Some(icons::center())
471    }
472    fn category() -> WidgetCategory {
473        WidgetCategory::ContainerA
474    }
475    fn build_with_children(
476        _variant: &str,
477        _knobs: &KnobValues,
478        children: Vec<SlottedChild>,
479    ) -> Box<dyn Widget> {
480        let mut c0 = Center::new();
481        if let Some(c) = children.into_iter().next() {
482            c0 = c0.child_id(c.id);
483        }
484        Box::new(c0)
485    }
486}
487register_widget_catalog_at!("crates/teksilo-widgets/src/primitives/center.rs", Center);
488
489impl WidgetCatalog for Spacer {
490    fn id() -> &'static str {
491        "spacer"
492    }
493    fn group() -> &'static str {
494        "Layout"
495    }
496    fn display_name() -> &'static str {
497        "Spacer"
498    }
499    fn variants() -> Vec<PreviewVariant> {
500        vec![PreviewVariant::defaults("default")]
501    }
502    fn build(_variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
503        Box::new(
504            HStack::new()
505                .child(sample_text("L"))
506                .child(Spacer::new())
507                .child(sample_text("R")),
508        )
509    }
510    fn icon() -> Option<Box<dyn Widget>> {
511        Some(icons::spacer())
512    }
513}
514register_widget_catalog_at!("crates/teksilo-widgets/src/primitives/spacer.rs", Spacer);
515
516impl WidgetCatalog for TextWidget {
517    fn id() -> &'static str {
518        "text_widget"
519    }
520    fn group() -> &'static str {
521        "Display"
522    }
523    fn display_name() -> &'static str {
524        // Match the actual widget type name (`TextWidget`), like every other
525        // catalog entry (`Button`, `VStack`, …). Tools that map source widget
526        // names to catalog entries (e.g. teksilo-designer's interpreter) rely
527        // on this; "Text" was an inconsistent shorthand.
528        "TextWidget"
529    }
530    fn knobs() -> KnobSpec {
531        KnobSpec::new()
532            .text("text", "Text", "Label")
533            .ctor(0)
534            .text_role("color", "Color", TextRole::Primary)
535            .text_style("style", "Style", TextStyleRole::Body)
536    }
537    fn variants() -> Vec<PreviewVariant> {
538        vec![
539            PreviewVariant::defaults("default"),
540            PreviewVariant::knobs(
541                "secondary",
542                KnobOverrides::new().text_role("color", TextRole::Secondary),
543            ),
544            PreviewVariant::knobs(
545                "bold",
546                KnobOverrides::new().text_style("style", TextStyleRole::BodyBold),
547            ),
548        ]
549    }
550    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
551        Box::new(
552            TextWidget::new(lit!(knobs.text("text").get()))
553                .style(knobs.text_style("style").get())
554                .color(knobs.text_role("color").get()),
555        )
556    }
557    fn icon() -> Option<Box<dyn Widget>> {
558        Some(icons::text_widget())
559    }
560}
561register_widget_catalog_at!(
562    "crates/teksilo-widgets/src/primitives/text_widget.rs",
563    TextWidget
564);
565
566impl WidgetCatalog for TextInput {
567    fn id() -> &'static str {
568        "text_input"
569    }
570    fn group() -> &'static str {
571        "Controls"
572    }
573    fn display_name() -> &'static str {
574        "TextInput"
575    }
576    fn knobs() -> KnobSpec {
577        KnobSpec::new()
578            .text("value", "Value", "")
579            .ctor(0)
580            .enum_(
581                "variant",
582                "Variant",
583                "TextInputVariant",
584                &["Outlined", "Filled", "Underline", "Bare"],
585                0,
586            )
587            .text("placeholder", "Placeholder", "Type here…")
588            .bool_("enabled", "Enabled", true)
589    }
590    fn variants() -> Vec<PreviewVariant> {
591        vec![PreviewVariant::defaults("default")]
592    }
593    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
594        Box::new(
595            FixedSize::new().width(220.0_f32).child(
596                TextInput::new(knobs.text("value"))
597                    .variant(text_input_variant(knobs.enum_("variant").get()))
598                    .placeholder(lit!(knobs.text("placeholder").get()))
599                    .enabled(knobs.bool_("enabled").get()),
600            ),
601        )
602    }
603    fn icon() -> Option<Box<dyn Widget>> {
604        Some(icons::text_input())
605    }
606}
607register_widget_catalog_at!("crates/teksilo-widgets/src/text_input.rs", TextInput);
608
609// ---------------------------------------------------------------------------
610// TextScaleControl
611// ---------------------------------------------------------------------------
612
613impl WidgetCatalog for TextScaleControl {
614    fn id() -> &'static str {
615        "text_scale_control"
616    }
617    fn group() -> &'static str {
618        "Accessibility"
619    }
620    fn display_name() -> &'static str {
621        "TextScaleControl"
622    }
623    fn variants() -> Vec<PreviewVariant> {
624        vec![PreviewVariant::defaults("default")]
625    }
626    fn build(_variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
627        // Ephemeral signal: the catalog has no app context, so this previews
628        // the control's chrome without driving real app-wide rescaling.
629        Box::new(TextScaleControl::new(Signal::new(1.0_f32)).label(lit!("Text size")))
630    }
631}
632register_widget_catalog_at!(
633    "crates/teksilo-widgets/src/text_scale_control.rs",
634    TextScaleControl
635);
636
637// ---------------------------------------------------------------------------
638// Checkbox
639// ---------------------------------------------------------------------------
640
641impl WidgetCatalog for Checkbox {
642    fn id() -> &'static str {
643        "checkbox"
644    }
645    fn group() -> &'static str {
646        "Controls"
647    }
648    fn display_name() -> &'static str {
649        "Checkbox"
650    }
651    fn knobs() -> KnobSpec {
652        KnobSpec::new()
653            .bool_("checked", "Checked", false)
654            .ctor(0)
655            .enum_(
656                "variant",
657                "Variant",
658                "CheckboxVariant",
659                &["Square", "Rounded", "Circle"],
660                0,
661            )
662            .text("label", "Label", "Enable feature")
663            .bool_("enabled", "Enabled", true)
664    }
665    fn variants() -> Vec<PreviewVariant> {
666        vec![
667            PreviewVariant::defaults("unchecked"),
668            PreviewVariant::knobs("checked", KnobOverrides::new().bool_("checked", true)),
669            PreviewVariant::knobs("disabled", KnobOverrides::new().bool_("enabled", false)),
670        ]
671    }
672    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
673        let label = knobs.text("label").get();
674        let checked = knobs.bool_("checked");
675        let enabled = knobs.bool_("enabled").get();
676        Box::new(
677            Checkbox::new(checked)
678                .variant(checkbox_variant(knobs.enum_("variant").get()))
679                .label(lit!(label))
680                .enabled(enabled),
681        )
682    }
683    fn icon() -> Option<Box<dyn Widget>> {
684        Some(icons::checkbox())
685    }
686}
687register_widget_catalog_at!("crates/teksilo-widgets/src/checkbox.rs", Checkbox);
688
689// ---------------------------------------------------------------------------
690// RadioButton
691// ---------------------------------------------------------------------------
692
693impl WidgetCatalog for RadioButton {
694    fn id() -> &'static str {
695        "radio_button"
696    }
697    fn group() -> &'static str {
698        "Controls"
699    }
700    fn display_name() -> &'static str {
701        "RadioButton"
702    }
703    fn knobs() -> KnobSpec {
704        // The knob's `selected` choice is the same `Signal<usize>` the
705        // RadioButton reads as its group selector. Index 0 = "Yes" =
706        // radio (value 0) selected; 1 = "No" = group on a different
707        // value so radio 0 is unselected. Using the knob's signal
708        // directly avoids needing a bridge.
709        KnobSpec::new()
710            .choice("selected", "Selected", &["Yes", "No"], 1)
711            .opt_text("label", "Label", Some("Option"))
712            .bool_("enabled", "Enabled", true)
713    }
714    fn variants() -> Vec<PreviewVariant> {
715        vec![
716            PreviewVariant::defaults("unselected"),
717            PreviewVariant::knobs("selected", KnobOverrides::new().choice("selected", 0)),
718            PreviewVariant::knobs("disabled", KnobOverrides::new().bool_("enabled", false)),
719            PreviewVariant::knobs("no-label", KnobOverrides::new().opt_text("label", None)),
720        ]
721    }
722    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
723        let label = knobs.opt_text("label").get();
724        let enabled = knobs.bool_("enabled").get();
725        let mut r = RadioButton::new(0, knobs.choice("selected")).enabled(enabled);
726        if let Some(label) = label {
727            r = r.label(lit!(label));
728        }
729        Box::new(r)
730    }
731}
732register_widget_catalog_at!("crates/teksilo-widgets/src/radio_button.rs", RadioButton);
733
734// ---------------------------------------------------------------------------
735// Toggle
736// ---------------------------------------------------------------------------
737
738impl WidgetCatalog for Toggle {
739    fn id() -> &'static str {
740        "toggle"
741    }
742    fn group() -> &'static str {
743        "Controls"
744    }
745    fn display_name() -> &'static str {
746        "Toggle"
747    }
748    fn knobs() -> KnobSpec {
749        // Toggle's `accessibility()` insists on a non-empty label
750        // (debug_assert!) — required for screen readers. Keep the
751        // knob mandatory rather than optional.
752        KnobSpec::new()
753            .bool_("on", "On", false)
754            .text("label", "Label", "Enable feature")
755            .bool_("enabled", "Enabled", true)
756    }
757    fn variants() -> Vec<PreviewVariant> {
758        vec![
759            PreviewVariant::defaults("off"),
760            PreviewVariant::knobs("on", KnobOverrides::new().bool_("on", true)),
761            PreviewVariant::knobs("disabled", KnobOverrides::new().bool_("enabled", false)),
762        ]
763    }
764    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
765        let on = knobs.bool_("on");
766        let label = knobs.text("label").get();
767        let enabled = knobs.bool_("enabled").get();
768        Box::new(Toggle::new(on).label(lit!(label)).enabled(enabled))
769    }
770    fn icon() -> Option<Box<dyn Widget>> {
771        Some(icons::toggle())
772    }
773}
774register_widget_catalog_at!("crates/teksilo-widgets/src/toggle.rs", Toggle);
775
776// ---------------------------------------------------------------------------
777// Slider
778// ---------------------------------------------------------------------------
779
780impl WidgetCatalog for Slider {
781    fn id() -> &'static str {
782        "slider"
783    }
784    fn group() -> &'static str {
785        "Controls"
786    }
787    fn display_name() -> &'static str {
788        "Slider"
789    }
790    fn knobs() -> KnobSpec {
791        KnobSpec::new()
792            .f32_("value", "Value", 0.5, 0.0, 1.0)
793            .ctor(0)
794            .enum_(
795                "orientation",
796                "Orientation",
797                "Orientation",
798                &["Horizontal", "Vertical"],
799                0,
800            )
801            .f32_step("step", "Step (0 = continuous)", 0.0, 0.0, 0.5, 0.05)
802            .bool_("enabled", "Enabled", true)
803            .opt_text("label", "Label", None)
804    }
805    fn variants() -> Vec<PreviewVariant> {
806        vec![
807            PreviewVariant::defaults("default"),
808            PreviewVariant::knobs("min", KnobOverrides::new().f32_("value", 0.0)),
809            PreviewVariant::knobs("max", KnobOverrides::new().f32_("value", 1.0)),
810            PreviewVariant::knobs(
811                "stepped",
812                KnobOverrides::new().f32_("value", 0.5).f32_("step", 0.1),
813            ),
814            PreviewVariant::knobs("vertical", KnobOverrides::new().enum_("orientation", 1)),
815            PreviewVariant::knobs("disabled", KnobOverrides::new().bool_("enabled", false)),
816            PreviewVariant::knobs(
817                "with-label",
818                KnobOverrides::new().opt_text("label", Some("Volume")),
819            ),
820        ]
821    }
822    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
823        use teksilo_tokens::Orientation;
824        let orient = match knobs.enum_("orientation").get() {
825            1 => Orientation::Vertical,
826            _ => Orientation::Horizontal,
827        };
828        let step = knobs.f32_("step").get();
829        let enabled = knobs.bool_("enabled").get();
830        let label = knobs.opt_text("label").get();
831        let mut s = Slider::new(knobs.f32_("value"), 0.0, 1.0)
832            .orientation(orient)
833            .enabled(enabled);
834        if step > 0.0 {
835            s = s.step(step);
836        }
837        if let Some(label) = label {
838            s = s.label(lit!(label));
839        }
840        // Vertical sliders need a fixed height to be visible.
841        let widget: Box<dyn Widget> = if matches!(orient, Orientation::Vertical) {
842            Box::new(FixedSize::new().height(160.0_f32).child(s))
843        } else {
844            Box::new(s)
845        };
846        widget
847    }
848    fn icon() -> Option<Box<dyn Widget>> {
849        Some(icons::slider())
850    }
851}
852register_widget_catalog_at!("crates/teksilo-widgets/src/slider.rs", Slider);
853
854// ---------------------------------------------------------------------------
855// ProgressBar
856// ---------------------------------------------------------------------------
857
858impl WidgetCatalog for ProgressBar {
859    fn id() -> &'static str {
860        "progress_bar"
861    }
862    fn group() -> &'static str {
863        "Controls"
864    }
865    fn display_name() -> &'static str {
866        "ProgressBar"
867    }
868    fn knobs() -> KnobSpec {
869        KnobSpec::new()
870            .f32_("value", "Value", 0.4, 0.0, 1.0)
871            .bool_("indeterminate", "Indeterminate", false)
872            .choice("orientation", "Orientation", &["Horizontal", "Vertical"], 0)
873            .opt_text("label", "Label", None)
874    }
875    fn variants() -> Vec<PreviewVariant> {
876        vec![
877            PreviewVariant::defaults("determinate"),
878            PreviewVariant::knobs("empty", KnobOverrides::new().f32_("value", 0.0)),
879            PreviewVariant::knobs("full", KnobOverrides::new().f32_("value", 1.0)),
880            PreviewVariant::knobs(
881                "indeterminate",
882                KnobOverrides::new().bool_("indeterminate", true),
883            ),
884            PreviewVariant::knobs("vertical", KnobOverrides::new().choice("orientation", 1)),
885            PreviewVariant::knobs(
886                "with-label",
887                KnobOverrides::new()
888                    .f32_("value", 0.7)
889                    .opt_text("label", Some("Uploading…")),
890            ),
891        ]
892    }
893    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
894        use teksilo_tokens::Orientation;
895        let indeterminate = knobs.bool_("indeterminate").get();
896        let orient = match knobs.choice("orientation").get() {
897            1 => Orientation::Vertical,
898            _ => Orientation::Horizontal,
899        };
900        let label = knobs.opt_text("label").get();
901        let mut bar = if indeterminate {
902            ProgressBar::indeterminate()
903        } else {
904            ProgressBar::new(knobs.f32_("value").get())
905        };
906        bar = bar.orientation(orient);
907        if let Some(label) = label {
908            bar = bar.label(lit!(label));
909        }
910        let widget: Box<dyn Widget> = if matches!(orient, Orientation::Vertical) {
911            Box::new(FixedSize::new().height(160.0_f32).child(bar))
912        } else {
913            Box::new(bar)
914        };
915        widget
916    }
917}
918register_widget_catalog_at!("crates/teksilo-widgets/src/progress_bar.rs", ProgressBar);
919
920// ---------------------------------------------------------------------------
921// Badge
922// ---------------------------------------------------------------------------
923
924impl WidgetCatalog for Badge {
925    fn id() -> &'static str {
926        "badge"
927    }
928    fn group() -> &'static str {
929        "Controls"
930    }
931    fn display_name() -> &'static str {
932        "Badge"
933    }
934    fn knobs() -> KnobSpec {
935        KnobSpec::new()
936            .text("label", "Label", "NEW")
937            .surface_role("background", "Background", SurfaceRole::Accent)
938            .text_role("text_role", "Text colour", TextRole::OnAccent)
939    }
940    fn variants() -> Vec<PreviewVariant> {
941        vec![
942            PreviewVariant::defaults("accent"),
943            PreviewVariant::knobs("long", KnobOverrides::new().text("label", "EXPERIMENTAL")),
944            PreviewVariant::knobs("short", KnobOverrides::new().text("label", "•")),
945            PreviewVariant::knobs(
946                "success",
947                KnobOverrides::new()
948                    .text("label", "OK")
949                    .surface_role("background", SurfaceRole::StatusSuccess)
950                    .text_role("text_role", TextRole::Success),
951            ),
952            PreviewVariant::knobs(
953                "warning",
954                KnobOverrides::new()
955                    .text("label", "BETA")
956                    .surface_role("background", SurfaceRole::StatusWarning)
957                    .text_role("text_role", TextRole::Warning),
958            ),
959            PreviewVariant::knobs(
960                "error",
961                KnobOverrides::new()
962                    .text("label", "ERR")
963                    .surface_role("background", SurfaceRole::StatusError)
964                    .text_role("text_role", TextRole::Error),
965            ),
966        ]
967    }
968    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
969        let bg = knobs.surface_role("background");
970        let fg = knobs.text_role("text_role");
971        Box::new(
972            Badge::new(lit!(knobs.text("label").get()))
973                .background(bg)
974                .text_role(fg),
975        )
976    }
977}
978register_widget_catalog_at!("crates/teksilo-widgets/src/badge.rs", Badge);
979
980// ---------------------------------------------------------------------------
981// Avatar
982// ---------------------------------------------------------------------------
983
984impl WidgetCatalog for Avatar {
985    fn id() -> &'static str {
986        "avatar"
987    }
988    fn group() -> &'static str {
989        "Controls"
990    }
991    fn display_name() -> &'static str {
992        "Avatar"
993    }
994    fn knobs() -> KnobSpec {
995        KnobSpec::new()
996            // Free-form name; initials are derived (`Jane Doe` → `JD`).
997            .text("name", "Name", "Jane Doe")
998            // Optional override for the displayed initials when the
999            // user wants something other than the auto-derived form.
1000            .opt_text("initials_override", "Initials override", None)
1001            .choice(
1002                "size",
1003                "Size",
1004                &["Small (24)", "Medium (32)", "Large (48)", "XLarge (64)"],
1005                1,
1006            )
1007            .choice("shape", "Shape", &["Circle", "RoundedSquare", "Square"], 0)
1008            .choice(
1009                "presence",
1010                "Presence",
1011                &["None", "Online", "Offline", "Away", "Busy"],
1012                0,
1013            )
1014            .choice(
1015                "presence_corner",
1016                "Presence corner",
1017                &[
1018                    "BottomTrailing",
1019                    "BottomLeading",
1020                    "TopTrailing",
1021                    "TopLeading",
1022                ],
1023                0,
1024            )
1025            .bool_("border", "Show ring", false)
1026            .bool_("clickable", "Clickable", false)
1027    }
1028    fn variants() -> Vec<PreviewVariant> {
1029        vec![
1030            PreviewVariant::defaults("default"),
1031            PreviewVariant::knobs(
1032                "small",
1033                KnobOverrides::new().choice("size", 0).text("name", "AB"),
1034            ),
1035            PreviewVariant::knobs(
1036                "large",
1037                KnobOverrides::new()
1038                    .choice("size", 2)
1039                    .text("name", "Sherlock Holmes"),
1040            ),
1041            PreviewVariant::knobs(
1042                "xlarge",
1043                KnobOverrides::new()
1044                    .choice("size", 3)
1045                    .text("name", "Marie Curie"),
1046            ),
1047            PreviewVariant::knobs(
1048                "rounded-square",
1049                KnobOverrides::new()
1050                    .choice("shape", 1)
1051                    .text("name", "Project X"),
1052            ),
1053            PreviewVariant::knobs("online", KnobOverrides::new().choice("presence", 1)),
1054            PreviewVariant::knobs("away", KnobOverrides::new().choice("presence", 3)),
1055            PreviewVariant::knobs("busy", KnobOverrides::new().choice("presence", 4)),
1056            PreviewVariant::knobs("with-ring", KnobOverrides::new().bool_("border", true)),
1057            PreviewVariant::knobs("clickable", KnobOverrides::new().bool_("clickable", true)),
1058            PreviewVariant::knobs("single-letter", KnobOverrides::new().text("name", "Cher")),
1059            PreviewVariant::knobs(
1060                "email-derived",
1061                KnobOverrides::new().text("name", "jane.doe@example.com"),
1062            ),
1063        ]
1064    }
1065    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
1066        let name = knobs.text("name").get();
1067        let size = match knobs.choice("size").get() {
1068            0 => AvatarSize::Small,
1069            2 => AvatarSize::Large,
1070            3 => AvatarSize::XLarge,
1071            _ => AvatarSize::Medium,
1072        };
1073        let shape = match knobs.choice("shape").get() {
1074            1 => AvatarShape::RoundedSquare,
1075            2 => AvatarShape::Square,
1076            _ => AvatarShape::Circle,
1077        };
1078        let presence = match knobs.choice("presence").get() {
1079            1 => Some(AvatarPresence::Online),
1080            2 => Some(AvatarPresence::Offline),
1081            3 => Some(AvatarPresence::Away),
1082            4 => Some(AvatarPresence::Busy),
1083            _ => None,
1084        };
1085        let presence_corner = match knobs.choice("presence_corner").get() {
1086            1 => crate::AvatarCorner::BottomLeading,
1087            2 => crate::AvatarCorner::TopTrailing,
1088            3 => crate::AvatarCorner::TopLeading,
1089            _ => crate::AvatarCorner::BottomTrailing,
1090        };
1091        let border = knobs.bool_("border").get();
1092        let clickable = knobs.bool_("clickable").get();
1093        let initials_override = knobs.opt_text("initials_override").get();
1094
1095        let mut a = if let Some(initials) = initials_override {
1096            Avatar::with_initials(lit!(&initials))
1097        } else {
1098            Avatar::with_name(lit!(&name))
1099        }
1100        .size(size)
1101        .shape(shape)
1102        .presence_corner(presence_corner);
1103
1104        if let Some(p) = presence {
1105            a = a.presence(p);
1106        }
1107        if border {
1108            a = a.border(2.0);
1109        }
1110        if clickable {
1111            a = a.label(lit!("Open user menu")).on_activate_fn(|_ctx| {});
1112        }
1113        Box::new(a)
1114    }
1115}
1116register_widget_catalog_at!("crates/teksilo-widgets/src/avatar.rs", Avatar);
1117
1118// ---------------------------------------------------------------------------
1119// Link
1120// ---------------------------------------------------------------------------
1121
1122impl WidgetCatalog for Link {
1123    fn id() -> &'static str {
1124        "link"
1125    }
1126    fn group() -> &'static str {
1127        "Controls"
1128    }
1129    fn display_name() -> &'static str {
1130        "Link"
1131    }
1132    fn knobs() -> KnobSpec {
1133        KnobSpec::new()
1134            .text("label", "Label", "Read more")
1135            .bool_("enabled", "Enabled", true)
1136    }
1137    fn variants() -> Vec<PreviewVariant> {
1138        vec![
1139            PreviewVariant::defaults("default"),
1140            PreviewVariant::knobs("disabled", KnobOverrides::new().bool_("enabled", false)),
1141        ]
1142    }
1143    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
1144        Box::new(Link::new(lit!(knobs.text("label").get())).enabled(knobs.bool_("enabled").get()))
1145    }
1146}
1147register_widget_catalog_at!("crates/teksilo-widgets/src/link.rs", Link);
1148
1149// ---------------------------------------------------------------------------
1150// SegmentedControl
1151// ---------------------------------------------------------------------------
1152
1153/// Maps a `display` enum-knob index to `SegmentDisplay` (declaration order).
1154fn segment_display(idx: usize) -> SegmentDisplay {
1155    match idx {
1156        1 => SegmentDisplay::Text,
1157        2 => SegmentDisplay::Icon,
1158        3 => SegmentDisplay::IconText,
1159        _ => SegmentDisplay::Auto,
1160    }
1161}
1162
1163/// Maps an `overflow` enum-knob index to `SegmentOverflow` (declaration order).
1164fn segment_overflow(idx: usize) -> SegmentOverflow {
1165    match idx {
1166        1 => SegmentOverflow::Compress,
1167        _ => SegmentOverflow::Menu,
1168    }
1169}
1170
1171impl WidgetCatalog for SegmentedControl {
1172    fn id() -> &'static str {
1173        "segmented_control"
1174    }
1175    fn group() -> &'static str {
1176        "Controls"
1177    }
1178    fn display_name() -> &'static str {
1179        "SegmentedControl"
1180    }
1181    fn knobs() -> KnobSpec {
1182        KnobSpec::new()
1183            .choice("selected", "Selected", &["Day", "Week", "Month"], 0)
1184            .choice(
1185                "display",
1186                "Display",
1187                &["Auto", "Text", "Icon", "Icon+Text"],
1188                0,
1189            )
1190            .choice("overflow", "Overflow", &["Menu", "Compress"], 0)
1191            .bool_("enabled", "Enabled", true)
1192    }
1193    fn variants() -> Vec<PreviewVariant> {
1194        vec![
1195            PreviewVariant::defaults("default"),
1196            PreviewVariant::knobs("middle", KnobOverrides::new().choice("selected", 1)),
1197            PreviewVariant::knobs("icon-only", KnobOverrides::new().choice("display", 2)),
1198            PreviewVariant::knobs("compress", KnobOverrides::new().choice("overflow", 1)),
1199            PreviewVariant::knobs("disabled", KnobOverrides::new().bool_("enabled", false)),
1200        ]
1201    }
1202    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
1203        // A knob `choice` is positional by construction and the option
1204        // list is closed, so the positional binding is the honest one here.
1205        Box::new(
1206            SegmentedControl::indexed(knobs.choice("selected"))
1207                .segments([lit!("Day"), lit!("Week"), lit!("Month")])
1208                .display(segment_display(knobs.choice("display").get()))
1209                .overflow(segment_overflow(knobs.choice("overflow").get()))
1210                .enabled(knobs.bool_("enabled").get()),
1211        )
1212    }
1213}
1214register_widget_catalog_at!(
1215    "crates/teksilo-widgets/src/segmented_control.rs",
1216    SegmentedControl
1217);
1218
1219// ---------------------------------------------------------------------------
1220// ComboBox
1221// ---------------------------------------------------------------------------
1222
1223impl WidgetCatalog for ComboBox<String> {
1224    fn id() -> &'static str {
1225        "combo_box"
1226    }
1227    fn group() -> &'static str {
1228        "Controls"
1229    }
1230    fn display_name() -> &'static str {
1231        "ComboBox"
1232    }
1233    fn knobs() -> KnobSpec {
1234        KnobSpec::new()
1235            .opt_text("selected", "Selected", Some("Apple"))
1236            .text("placeholder", "Placeholder", "Select a fruit…")
1237            .opt_text("label", "A11y label", Some("Fruit"))
1238            .bool_("enabled", "Enabled", true)
1239    }
1240    fn variants() -> Vec<PreviewVariant> {
1241        vec![
1242            PreviewVariant::defaults("with-selection"),
1243            PreviewVariant::knobs("empty", KnobOverrides::new().opt_text("selected", None)),
1244            PreviewVariant::knobs("disabled", KnobOverrides::new().bool_("enabled", false)),
1245        ]
1246    }
1247    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
1248        let items = vec![
1249            "Apple".to_string(),
1250            "Banana".to_string(),
1251            "Cherry".to_string(),
1252            "Date".to_string(),
1253            "Elderberry".to_string(),
1254        ];
1255        // ComboBox<String> takes `Signal<Option<String>>` — the knob's
1256        // `opt_text` accessor returns exactly that, so we use it
1257        // directly. User clicks on items mutate the knob signal,
1258        // which the inspector's editor sees and re-renders.
1259        let placeholder = knobs.text("placeholder").get();
1260        let enabled = knobs.bool_("enabled").get();
1261        let mut cb = ComboBox::new(items, knobs.opt_text("selected"))
1262            .placeholder(lit!(placeholder))
1263            .enabled(enabled);
1264        if let Some(label) = knobs.opt_text("label").get() {
1265            cb = cb.label(lit!(label));
1266        }
1267        Box::new(cb)
1268    }
1269    fn icon() -> Option<Box<dyn Widget>> {
1270        Some(icons::combo_box())
1271    }
1272}
1273register_widget_catalog_at!("crates/teksilo-widgets/src/combo_box.rs", ComboBox<String>);
1274
1275// ---------------------------------------------------------------------------
1276// FontPicker
1277// ---------------------------------------------------------------------------
1278
1279impl WidgetCatalog for FontPicker {
1280    fn id() -> &'static str {
1281        "font_picker"
1282    }
1283    fn group() -> &'static str {
1284        "Controls"
1285    }
1286    fn display_name() -> &'static str {
1287        "FontPicker"
1288    }
1289    fn knobs() -> KnobSpec {
1290        KnobSpec::new()
1291            .opt_text("selected", "Selected", Some("Georgia"))
1292            .opt_text("label", "A11y label", Some("Font"))
1293            .bool_("enabled", "Enabled", true)
1294    }
1295    fn variants() -> Vec<PreviewVariant> {
1296        vec![
1297            PreviewVariant::defaults("default"),
1298            PreviewVariant::knobs("empty", KnobOverrides::new().opt_text("selected", None)),
1299            PreviewVariant::knobs("disabled", KnobOverrides::new().bool_("enabled", false)),
1300        ]
1301    }
1302    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
1303        // A fixed family list keeps the preview deterministic (and PNG
1304        // export stable) instead of enumerating the host's system fonts.
1305        // Names that resolve on the host render their own-font samples;
1306        // others fall back to the default face.
1307        let enabled = knobs.bool_("enabled").get();
1308        let mut fp = FontPicker::new(knobs.opt_text("selected"))
1309            .families([
1310                "Georgia",
1311                "Verdana",
1312                "Arial",
1313                "Courier New",
1314                "Times New Roman",
1315                "Trebuchet MS",
1316            ])
1317            .enabled(enabled);
1318        if let Some(label) = knobs.opt_text("label").get() {
1319            fp = fp.label(lit!(label));
1320        }
1321        Box::new(fp)
1322    }
1323    fn icon() -> Option<Box<dyn Widget>> {
1324        Some(icons::combo_box())
1325    }
1326}
1327register_widget_catalog_at!("crates/teksilo-widgets/src/font_picker.rs", FontPicker);
1328
1329// ---------------------------------------------------------------------------
1330// LanguageSwitcher
1331// ---------------------------------------------------------------------------
1332
1333impl WidgetCatalog for LanguageSwitcher {
1334    fn id() -> &'static str {
1335        "language_switcher"
1336    }
1337    fn group() -> &'static str {
1338        "Controls"
1339    }
1340    fn display_name() -> &'static str {
1341        "LanguageSwitcher"
1342    }
1343    fn knobs() -> KnobSpec {
1344        KnobSpec::new()
1345    }
1346    fn variants() -> Vec<PreviewVariant> {
1347        vec![PreviewVariant::defaults("default")]
1348    }
1349    fn build(_variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
1350        // The previewer has no `I18nManager`, so seed an explicit locale
1351        // list to demonstrate the endonym + tag display ("français (fr-FR)").
1352        let locales = ["en-US", "fr-FR", "de-DE", "es-ES", "ar-SA", "ja-JP"]
1353            .iter()
1354            .map(|t| t.parse().expect("valid BCP-47 tag"))
1355            .collect();
1356        Box::new(LanguageSwitcher::new().locales(locales))
1357    }
1358    fn icon() -> Option<Box<dyn Widget>> {
1359        Some(icons::combo_box())
1360    }
1361}
1362register_widget_catalog_at!(
1363    "crates/teksilo-widgets/src/language_switcher.rs",
1364    LanguageSwitcher
1365);
1366
1367// ---------------------------------------------------------------------------
1368// ThemeSwitcher
1369// ---------------------------------------------------------------------------
1370
1371impl WidgetCatalog for ThemeSwitcher {
1372    fn id() -> &'static str {
1373        "theme_switcher"
1374    }
1375    fn group() -> &'static str {
1376        "Controls"
1377    }
1378    fn display_name() -> &'static str {
1379        "ThemeSwitcher"
1380    }
1381    fn knobs() -> KnobSpec {
1382        KnobSpec::new()
1383    }
1384    fn variants() -> Vec<PreviewVariant> {
1385        vec![PreviewVariant::defaults("default")]
1386    }
1387    fn build(_variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
1388        // Default Light / Dark / System — the System entry follows the OS.
1389        Box::new(ThemeSwitcher::new())
1390    }
1391    fn icon() -> Option<Box<dyn Widget>> {
1392        Some(icons::combo_box())
1393    }
1394}
1395register_widget_catalog_at!(
1396    "crates/teksilo-widgets/src/theme_switcher.rs",
1397    ThemeSwitcher
1398);
1399
1400// ---------------------------------------------------------------------------
1401// Divider
1402// ---------------------------------------------------------------------------
1403
1404impl WidgetCatalog for crate::primitives::Divider {
1405    fn id() -> &'static str {
1406        "divider"
1407    }
1408    fn group() -> &'static str {
1409        "Primitives"
1410    }
1411    fn display_name() -> &'static str {
1412        "Divider"
1413    }
1414    fn knobs() -> KnobSpec {
1415        KnobSpec::new()
1416            .choice("orientation", "Orientation", &["Horizontal", "Vertical"], 0)
1417            .f32_("thickness", "Thickness", 1.0, 0.5, 6.0)
1418            .border_role("color", "Colour", BorderRole::Divider)
1419    }
1420    fn variants() -> Vec<PreviewVariant> {
1421        vec![
1422            PreviewVariant::defaults("horizontal"),
1423            PreviewVariant::knobs("vertical", KnobOverrides::new().choice("orientation", 1)),
1424            PreviewVariant::knobs("thick", KnobOverrides::new().f32_("thickness", 4.0)),
1425            PreviewVariant::knobs(
1426                "strong",
1427                KnobOverrides::new().border_role("color", BorderRole::DividerStrong),
1428            ),
1429            PreviewVariant::knobs(
1430                "accent",
1431                KnobOverrides::new().border_role("color", BorderRole::Accent),
1432            ),
1433        ]
1434    }
1435    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
1436        let orient = knobs.choice("orientation").get();
1437        let thickness = knobs.f32_("thickness").get();
1438        let role = knobs.border_role("color").get();
1439        let mut d = if orient == 1 {
1440            crate::primitives::Divider::vertical()
1441        } else {
1442            crate::primitives::Divider::horizontal()
1443        };
1444        d = d.thickness(thickness).color(role);
1445        // Wrap a vertical divider in a fixed-height block so it has
1446        // something to draw across; horizontal in a fixed width.
1447        let wrapped: Box<dyn Widget> = if orient == 1 {
1448            Box::new(
1449                crate::primitives::FixedSize::new()
1450                    .height(120.0_f32)
1451                    .child(d),
1452            )
1453        } else {
1454            Box::new(
1455                crate::primitives::FixedSize::new()
1456                    .width(220.0_f32)
1457                    .child(d),
1458            )
1459        };
1460        wrapped
1461    }
1462}
1463register_widget_catalog_at!(
1464    "crates/teksilo-widgets/src/primitives/divider.rs",
1465    crate::primitives::Divider
1466);
1467
1468// ---------------------------------------------------------------------------
1469// IconWidget
1470// ---------------------------------------------------------------------------
1471
1472impl WidgetCatalog for IconWidget {
1473    fn id() -> &'static str {
1474        "icon_widget"
1475    }
1476    fn group() -> &'static str {
1477        "Primitives"
1478    }
1479    fn display_name() -> &'static str {
1480        "IconWidget"
1481    }
1482    fn knobs() -> KnobSpec {
1483        KnobSpec::new()
1484            .f32_("size", "Size (dp)", 24.0, 12.0, 96.0)
1485            .choice("shape", "Shape", &["Square", "Circle", "Triangle"], 0)
1486            .text_role("color", "Colour", TextRole::Primary)
1487    }
1488    fn variants() -> Vec<PreviewVariant> {
1489        vec![
1490            PreviewVariant::defaults("square"),
1491            PreviewVariant::knobs("circle", KnobOverrides::new().choice("shape", 1)),
1492            PreviewVariant::knobs("triangle", KnobOverrides::new().choice("shape", 2)),
1493            PreviewVariant::knobs("large", KnobOverrides::new().f32_("size", 64.0)),
1494            PreviewVariant::knobs(
1495                "accent",
1496                KnobOverrides::new().text_role("color", TextRole::Accent),
1497            ),
1498            PreviewVariant::knobs(
1499                "error",
1500                KnobOverrides::new().text_role("color", TextRole::Error),
1501            ),
1502        ]
1503    }
1504    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
1505        use teksilo_canvas::{Path, Point};
1506        let size = knobs.f32_("size").get();
1507        let shape = knobs.choice("shape").get();
1508        let mut path = Path::new();
1509        match shape {
1510            0 => {
1511                path.move_to(Point::new(2.0, 2.0));
1512                path.line_to(Point::new(size - 2.0, 2.0));
1513                path.line_to(Point::new(size - 2.0, size - 2.0));
1514                path.line_to(Point::new(2.0, size - 2.0));
1515                path.close();
1516            }
1517            1 => {
1518                let r = (size / 2.0) - 2.0;
1519                let c = Point::new(size / 2.0, size / 2.0);
1520                // Approximate a circle with cubic-bezier arcs (4 quadrants).
1521                let k = 0.552_284_8 * r;
1522                path.move_to(Point::new(c.x + r, c.y));
1523                path.cubic_to(
1524                    Point::new(c.x + r, c.y + k),
1525                    Point::new(c.x + k, c.y + r),
1526                    Point::new(c.x, c.y + r),
1527                );
1528                path.cubic_to(
1529                    Point::new(c.x - k, c.y + r),
1530                    Point::new(c.x - r, c.y + k),
1531                    Point::new(c.x - r, c.y),
1532                );
1533                path.cubic_to(
1534                    Point::new(c.x - r, c.y - k),
1535                    Point::new(c.x - k, c.y - r),
1536                    Point::new(c.x, c.y - r),
1537                );
1538                path.cubic_to(
1539                    Point::new(c.x + k, c.y - r),
1540                    Point::new(c.x + r, c.y - k),
1541                    Point::new(c.x + r, c.y),
1542                );
1543                path.close();
1544            }
1545            _ => {
1546                path.move_to(Point::new(size / 2.0, 2.0));
1547                path.line_to(Point::new(size - 2.0, size - 2.0));
1548                path.line_to(Point::new(2.0, size - 2.0));
1549                path.close();
1550            }
1551        }
1552        let role = knobs.text_role("color").get();
1553        Box::new(IconWidget::from_path(path, size).color(role))
1554    }
1555}
1556register_widget_catalog_at!(
1557    "crates/teksilo-widgets/src/primitives/icon_widget.rs",
1558    IconWidget
1559);
1560
1561// =========================================================================
1562// Tier B — composites with fixture variants
1563// =========================================================================
1564//
1565// These widgets don't have flat knob surfaces; their interesting states
1566// are structural ("with header + footer", "expanded with content",
1567// "two segments"). Each `variants()` is a hand-authored list of
1568// scenarios, and `knobs()` is left empty.
1569
1570// ---------------------------------------------------------------------------
1571// Card
1572// ---------------------------------------------------------------------------
1573
1574fn sample_text(label: &str) -> TextWidget {
1575    TextWidget::new(lit!(label))
1576        .style(TextStyleRole::Body)
1577        .color(TextRole::Primary)
1578}
1579
1580impl WidgetCatalog for Card {
1581    fn id() -> &'static str {
1582        "card"
1583    }
1584    fn group() -> &'static str {
1585        "Containers"
1586    }
1587    fn display_name() -> &'static str {
1588        "Card"
1589    }
1590    fn knobs() -> KnobSpec {
1591        KnobSpec::new()
1592            .text("title", "Title", "Card title")
1593            .text(
1594                "body",
1595                "Body",
1596                "Card body text. Cards group related controls into a labelled rectangular region.",
1597            )
1598            .bool_("show_header", "Show header", true)
1599            .bool_("show_footer", "Show footer", false)
1600            .surface_role("background", "Background", SurfaceRole::Main)
1601            .f32_("corner_radius", "Corner radius", 8.0, 0.0, 32.0)
1602            .f32_("padding", "Padding", 16.0, 0.0, 48.0)
1603    }
1604    fn variants() -> Vec<PreviewVariant> {
1605        vec![
1606            PreviewVariant::defaults("default"),
1607            PreviewVariant::knobs(
1608                "with-footer",
1609                KnobOverrides::new()
1610                    .text("title", "Settings")
1611                    .text("body", "Configure the application settings here.")
1612                    .bool_("show_footer", true),
1613            ),
1614            PreviewVariant::knobs(
1615                "headerless",
1616                KnobOverrides::new().bool_("show_header", false),
1617            ),
1618            PreviewVariant::knobs(
1619                "raised",
1620                KnobOverrides::new().surface_role("background", SurfaceRole::Raised),
1621            ),
1622        ]
1623    }
1624    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
1625        let mut card = Card::new()
1626            .background(knobs.surface_role("background"))
1627            .corner_radius(knobs.f32_("corner_radius").get())
1628            .padding(knobs.f32_("padding").get())
1629            .content(
1630                TextWidget::new(lit!(knobs.text("body").get()))
1631                    .style(TextStyleRole::Body)
1632                    .color(TextRole::Primary),
1633            );
1634        if knobs.bool_("show_header").get() {
1635            card = card.header(
1636                TextWidget::new(lit!(knobs.text("title").get()))
1637                    .style(TextStyleRole::BodyBold)
1638                    .color(TextRole::Primary),
1639            );
1640        }
1641        if knobs.bool_("show_footer").get() {
1642            card = card.footer(
1643                HStack::new()
1644                    .spacing(8.0)
1645                    .child(Spacer::new())
1646                    .child(Button::new(lit!("Cancel")).variant(ButtonVariant::Plain))
1647                    .child(Button::new(lit!("Save")).variant(ButtonVariant::Filled)),
1648            );
1649        }
1650        Box::new(card)
1651    }
1652    fn icon() -> Option<Box<dyn Widget>> {
1653        Some(icons::card())
1654    }
1655    fn category() -> WidgetCategory {
1656        WidgetCategory::ContainerB
1657    }
1658    fn slots() -> &'static [&'static str] {
1659        &["header", "content", "footer"]
1660    }
1661    fn build_with_children(
1662        _variant: &str,
1663        knobs: &KnobValues,
1664        children: Vec<SlottedChild>,
1665    ) -> Box<dyn Widget> {
1666        let mut card = Card::new()
1667            .background(knobs.surface_role("background"))
1668            .corner_radius(knobs.f32_("corner_radius").get())
1669            .padding(knobs.f32_("padding").get());
1670        for c in children {
1671            match c.slot.as_deref() {
1672                Some("header") => card = card.header_id(c.id),
1673                Some("footer") => card = card.footer_id(c.id),
1674                _ => card = card.content_id(c.id),
1675            }
1676        }
1677        Box::new(card)
1678    }
1679}
1680register_widget_catalog_at!("crates/teksilo-widgets/src/card.rs", Card);
1681
1682// ---------------------------------------------------------------------------
1683// Panel
1684// ---------------------------------------------------------------------------
1685
1686impl WidgetCatalog for Panel {
1687    fn id() -> &'static str {
1688        "panel"
1689    }
1690    fn group() -> &'static str {
1691        "Containers"
1692    }
1693    fn display_name() -> &'static str {
1694        "Panel"
1695    }
1696    fn knobs() -> KnobSpec {
1697        KnobSpec::new()
1698            .surface_role("background", "Background", SurfaceRole::Raised)
1699            .border_role("border_color", "Border colour", BorderRole::Default)
1700            .f32_("border_width", "Border width", 1.0, 0.0, 4.0)
1701            .f32_("corner_radius", "Corner radius", 6.0, 0.0, 32.0)
1702            .f32_("padding", "Padding", 16.0, 0.0, 48.0)
1703            .text("content", "Sample content", "Panel content")
1704    }
1705    fn variants() -> Vec<PreviewVariant> {
1706        vec![
1707            PreviewVariant::defaults("default"),
1708            PreviewVariant::knobs(
1709                "accent",
1710                KnobOverrides::new()
1711                    .surface_role("background", SurfaceRole::AccentSubtle)
1712                    .border_role("border_color", BorderRole::Accent)
1713                    .text("content", "Accent panel"),
1714            ),
1715            PreviewVariant::knobs(
1716                "sunken",
1717                KnobOverrides::new()
1718                    .surface_role("background", SurfaceRole::Sunken)
1719                    .text("content", "Sunken panel"),
1720            ),
1721            PreviewVariant::knobs("no-border", KnobOverrides::new().f32_("border_width", 0.0)),
1722            PreviewVariant::knobs("rounded", KnobOverrides::new().f32_("corner_radius", 16.0)),
1723        ]
1724    }
1725    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
1726        Box::new(
1727            Panel::new()
1728                .background(knobs.surface_role("background"))
1729                .border_color(knobs.border_role("border_color"))
1730                .border_width(knobs.f32_("border_width").get())
1731                .corner_radius(knobs.f32_("corner_radius").get())
1732                .padding(knobs.f32_("padding").get())
1733                .child(sample_text(&knobs.text("content").get())),
1734        )
1735    }
1736    fn icon() -> Option<Box<dyn Widget>> {
1737        Some(icons::panel())
1738    }
1739    fn category() -> WidgetCategory {
1740        WidgetCategory::ContainerA
1741    }
1742    fn build_with_children(
1743        _variant: &str,
1744        knobs: &KnobValues,
1745        children: Vec<SlottedChild>,
1746    ) -> Box<dyn Widget> {
1747        let mut p = Panel::new()
1748            .background(knobs.surface_role("background"))
1749            .border_color(knobs.border_role("border_color"))
1750            .border_width(knobs.f32_("border_width").get())
1751            .corner_radius(knobs.f32_("corner_radius").get())
1752            .padding(knobs.f32_("padding").get());
1753        if let Some(c) = children.into_iter().next() {
1754            p = p.child_id(c.id);
1755        }
1756        Box::new(p)
1757    }
1758}
1759register_widget_catalog_at!("crates/teksilo-widgets/src/panel.rs", Panel);
1760
1761// ---------------------------------------------------------------------------
1762// GroupBox
1763// ---------------------------------------------------------------------------
1764
1765impl WidgetCatalog for GroupBox {
1766    fn id() -> &'static str {
1767        "group_box"
1768    }
1769    fn group() -> &'static str {
1770        "Containers"
1771    }
1772    fn display_name() -> &'static str {
1773        "GroupBox"
1774    }
1775    fn knobs() -> KnobSpec {
1776        KnobSpec::new().text("title", "Title", "Notifications")
1777    }
1778    fn variants() -> Vec<PreviewVariant> {
1779        vec![
1780            PreviewVariant::defaults("default"),
1781            PreviewVariant::knobs("alt-title", KnobOverrides::new().text("title", "Privacy")),
1782        ]
1783    }
1784    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
1785        Box::new(
1786            GroupBox::new(lit!(knobs.text("title").get(),)).child(
1787                VStack::new()
1788                    .spacing(8.0)
1789                    .child(Checkbox::new(Signal::new(true)).label(lit!("Sounds")))
1790                    .child(Checkbox::new(Signal::new(false)).label(lit!("Badges")))
1791                    .child(Checkbox::new(Signal::new(true)).label(lit!("Banners"))),
1792            ),
1793        )
1794    }
1795}
1796register_widget_catalog_at!("crates/teksilo-widgets/src/group_box.rs", GroupBox);
1797
1798// ---------------------------------------------------------------------------
1799// GroupHeader
1800// ---------------------------------------------------------------------------
1801
1802impl WidgetCatalog for GroupHeader {
1803    fn id() -> &'static str {
1804        "group_header"
1805    }
1806    fn group() -> &'static str {
1807        "Containers"
1808    }
1809    fn display_name() -> &'static str {
1810        "GroupHeader"
1811    }
1812    fn knobs() -> KnobSpec {
1813        KnobSpec::new().text("label", "Label", "Section title")
1814    }
1815    fn variants() -> Vec<PreviewVariant> {
1816        vec![
1817            PreviewVariant::defaults("default"),
1818            // Demonstrates the role-based styling API: a bold, accent-colored
1819            // header that tracks runtime theme changes.
1820            PreviewVariant::defaults("accent"),
1821        ]
1822    }
1823    fn build(variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
1824        let header = GroupHeader::new(lit!(knobs.text("label").get(),));
1825        match variant {
1826            "accent" => Box::new(
1827                header
1828                    .style(teksilo_tokens::TextStyleRole::BodyBold)
1829                    .color(teksilo_tokens::TextRole::Accent),
1830            ),
1831            _ => Box::new(header),
1832        }
1833    }
1834}
1835register_widget_catalog_at!("crates/teksilo-widgets/src/group_header.rs", GroupHeader);
1836
1837// ---------------------------------------------------------------------------
1838// IconButton
1839// ---------------------------------------------------------------------------
1840
1841impl WidgetCatalog for IconButton {
1842    fn id() -> &'static str {
1843        "icon_button"
1844    }
1845    fn group() -> &'static str {
1846        "Controls"
1847    }
1848    fn display_name() -> &'static str {
1849        "IconButton"
1850    }
1851    fn variants() -> Vec<PreviewVariant> {
1852        // Stand-alone scenarios (default visual mode).
1853        fn build_search_toolbar() -> Box<dyn Widget> {
1854            Box::new(IconButton::search().toolbar())
1855        }
1856        fn build_add_hero() -> Box<dyn Widget> {
1857            Box::new(IconButton::add().hero())
1858        }
1859        // Embedded scenarios — the JetBrains "built-in" dim look.
1860        fn build_browse_embedded() -> Box<dyn Widget> {
1861            Box::new(IconButton::browse().embedded())
1862        }
1863        fn build_clear_embedded_compact() -> Box<dyn Widget> {
1864            Box::new(IconButton::clear().embedded().size(IconButtonSize::Compact))
1865        }
1866        vec![
1867            PreviewVariant::scenario("search-toolbar", build_search_toolbar),
1868            PreviewVariant::scenario("add-hero", build_add_hero),
1869            PreviewVariant::scenario("browse-embedded", build_browse_embedded),
1870            PreviewVariant::scenario("clear-embedded-compact", build_clear_embedded_compact),
1871        ]
1872    }
1873    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
1874        scenario_for::<Self>(variant)
1875    }
1876}
1877register_widget_catalog_at!("crates/teksilo-widgets/src/icon_button.rs", IconButton);
1878
1879// ---------------------------------------------------------------------------
1880// Snackbar
1881// ---------------------------------------------------------------------------
1882
1883/// `Snackbar` in this codebase is an *overlay-trigger* pattern: a
1884/// labelled button that opens a popup with the supplied content when
1885/// clicked. The `label` argument names the trigger button; `.content()`
1886/// (required — `expect(...)` panics if missing) holds the popup body.
1887/// We populate both: the trigger reads from a `trigger_label` knob and
1888/// the popup body reads from a `message` knob, wrapped in a `Panel`
1889/// for readability when the popup opens.
1890impl WidgetCatalog for Snackbar {
1891    fn id() -> &'static str {
1892        "snackbar"
1893    }
1894    fn group() -> &'static str {
1895        "Feedback"
1896    }
1897    fn display_name() -> &'static str {
1898        "Snackbar"
1899    }
1900    fn knobs() -> KnobSpec {
1901        KnobSpec::new()
1902            .text("trigger_label", "Trigger label", "Show notification")
1903            .text("message", "Message", "File saved successfully.")
1904    }
1905    fn variants() -> Vec<PreviewVariant> {
1906        vec![
1907            PreviewVariant::defaults("default"),
1908            PreviewVariant::knobs(
1909                "long",
1910                KnobOverrides::new().text(
1911                    "message",
1912                    "The operation completed but with warnings — review the log for details.",
1913                ),
1914            ),
1915        ]
1916    }
1917    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
1918        let trigger_label = knobs.text("trigger_label").get();
1919        let message = knobs.text("message").get();
1920        let popup_content = Panel::new()
1921            .background(SurfaceRole::Raised)
1922            .border_color(BorderRole::Default)
1923            .border_width(1.0)
1924            .corner_radius(6.0)
1925            .padding(12.0)
1926            .child(
1927                TextWidget::new(lit!(message))
1928                    .style(TextStyleRole::Body)
1929                    .color(TextRole::Primary),
1930            );
1931        Box::new(Snackbar::new(lit!(trigger_label)).content(popup_content))
1932    }
1933}
1934register_widget_catalog_at!("crates/teksilo-widgets/src/snackbar.rs", Snackbar);
1935
1936// ---------------------------------------------------------------------------
1937// Breadcrumb
1938// ---------------------------------------------------------------------------
1939
1940impl WidgetCatalog for Breadcrumb {
1941    fn id() -> &'static str {
1942        "breadcrumb"
1943    }
1944    fn group() -> &'static str {
1945        "Containers"
1946    }
1947    fn display_name() -> &'static str {
1948        "Breadcrumb"
1949    }
1950    fn variants() -> Vec<PreviewVariant> {
1951        fn build_path() -> Box<dyn Widget> {
1952            Box::new(
1953                Breadcrumb::new()
1954                    .item(BreadcrumbItem::new(lit!("Home",)))
1955                    .item(BreadcrumbItem::new(lit!("Projects",)))
1956                    .item(BreadcrumbItem::new(lit!("Teksilo",)))
1957                    .item(BreadcrumbItem::new(lit!("crates",))),
1958            )
1959        }
1960        vec![PreviewVariant::scenario("path", build_path)]
1961    }
1962    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
1963        scenario_for::<Self>(variant)
1964    }
1965}
1966register_widget_catalog_at!("crates/teksilo-widgets/src/breadcrumb.rs", Breadcrumb);
1967
1968// ---------------------------------------------------------------------------
1969// Toolbar (the chrome widget — distinct from the previewer's toolbar pane)
1970// ---------------------------------------------------------------------------
1971
1972impl WidgetCatalog for Toolbar {
1973    fn id() -> &'static str {
1974        "toolbar"
1975    }
1976    fn group() -> &'static str {
1977        "Chrome"
1978    }
1979    fn display_name() -> &'static str {
1980        "Toolbar"
1981    }
1982    fn variants() -> Vec<PreviewVariant> {
1983        fn build_default() -> Box<dyn Widget> {
1984            Box::new(
1985                Toolbar::new()
1986                    .child(Button::new(lit!("New")).variant(ButtonVariant::Ghost))
1987                    .child(Button::new(lit!("Open…")).variant(ButtonVariant::Ghost))
1988                    .child(Button::new(lit!("Save")).variant(ButtonVariant::Ghost)),
1989            )
1990        }
1991        vec![PreviewVariant::scenario("default", build_default)]
1992    }
1993    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
1994        scenario_for::<Self>(variant)
1995    }
1996}
1997register_widget_catalog_at!("crates/teksilo-widgets/src/toolbar.rs", Toolbar);
1998
1999// ---------------------------------------------------------------------------
2000// StatusBar
2001// ---------------------------------------------------------------------------
2002
2003impl WidgetCatalog for StatusBar {
2004    fn id() -> &'static str {
2005        "status_bar"
2006    }
2007    fn group() -> &'static str {
2008        "Chrome"
2009    }
2010    fn display_name() -> &'static str {
2011        "StatusBar"
2012    }
2013    fn variants() -> Vec<PreviewVariant> {
2014        fn build_default() -> Box<dyn Widget> {
2015            Box::new(
2016                StatusBar::new()
2017                    .child(
2018                        TextWidget::new(lit!("Ready"))
2019                            .style(TextStyleRole::Tiny)
2020                            .color(TextRole::Secondary),
2021                    )
2022                    .child(Spacer::new())
2023                    .child(
2024                        TextWidget::new(lit!("Ln 42, Col 17"))
2025                            .style(TextStyleRole::Tiny)
2026                            .color(TextRole::Secondary),
2027                    ),
2028            )
2029        }
2030        vec![PreviewVariant::scenario("default", build_default)]
2031    }
2032    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2033        scenario_for::<Self>(variant)
2034    }
2035}
2036register_widget_catalog_at!("crates/teksilo-widgets/src/status_bar.rs", StatusBar);
2037
2038// ---------------------------------------------------------------------------
2039// Accordion
2040// ---------------------------------------------------------------------------
2041
2042impl WidgetCatalog for Accordion {
2043    fn id() -> &'static str {
2044        "accordion"
2045    }
2046    fn group() -> &'static str {
2047        "Containers"
2048    }
2049    fn display_name() -> &'static str {
2050        "Accordion"
2051    }
2052    fn knobs() -> KnobSpec {
2053        KnobSpec::new()
2054            .text("title", "Title", "Advanced")
2055            .bool_("expanded", "Expanded", false)
2056            .text("content", "Content body", "Hidden until expanded.")
2057    }
2058    fn variants() -> Vec<PreviewVariant> {
2059        vec![
2060            PreviewVariant::defaults("collapsed"),
2061            PreviewVariant::knobs(
2062                "expanded",
2063                KnobOverrides::new()
2064                    .bool_("expanded", true)
2065                    .text("content", "Now visible because the section is expanded."),
2066            ),
2067        ]
2068    }
2069    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
2070        let title = knobs.text("title").get();
2071        let body = knobs.text("content").get();
2072        // The accordion's `expanded` Signal IS the bool knob — clicking
2073        // the header in the canvas mutates the signal, the inspector
2074        // toggle reflects it.
2075        let expanded = knobs.bool_("expanded");
2076        Box::new(Accordion::new(lit!(title), expanded).content(sample_text(&body)))
2077    }
2078}
2079register_widget_catalog_at!("crates/teksilo-widgets/src/accordion.rs", Accordion);
2080
2081// ---------------------------------------------------------------------------
2082// RadioGroup
2083// ---------------------------------------------------------------------------
2084
2085impl WidgetCatalog for RadioGroup {
2086    fn id() -> &'static str {
2087        "radio_group"
2088    }
2089    fn group() -> &'static str {
2090        "Controls"
2091    }
2092    fn display_name() -> &'static str {
2093        "RadioGroup"
2094    }
2095    fn variants() -> Vec<PreviewVariant> {
2096        fn build_default() -> Box<dyn Widget> {
2097            let selected = Signal::new(0_usize);
2098            Box::new(
2099                RadioGroup::new()
2100                    .child(RadioButton::new(0, selected.clone()).label(lit!("First")))
2101                    .child(RadioButton::new(1, selected.clone()).label(lit!("Second")))
2102                    .child(RadioButton::new(2, selected).label(lit!("Third"))),
2103            )
2104        }
2105        vec![PreviewVariant::scenario("default", build_default)]
2106    }
2107    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2108        scenario_for::<Self>(variant)
2109    }
2110}
2111register_widget_catalog_at!("crates/teksilo-widgets/src/radio_group.rs", RadioGroup);
2112
2113// ---------------------------------------------------------------------------
2114// RadioTile
2115// ---------------------------------------------------------------------------
2116impl WidgetCatalog for RadioTile {
2117    fn id() -> &'static str {
2118        "radio_tile"
2119    }
2120    fn group() -> &'static str {
2121        "Controls"
2122    }
2123    fn display_name() -> &'static str {
2124        "RadioTile"
2125    }
2126    fn knobs() -> KnobSpec {
2127        KnobSpec::new()
2128            .choice("selected", "Selected", &["Yes", "No"], 0)
2129            .opt_text("title", "Title", Some("Single file"))
2130            .opt_text(
2131                "description",
2132                "Description",
2133                Some("One .skrib archive (zip). Portable, easy to back up."),
2134            )
2135            .bool_("enabled", "Enabled", true)
2136    }
2137    fn variants() -> Vec<PreviewVariant> {
2138        vec![
2139            PreviewVariant::defaults("selected"),
2140            PreviewVariant::knobs("unselected", KnobOverrides::new().choice("selected", 1)),
2141            PreviewVariant::knobs("disabled", KnobOverrides::new().bool_("enabled", false)),
2142            PreviewVariant::knobs(
2143                "no-description",
2144                KnobOverrides::new().opt_text("description", None),
2145            ),
2146        ]
2147    }
2148    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
2149        let title = knobs.opt_text("title").get();
2150        let description = knobs.opt_text("description").get();
2151        let enabled = knobs.bool_("enabled").get();
2152        let mut tile = RadioTile::new()
2153            .selection(0, knobs.choice("selected"))
2154            .enabled(enabled);
2155        if let Some(title) = title {
2156            tile = tile.title(lit!(title));
2157        }
2158        if let Some(description) = description {
2159            tile = tile.description(lit!(description));
2160        }
2161        Box::new(FixedSize::new().width(300.0).child(tile))
2162    }
2163}
2164register_widget_catalog_at!("crates/teksilo-widgets/src/radio_tile.rs", RadioTile);
2165
2166// ---------------------------------------------------------------------------
2167// RadioTileGroup
2168// ---------------------------------------------------------------------------
2169impl WidgetCatalog for RadioTileGroup {
2170    fn id() -> &'static str {
2171        "radio_tile_group"
2172    }
2173    fn group() -> &'static str {
2174        "Controls"
2175    }
2176    fn display_name() -> &'static str {
2177        "RadioTileGroup"
2178    }
2179    fn variants() -> Vec<PreviewVariant> {
2180        fn build_row() -> Box<dyn Widget> {
2181            let selected = Signal::new(0_usize);
2182            Box::new(
2183                FixedSize::new().width(560.0).child(
2184                    RadioTileGroup::new(selected)
2185                        .tile(
2186                            RadioTile::new()
2187                                .title(lit!("Single file"))
2188                                .description(lit!(
2189                                    "One .skrib archive (zip). Portable, easy to back up."
2190                                )),
2191                        )
2192                        .tile(RadioTile::new().title(lit!("Bundle")).description(lit!(
2193                            "A folder holding every text & asset. Friendlier to version control."
2194                        )))
2195                        .layout(TileLayout::Row),
2196                ),
2197            )
2198        }
2199        fn build_grid() -> Box<dyn Widget> {
2200            let selected = Signal::new(1_usize);
2201            Box::new(
2202                FixedSize::new().width(560.0).child(
2203                    RadioTileGroup::new(selected)
2204                        .tiles((0..4).map(|i| {
2205                            RadioTile::new()
2206                                .title(lit!(format!("Option {}", i + 1)))
2207                                .description(lit!("A selectable option in the grid."))
2208                        }))
2209                        .layout(TileLayout::Grid {
2210                            min_tile_width: 240.0,
2211                        }),
2212                ),
2213            )
2214        }
2215        vec![
2216            PreviewVariant::scenario("row", build_row),
2217            PreviewVariant::scenario("grid", build_grid),
2218        ]
2219    }
2220    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2221        scenario_for::<Self>(variant)
2222    }
2223}
2224register_widget_catalog_at!(
2225    "crates/teksilo-widgets/src/radio_tile_group.rs",
2226    RadioTileGroup
2227);
2228
2229// ---------------------------------------------------------------------------
2230// SplitButton
2231// ---------------------------------------------------------------------------
2232
2233impl WidgetCatalog for SplitButton {
2234    fn id() -> &'static str {
2235        "split_button"
2236    }
2237    fn group() -> &'static str {
2238        "Controls"
2239    }
2240    fn display_name() -> &'static str {
2241        "SplitButton"
2242    }
2243    fn variants() -> Vec<PreviewVariant> {
2244        fn build_default() -> Box<dyn Widget> {
2245            Box::new(
2246                SplitButton::new_static()
2247                    .item(MenuItem::new(lit!("Save")))
2248                    .item(MenuItem::new(lit!("Save As…")))
2249                    .item(MenuItem::new(lit!("Save All"))),
2250            )
2251        }
2252        vec![PreviewVariant::scenario("default", build_default)]
2253    }
2254    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2255        scenario_for::<Self>(variant)
2256    }
2257}
2258register_widget_catalog_at!("crates/teksilo-widgets/src/split_button.rs", SplitButton);
2259
2260// =========================================================================
2261// Tier C — data-driven / structural
2262// =========================================================================
2263
2264// ---------------------------------------------------------------------------
2265// ListView
2266// ---------------------------------------------------------------------------
2267
2268impl WidgetCatalog for ListView<String> {
2269    fn id() -> &'static str {
2270        "list_view"
2271    }
2272    fn group() -> &'static str {
2273        "Data"
2274    }
2275    fn display_name() -> &'static str {
2276        "ListView"
2277    }
2278    fn variants() -> Vec<PreviewVariant> {
2279        fn build_short() -> Box<dyn Widget> {
2280            let model = teksilo_data::ListModel::from_vec(vec![
2281                "Apple".to_string(),
2282                "Banana".to_string(),
2283                "Cherry".to_string(),
2284                "Date".to_string(),
2285                "Elderberry".to_string(),
2286                "Fig".to_string(),
2287            ]);
2288            Box::new(
2289                FixedSize::new()
2290                    .width(280.0_f32)
2291                    .height(220.0_f32)
2292                    .child(ListView::new(model, |_idx, item, selected| {
2293                        Box::new(StandardListItem::new(lit!(item.clone())).selected(selected))
2294                    })),
2295            )
2296        }
2297        vec![PreviewVariant::scenario("short", build_short)]
2298    }
2299    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2300        scenario_for::<Self>(variant)
2301    }
2302}
2303register_widget_catalog_at!("crates/teksilo-widgets/src/list_view.rs", ListView<String>);
2304
2305// ---------------------------------------------------------------------------
2306// GridView
2307// ---------------------------------------------------------------------------
2308
2309impl WidgetCatalog for GridView<String> {
2310    fn id() -> &'static str {
2311        "grid_view"
2312    }
2313    fn group() -> &'static str {
2314        "Data"
2315    }
2316    fn display_name() -> &'static str {
2317        "GridView"
2318    }
2319    fn variants() -> Vec<PreviewVariant> {
2320        fn items(n: usize) -> teksilo_data::ListModel<String> {
2321            teksilo_data::ListModel::from_vec((0..n).map(|i| format!("Tile {i}")).collect())
2322        }
2323        // RectWidget is a leaf, so layer the label over it in a ZStack.
2324        fn tile_z(caption: &str, selected: bool) -> Box<dyn Widget> {
2325            let bg = if selected {
2326                SurfaceRole::AccentSubtle
2327            } else {
2328                SurfaceRole::Raised
2329            };
2330            Box::new(
2331                crate::primitives::ZStack::new()
2332                    .child(RectWidget::new().background(bg))
2333                    .child(Center::new().child(
2334                        TextWidget::new(lit!(caption.to_string())).color(TextRole::Primary),
2335                    )),
2336            )
2337        }
2338        fn framed(grid: GridView<String>) -> Box<dyn Widget> {
2339            Box::new(
2340                FixedSize::new()
2341                    .width(360.0_f32)
2342                    .height(320.0_f32)
2343                    .child(grid),
2344            )
2345        }
2346
2347        fn adaptive() -> Box<dyn Widget> {
2348            framed(
2349                GridView::new(items(40), |tc| tile_z(tc.item, tc.is_selected))
2350                    .sizing(GridSizing::Adaptive {
2351                        min_width: 90.0,
2352                        max_width: None,
2353                        height: 64.0,
2354                    })
2355                    .spacing(8.0),
2356            )
2357        }
2358        fn fixed_columns() -> Box<dyn Widget> {
2359            framed(
2360                GridView::new(items(40), |tc| tile_z(tc.item, tc.is_selected))
2361                    .column_count(4, 64.0)
2362                    .spacing(8.0),
2363            )
2364        }
2365        fn selectable() -> Box<dyn Widget> {
2366            use teksilo_data::{SelectionMode, SelectionModel};
2367            let sel = SelectionModel::new(SelectionMode::Multi);
2368            sel.select(2);
2369            framed(
2370                GridView::new(items(40), |tc| tile_z(tc.item, tc.is_selected))
2371                    .sizing(GridSizing::Adaptive {
2372                        min_width: 90.0,
2373                        max_width: None,
2374                        height: 64.0,
2375                    })
2376                    .spacing(8.0)
2377                    .selection(sel),
2378            )
2379        }
2380        fn waterfall() -> Box<dyn Widget> {
2381            // `.item_height` drives the exact per-item height, so the tile
2382            // widget itself can stay plain.
2383            framed(
2384                GridView::new(items(40), |tc| tile_z(tc.item, tc.is_selected))
2385                    .column_count(3, 64.0)
2386                    .waterfall(64.0)
2387                    .item_height(|i| 48.0 + (i % 5) as f32 * 18.0)
2388                    .spacing(8.0),
2389            )
2390        }
2391
2392        vec![
2393            PreviewVariant::scenario("adaptive", adaptive),
2394            PreviewVariant::scenario("fixed_columns", fixed_columns),
2395            PreviewVariant::scenario("selection", selectable),
2396            PreviewVariant::scenario("waterfall", waterfall),
2397        ]
2398    }
2399    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2400        scenario_for::<Self>(variant)
2401    }
2402}
2403register_widget_catalog_at!("crates/teksilo-widgets/src/grid_view.rs", GridView<String>);
2404
2405// ---------------------------------------------------------------------------
2406// TreeView
2407// ---------------------------------------------------------------------------
2408
2409impl WidgetCatalog for TreeView<String> {
2410    fn id() -> &'static str {
2411        "tree_view"
2412    }
2413    fn group() -> &'static str {
2414        "Data"
2415    }
2416    fn display_name() -> &'static str {
2417        "TreeView"
2418    }
2419    fn variants() -> Vec<PreviewVariant> {
2420        fn build_default() -> Box<dyn Widget> {
2421            let model = teksilo_data::TreeModel::<String>::new();
2422            let root = model.insert_root(0, "Project".to_string());
2423            let crates_node = model.insert_child(root, 0, "crates".to_string());
2424            model.insert_child(crates_node, 0, "teksilo-core".to_string());
2425            model.insert_child(crates_node, 1, "teksilo-widgets".to_string());
2426            model.insert_child(crates_node, 2, "teksilo-render".to_string());
2427            let docs = model.insert_child(root, 1, "docs".to_string());
2428            model.insert_child(docs, 0, "architecture.md".to_string());
2429            Box::new(FixedSize::new().width(280.0_f32).height(220.0_f32).child(
2430                TreeView::new_with_context(model, |item, entry, selected, ctx| {
2431                    Box::new(
2432                        StandardTreeItem::new(lit!(item.clone()))
2433                            .from_entry(entry)
2434                            .selected(selected)
2435                            .on_toggle_rc(ctx.toggle_callback()),
2436                    )
2437                }),
2438            ))
2439        }
2440        vec![PreviewVariant::scenario("default", build_default)]
2441    }
2442    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2443        scenario_for::<Self>(variant)
2444    }
2445}
2446register_widget_catalog_at!("crates/teksilo-widgets/src/tree_view.rs", TreeView<String>);
2447
2448// ---------------------------------------------------------------------------
2449// StandardListItem
2450// ---------------------------------------------------------------------------
2451
2452impl WidgetCatalog for StandardListItem {
2453    fn id() -> &'static str {
2454        "standard_list_item"
2455    }
2456    fn group() -> &'static str {
2457        "Data"
2458    }
2459    fn display_name() -> &'static str {
2460        "StandardListItem"
2461    }
2462    fn variants() -> Vec<PreviewVariant> {
2463        fn build_single_line() -> Box<dyn Widget> {
2464            Box::new(StandardListItem::new(lit!("Single-line item")))
2465        }
2466        fn build_with_all_primary_slots() -> Box<dyn Widget> {
2467            Box::new(
2468                StandardListItem::new(lit!("With every primary slot"))
2469                    .leading_slot(TextWidget::new(lit!("●")).color(TextRole::Accent))
2470                    .center_slot(TextWidget::new(lit!("•")).color(TextRole::Secondary))
2471                    .trailing_slot(TextWidget::new(lit!("12")).color(TextRole::Secondary)),
2472            )
2473        }
2474        fn build_two_line_with_subtitle_slots() -> Box<dyn Widget> {
2475            Box::new(
2476                StandardListItem::new(lit!("Title line"))
2477                    .subtitle(lit!("Subtitle line"))
2478                    .leading_slot(TextWidget::new(lit!("●")).color(TextRole::Accent))
2479                    .subtitle_leading_slot(TextWidget::new(lit!("•")).color(TextRole::Secondary))
2480                    .subtitle_trailing_slot(
2481                        TextWidget::new(lit!("just now")).color(TextRole::Secondary),
2482                    )
2483                    .trailing_slot(TextWidget::new(lit!("∗")).color(TextRole::Accent)),
2484            )
2485        }
2486        fn build_with_checkbox() -> Box<dyn Widget> {
2487            let checked = Signal::new(true);
2488            Box::new(StandardListItem::new(lit!("With two-state checkbox")).checkbox(checked))
2489        }
2490        fn build_with_tristate_checkbox() -> Box<dyn Widget> {
2491            use teksilo_data::CheckState;
2492            let s = Signal::new(CheckState::Indeterminate);
2493            Box::new(StandardListItem::new(lit!("With tristate checkbox")).tristate_checkbox(s))
2494        }
2495        fn build_selected() -> Box<dyn Widget> {
2496            Box::new(StandardListItem::new(lit!("Selected")).selected(true))
2497        }
2498        fn build_disabled() -> Box<dyn Widget> {
2499            Box::new(StandardListItem::new(lit!("Disabled")).enabled(false))
2500        }
2501        vec![
2502            PreviewVariant::scenario("single_line", build_single_line),
2503            PreviewVariant::scenario("all_primary_slots", build_with_all_primary_slots),
2504            PreviewVariant::scenario("two_line", build_two_line_with_subtitle_slots),
2505            PreviewVariant::scenario("checkbox", build_with_checkbox),
2506            PreviewVariant::scenario("tristate_checkbox", build_with_tristate_checkbox),
2507            PreviewVariant::scenario("selected", build_selected),
2508            PreviewVariant::scenario("disabled", build_disabled),
2509        ]
2510    }
2511    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2512        scenario_for::<Self>(variant)
2513    }
2514}
2515register_widget_catalog_at!(
2516    "crates/teksilo-widgets/src/standard_item.rs",
2517    StandardListItem
2518);
2519
2520// ---------------------------------------------------------------------------
2521// StandardTreeItem
2522// ---------------------------------------------------------------------------
2523
2524impl WidgetCatalog for StandardTreeItem {
2525    fn id() -> &'static str {
2526        "standard_tree_item"
2527    }
2528    fn group() -> &'static str {
2529        "Data"
2530    }
2531    fn display_name() -> &'static str {
2532        "StandardTreeItem"
2533    }
2534    fn variants() -> Vec<PreviewVariant> {
2535        fn build_collapsed_branch() -> Box<dyn Widget> {
2536            Box::new(
2537                StandardTreeItem::new(lit!("Folder (collapsed)"))
2538                    .depth(0)
2539                    .has_children(true)
2540                    .is_expanded(false),
2541            )
2542        }
2543        fn build_expanded_branch() -> Box<dyn Widget> {
2544            Box::new(
2545                StandardTreeItem::new(lit!("Folder (expanded)"))
2546                    .depth(0)
2547                    .has_children(true)
2548                    .is_expanded(true),
2549            )
2550        }
2551        fn build_leaf_indented() -> Box<dyn Widget> {
2552            Box::new(
2553                StandardTreeItem::new(lit!("Deep leaf"))
2554                    .depth(2)
2555                    .has_children(false),
2556            )
2557        }
2558        fn build_with_tristate_checkbox() -> Box<dyn Widget> {
2559            use teksilo_data::CheckState;
2560            let s = Signal::new(CheckState::Indeterminate);
2561            Box::new(
2562                StandardTreeItem::new(lit!("Folder with tristate"))
2563                    .depth(1)
2564                    .has_children(true)
2565                    .is_expanded(true)
2566                    .tristate_checkbox(s),
2567            )
2568        }
2569        fn build_two_line() -> Box<dyn Widget> {
2570            Box::new(
2571                StandardTreeItem::new(lit!("Folder"))
2572                    .subtitle(lit!("3 items · last week"))
2573                    .depth(0)
2574                    .has_children(true)
2575                    .is_expanded(false)
2576                    .subtitle_trailing_slot(TextWidget::new(lit!("3")).color(TextRole::Secondary)),
2577            )
2578        }
2579        vec![
2580            PreviewVariant::scenario("collapsed_branch", build_collapsed_branch),
2581            PreviewVariant::scenario("expanded_branch", build_expanded_branch),
2582            PreviewVariant::scenario("leaf_indented", build_leaf_indented),
2583            PreviewVariant::scenario("tristate_checkbox", build_with_tristate_checkbox),
2584            PreviewVariant::scenario("two_line", build_two_line),
2585        ]
2586    }
2587    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2588        scenario_for::<Self>(variant)
2589    }
2590}
2591register_widget_catalog_at!(
2592    "crates/teksilo-widgets/src/standard_item.rs",
2593    StandardTreeItem
2594);
2595
2596// ---------------------------------------------------------------------------
2597// MenuList
2598// ---------------------------------------------------------------------------
2599
2600impl WidgetCatalog for MenuList {
2601    fn id() -> &'static str {
2602        "menu_list"
2603    }
2604    fn group() -> &'static str {
2605        "Menus"
2606    }
2607    fn display_name() -> &'static str {
2608        "MenuList"
2609    }
2610    fn variants() -> Vec<PreviewVariant> {
2611        fn build_default() -> Box<dyn Widget> {
2612            Box::new(
2613                MenuList::new()
2614                    .item(MenuItem::new(lit!("New")))
2615                    .item(MenuItem::new(lit!("Open…")))
2616                    .item(MenuItem::new(lit!("Save")))
2617                    .item(MenuItem::new(lit!("Save As…")))
2618                    .item(MenuItem::new(lit!("Close"))),
2619            )
2620        }
2621        vec![PreviewVariant::scenario("default", build_default)]
2622    }
2623    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2624        scenario_for::<Self>(variant)
2625    }
2626}
2627register_widget_catalog_at!("crates/teksilo-widgets/src/menu_list.rs", MenuList);
2628
2629// ---------------------------------------------------------------------------
2630// ScrollArea
2631// ---------------------------------------------------------------------------
2632
2633impl WidgetCatalog for ScrollArea {
2634    fn id() -> &'static str {
2635        "scroll_area"
2636    }
2637    fn group() -> &'static str {
2638        "Data"
2639    }
2640    fn display_name() -> &'static str {
2641        "ScrollArea"
2642    }
2643    fn variants() -> Vec<PreviewVariant> {
2644        fn build_long_content() -> Box<dyn Widget> {
2645            let mut col = VStack::new().spacing(4.0);
2646            for i in 1..=40 {
2647                col = col.child(
2648                    Padding::symmetric(4.0, 8.0).child(
2649                        TextWidget::new(lit!(format!("Row {}", i)))
2650                            .style(TextStyleRole::Body)
2651                            .color(TextRole::Primary),
2652                    ),
2653                );
2654            }
2655            Box::new(
2656                FixedSize::new()
2657                    .width(280.0_f32)
2658                    .height(180.0_f32)
2659                    .child(ScrollArea::new().child(col)),
2660            )
2661        }
2662        vec![PreviewVariant::scenario("long-content", build_long_content)]
2663    }
2664    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2665        scenario_for::<Self>(variant)
2666    }
2667    fn icon() -> Option<Box<dyn Widget>> {
2668        Some(icons::scroll_area())
2669    }
2670    fn category() -> WidgetCategory {
2671        WidgetCategory::ContainerA
2672    }
2673    fn build_with_children(
2674        _variant: &str,
2675        _knobs: &KnobValues,
2676        children: Vec<SlottedChild>,
2677    ) -> Box<dyn Widget> {
2678        match children.into_iter().next() {
2679            Some(c) => Box::new(ScrollArea::from_id(c.id)),
2680            None => Box::new(ScrollArea::new()),
2681        }
2682    }
2683}
2684register_widget_catalog_at!("crates/teksilo-widgets/src/scroll_area.rs", ScrollArea);
2685
2686// ---------------------------------------------------------------------------
2687// Splitter
2688// ---------------------------------------------------------------------------
2689
2690impl WidgetCatalog for Splitter {
2691    fn id() -> &'static str {
2692        "splitter"
2693    }
2694    fn group() -> &'static str {
2695        "Containers"
2696    }
2697    fn display_name() -> &'static str {
2698        "Splitter"
2699    }
2700    fn variants() -> Vec<PreviewVariant> {
2701        fn build_horizontal() -> Box<dyn Widget> {
2702            let left = Panel::new()
2703                .background(SurfaceRole::Sunken)
2704                .padding(12.0)
2705                .child(sample_text("Left pane"));
2706            let right = Panel::new()
2707                .background(SurfaceRole::Raised)
2708                .padding(12.0)
2709                .child(sample_text("Right pane"));
2710            Box::new(
2711                FixedSize::new().width(420.0_f32).height(220.0_f32).child(
2712                    Splitter::new(SplitterModel::new(2, Orientation::Horizontal))
2713                        .pane(left)
2714                        .pane(right),
2715                ),
2716            )
2717        }
2718        fn build_three_pane() -> Box<dyn Widget> {
2719            let model = SplitterModel::from_panes(
2720                vec![
2721                    PaneDescriptor::new()
2722                        .size(120.0)
2723                        .collapsible(true)
2724                        .stretch(0.0),
2725                    PaneDescriptor::new().stretch(1.0),
2726                    PaneDescriptor::new()
2727                        .size(120.0)
2728                        .collapsible(true)
2729                        .stretch(0.0),
2730                ],
2731                Orientation::Horizontal,
2732            );
2733            let pane = |label: &str, role| {
2734                Panel::new()
2735                    .background(role)
2736                    .padding(12.0)
2737                    .child(sample_text(label))
2738            };
2739            Box::new(
2740                FixedSize::new().width(480.0_f32).height(220.0_f32).child(
2741                    Splitter::new(model)
2742                        .pane(pane("Sidebar", SurfaceRole::Sunken))
2743                        .pane(pane("Editor", SurfaceRole::Raised))
2744                        .pane(pane("Inspector", SurfaceRole::Sunken)),
2745                ),
2746            )
2747        }
2748        vec![
2749            PreviewVariant::scenario("horizontal", build_horizontal),
2750            PreviewVariant::scenario("three_pane", build_three_pane),
2751        ]
2752    }
2753    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2754        scenario_for::<Self>(variant)
2755    }
2756}
2757register_widget_catalog_at!("crates/teksilo-widgets/src/splitter.rs", Splitter);
2758
2759// ---------------------------------------------------------------------------
2760// TabWidget
2761// ---------------------------------------------------------------------------
2762
2763impl WidgetCatalog for TabWidget {
2764    fn id() -> &'static str {
2765        "tab_widget"
2766    }
2767    fn group() -> &'static str {
2768        "Containers"
2769    }
2770    fn display_name() -> &'static str {
2771        "TabWidget"
2772    }
2773    fn variants() -> Vec<PreviewVariant> {
2774        fn build_three_tabs() -> Box<dyn Widget> {
2775            use crate::tab_widget::{TabId, TabInfo};
2776            let selected: Signal<Option<TabId>> = Signal::new(None);
2777            Box::new(
2778                FixedSize::new().width(420.0_f32).height(220.0_f32).child(
2779                    TabWidget::new(selected)
2780                        .static_tab(
2781                            TabInfo::new().title(lit!("Overview")),
2782                            Center::new().child(sample_text("Overview tab content")),
2783                        )
2784                        .static_tab(
2785                            TabInfo::new().title(lit!("Details")),
2786                            Center::new().child(sample_text("Details tab content")),
2787                        )
2788                        .static_tab(
2789                            TabInfo::new().title(lit!("Settings")),
2790                            Center::new().child(sample_text("Settings tab content")),
2791                        ),
2792                ),
2793            )
2794        }
2795        vec![PreviewVariant::scenario("three-tabs", build_three_tabs)]
2796    }
2797    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2798        scenario_for::<Self>(variant)
2799    }
2800}
2801register_widget_catalog_at!("crates/teksilo-widgets/src/tab_widget.rs", TabWidget);
2802
2803// ---------------------------------------------------------------------------
2804// ToolBox
2805// ---------------------------------------------------------------------------
2806
2807impl WidgetCatalog for ToolBox {
2808    fn id() -> &'static str {
2809        "tool_box"
2810    }
2811    fn group() -> &'static str {
2812        "Containers"
2813    }
2814    fn display_name() -> &'static str {
2815        "ToolBox"
2816    }
2817    fn variants() -> Vec<PreviewVariant> {
2818        fn build_three_items() -> Box<dyn Widget> {
2819            let selected = Signal::new(0_usize);
2820            Box::new(
2821                FixedSize::new().width(280.0_f32).height(280.0_f32).child(
2822                    ToolBox::new(selected)
2823                        .item(
2824                            lit!("General"),
2825                            Padding::uniform(12.0).child(sample_text("General settings")),
2826                        )
2827                        .item(
2828                            lit!("Editor"),
2829                            Padding::uniform(12.0).child(sample_text("Editor settings")),
2830                        )
2831                        .item(
2832                            lit!("Keymap"),
2833                            Padding::uniform(12.0).child(sample_text("Keymap settings")),
2834                        ),
2835                ),
2836            )
2837        }
2838        vec![PreviewVariant::scenario("three-items", build_three_items)]
2839    }
2840    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2841        scenario_for::<Self>(variant)
2842    }
2843}
2844register_widget_catalog_at!("crates/teksilo-widgets/src/tool_box.rs", ToolBox);
2845
2846// ---------------------------------------------------------------------------
2847// Repeater
2848// ---------------------------------------------------------------------------
2849
2850impl WidgetCatalog for crate::Repeater<String> {
2851    fn id() -> &'static str {
2852        "repeater"
2853    }
2854    fn group() -> &'static str {
2855        "Data"
2856    }
2857    fn display_name() -> &'static str {
2858        "Repeater"
2859    }
2860    fn variants() -> Vec<PreviewVariant> {
2861        fn build_default() -> Box<dyn Widget> {
2862            let model = teksilo_data::ListModel::from_vec(vec![
2863                "Alpha".to_string(),
2864                "Beta".to_string(),
2865                "Gamma".to_string(),
2866                "Delta".to_string(),
2867            ]);
2868            Box::new(
2869                crate::Repeater::new(model, |item| {
2870                    Box::new(
2871                        Padding::symmetric(4.0, 8.0).child(
2872                            TextWidget::new(lit!(item.clone()))
2873                                .style(TextStyleRole::Body)
2874                                .color(TextRole::Primary),
2875                        ),
2876                    )
2877                })
2878                .spacing(4.0),
2879            )
2880        }
2881        vec![PreviewVariant::scenario("default", build_default)]
2882    }
2883    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2884        scenario_for::<Self>(variant)
2885    }
2886}
2887register_widget_catalog_at!(
2888    "crates/teksilo-widgets/src/repeater.rs",
2889    crate::Repeater<String>
2890);
2891
2892// =========================================================================
2893// Helpers
2894// =========================================================================
2895
2896/// Resolve a variant name to its scenario builder result. Used by
2897/// every Tier B/C `build(...)` method whose variants are all
2898/// `Scenario`-shaped — saves ~6 lines per impl. Falls back to the
2899/// first declared variant when `name` is unknown.
2900fn scenario_for<W: WidgetCatalog>(name: &str) -> Box<dyn Widget> {
2901    let variants = W::variants();
2902    let chosen = variants
2903        .iter()
2904        .find(|v| v.name() == name)
2905        .or_else(|| variants.first());
2906    match chosen {
2907        Some(PreviewVariant::Scenario { builder, .. }) => builder(),
2908        _ => Box::new(
2909            TextWidget::new(lit!(format!("(no scenario for variant '{}')", name)))
2910                .style(TextStyleRole::Small)
2911                .color(TextRole::Secondary),
2912        ),
2913    }
2914}
2915
2916// =========================================================================
2917// Color picker family (HexColorInput, ColorPicker, ColorEdit)
2918// =========================================================================
2919
2920mod color_family {
2921    use super::*;
2922    use crate::{ColorEdit, ColorPicker, ColorPickerLayout, HexColorInput};
2923    use teksilo_tokens::Color;
2924
2925    impl WidgetCatalog for HexColorInput {
2926        fn id() -> &'static str {
2927            "hex-color-input"
2928        }
2929        fn group() -> &'static str {
2930            "Color"
2931        }
2932        fn display_name() -> &'static str {
2933            "HexColorInput"
2934        }
2935        fn knobs() -> KnobSpec {
2936            KnobSpec::new()
2937        }
2938        fn variants() -> Vec<PreviewVariant> {
2939            fn default_var() -> Box<dyn Widget> {
2940                Box::new(HexColorInput::new(Signal::new(Color::from_hex("#3584E4"))))
2941            }
2942            fn alpha_var() -> Box<dyn Widget> {
2943                Box::new(
2944                    HexColorInput::new(Signal::new(Color::from_rgba(1.0, 0.5, 0.0, 0.6)))
2945                        .alpha_enabled(true),
2946                )
2947            }
2948            vec![
2949                PreviewVariant::scenario("default", default_var),
2950                PreviewVariant::scenario("with-alpha", alpha_var),
2951            ]
2952        }
2953        fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2954            scenario_for::<Self>(variant)
2955        }
2956    }
2957    register_widget_catalog_at!(
2958        "crates/teksilo-widgets/src/hex_color_input.rs",
2959        HexColorInput
2960    );
2961
2962    impl WidgetCatalog for ColorPicker {
2963        fn id() -> &'static str {
2964            "color-picker"
2965        }
2966        fn group() -> &'static str {
2967            "Color"
2968        }
2969        fn display_name() -> &'static str {
2970            "ColorPicker"
2971        }
2972        fn knobs() -> KnobSpec {
2973            KnobSpec::new()
2974        }
2975        fn variants() -> Vec<PreviewVariant> {
2976            fn default_var() -> Box<dyn Widget> {
2977                Box::new(ColorPicker::new(Signal::new(Color::from_hex("#3584E4"))))
2978            }
2979            fn with_alpha() -> Box<dyn Widget> {
2980                Box::new(
2981                    ColorPicker::new(Signal::new(Color::from_rgba(0.21, 0.66, 0.40, 0.5)))
2982                        .alpha_enabled(true),
2983                )
2984            }
2985            fn compact() -> Box<dyn Widget> {
2986                Box::new(
2987                    ColorPicker::new(Signal::new(Color::from_hex("#E91E63")))
2988                        .layout(ColorPickerLayout::Compact),
2989                )
2990            }
2991            fn wide() -> Box<dyn Widget> {
2992                Box::new(
2993                    ColorPicker::new(Signal::new(Color::from_hex("#FF9800")))
2994                        .alpha_enabled(true)
2995                        .layout(ColorPickerLayout::Wide)
2996                        .show_hsv_spinners(true),
2997                )
2998            }
2999            vec![
3000                PreviewVariant::scenario("default", default_var),
3001                PreviewVariant::scenario("with-alpha", with_alpha),
3002                PreviewVariant::scenario("compact", compact),
3003                PreviewVariant::scenario("wide", wide),
3004            ]
3005        }
3006        fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
3007            scenario_for::<Self>(variant)
3008        }
3009    }
3010    register_widget_catalog_at!("crates/teksilo-widgets/src/color_picker.rs", ColorPicker);
3011
3012    impl WidgetCatalog for ColorEdit {
3013        fn id() -> &'static str {
3014            "color-edit"
3015        }
3016        fn group() -> &'static str {
3017            "Color"
3018        }
3019        fn display_name() -> &'static str {
3020            "ColorEdit"
3021        }
3022        fn knobs() -> KnobSpec {
3023            KnobSpec::new()
3024        }
3025        fn variants() -> Vec<PreviewVariant> {
3026            fn default_var() -> Box<dyn Widget> {
3027                Box::new(ColorEdit::new(Signal::new(Color::from_hex("#3584E4"))))
3028            }
3029            fn with_alpha() -> Box<dyn Widget> {
3030                Box::new(
3031                    ColorEdit::new(Signal::new(Color::from_rgba(0.92, 0.27, 0.18, 0.6)))
3032                        .alpha_enabled(true),
3033                )
3034            }
3035            fn no_hex_in_trigger() -> Box<dyn Widget> {
3036                Box::new(
3037                    ColorEdit::new(Signal::new(Color::from_hex("#9C27B0")))
3038                        .show_hex_in_trigger(false),
3039                )
3040            }
3041            fn nullable_var() -> Box<dyn Widget> {
3042                let v: Signal<Option<Color>> = Signal::new(None);
3043                Box::new(ColorEdit::nullable(v))
3044            }
3045            vec![
3046                PreviewVariant::scenario("default", default_var),
3047                PreviewVariant::scenario("with-alpha", with_alpha),
3048                PreviewVariant::scenario("no-hex-in-trigger", no_hex_in_trigger),
3049                PreviewVariant::scenario("nullable", nullable_var),
3050            ]
3051        }
3052        fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
3053            scenario_for::<Self>(variant)
3054        }
3055    }
3056    register_widget_catalog_at!("crates/teksilo-widgets/src/color_edit.rs", ColorEdit);
3057}
3058
3059// =========================================================================
3060// Secure input family (PasswordField)
3061// =========================================================================
3062
3063mod secure_input_family {
3064    use super::*;
3065    use crate::{EchoMode, PasswordField, RevealMode};
3066
3067    impl WidgetCatalog for PasswordField {
3068        fn id() -> &'static str {
3069            "password-field"
3070        }
3071        fn group() -> &'static str {
3072            "Inputs"
3073        }
3074        fn display_name() -> &'static str {
3075            "PasswordField"
3076        }
3077        fn knobs() -> KnobSpec {
3078            KnobSpec::new()
3079                .text("placeholder", "Placeholder", "Enter your password")
3080                .text("text", "Initial text", "hunter2")
3081                .choice(
3082                    "echo_mode",
3083                    "Echo mode",
3084                    &["Masked", "NoEcho", "RevealWhileTyping"],
3085                    0,
3086                )
3087                .choice(
3088                    "reveal_mode",
3089                    "Reveal button",
3090                    &["Toggle", "Hold", "None"],
3091                    0,
3092                )
3093                .bool_("enabled", "Enabled", true)
3094                .bool_("caps_warning", "Caps Lock warning", true)
3095        }
3096        fn variants() -> Vec<PreviewVariant> {
3097            vec![
3098                PreviewVariant::defaults("default"),
3099                PreviewVariant::knobs(
3100                    "reveal-while-typing",
3101                    KnobOverrides::new().choice("echo_mode", 2),
3102                ),
3103                PreviewVariant::knobs(
3104                    "hold-to-reveal",
3105                    KnobOverrides::new().choice("reveal_mode", 1),
3106                ),
3107                PreviewVariant::knobs("no-echo", KnobOverrides::new().choice("echo_mode", 1)),
3108                PreviewVariant::knobs(
3109                    "no-reveal-button",
3110                    KnobOverrides::new().choice("reveal_mode", 2),
3111                ),
3112                PreviewVariant::knobs("disabled", KnobOverrides::new().bool_("enabled", false)),
3113            ]
3114        }
3115        fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
3116            let placeholder = knobs.text("placeholder").get();
3117            let initial = knobs.text("text").get();
3118            let echo = match knobs.choice("echo_mode").get() {
3119                1 => EchoMode::NoEcho,
3120                2 => EchoMode::RevealWhileTyping,
3121                _ => EchoMode::Masked,
3122            };
3123            let reveal = match knobs.choice("reveal_mode").get() {
3124                1 => RevealMode::Hold,
3125                2 => RevealMode::None,
3126                _ => RevealMode::Toggle,
3127            };
3128            let enabled = knobs.bool_("enabled").get();
3129            let caps = knobs.bool_("caps_warning").get();
3130            Box::new(
3131                PasswordField::new(Signal::new(initial))
3132                    .label(lit!("Password"))
3133                    .placeholder(lit!(placeholder))
3134                    .echo_mode(echo)
3135                    .reveal_mode(reveal)
3136                    .enabled(enabled)
3137                    .caps_lock_warning(caps),
3138            )
3139        }
3140    }
3141    register_widget_catalog_at!(
3142        "crates/teksilo-widgets/src/password_field.rs",
3143        PasswordField
3144    );
3145}
3146
3147#[cfg(all(test, feature = "preview"))]
3148mod build_with_children_tests {
3149    use super::*;
3150    use teksilo_canvas::SizeProposal;
3151    use teksilo_core::widget_tree::WidgetTree;
3152    use teksilo_preview::{CatalogEntry, KnobValues, SlottedChild, WidgetCategory, find_by_id};
3153
3154    fn knobs_for(entry: &dyn CatalogEntry) -> KnobValues {
3155        KnobValues::from_spec(&entry.knobs(), None)
3156    }
3157
3158    /// Collect every descendant id under `root` (exclusive).
3159    fn descendants(
3160        tree: &WidgetTree,
3161        root: teksilo_core::widget_id::WidgetId,
3162    ) -> Vec<teksilo_core::widget_id::WidgetId> {
3163        let mut out = Vec::new();
3164        let mut stack = vec![root];
3165        while let Some(n) = stack.pop() {
3166            for ch in tree.children(n) {
3167                out.push(ch);
3168                stack.push(ch);
3169            }
3170        }
3171        out
3172    }
3173
3174    #[test]
3175    fn leaf_button_default_ignores_children() {
3176        let entry = find_by_id("button").expect("button registered");
3177        assert_eq!(entry.category(), WidgetCategory::Leaf);
3178        assert!(entry.icon().is_some());
3179        let knobs = knobs_for(entry);
3180        let mut tree = WidgetTree::new();
3181        let stray = tree.add(TextWidget::new(lit!("x")));
3182        let w = entry.build_with_children(
3183            "default",
3184            &knobs,
3185            vec![SlottedChild {
3186                slot: None,
3187                id: stray,
3188            }],
3189        );
3190        let id = tree.add_boxed(w);
3191        tree.layout(SizeProposal::exact(400.0, 200.0));
3192        // The leaf default ignores injected children — the stray is not adopted.
3193        assert!(!descendants(&tree, id).contains(&stray));
3194    }
3195
3196    #[test]
3197    fn vstack_container_a_wires_ordered_children() {
3198        let entry = find_by_id("vstack").expect("vstack registered");
3199        assert_eq!(entry.category(), WidgetCategory::ContainerA);
3200        assert!(entry.icon().is_some());
3201        let knobs = knobs_for(entry);
3202        let mut tree = WidgetTree::new();
3203        let a = tree.add(TextWidget::new(lit!("A")));
3204        let b = tree.add(TextWidget::new(lit!("B")));
3205        let c = tree.add(TextWidget::new(lit!("C")));
3206        let w = entry.build_with_children(
3207            "default",
3208            &knobs,
3209            vec![
3210                SlottedChild { slot: None, id: a },
3211                SlottedChild { slot: None, id: b },
3212                SlottedChild { slot: None, id: c },
3213            ],
3214        );
3215        let id = tree.add_boxed(w);
3216        tree.layout(SizeProposal::exact(400.0, 300.0));
3217        assert_eq!(tree.children(id), vec![a, b, c]);
3218    }
3219
3220    #[test]
3221    fn card_container_b_routes_named_slots() {
3222        let entry = find_by_id("card").expect("card registered");
3223        assert_eq!(entry.category(), WidgetCategory::ContainerB);
3224        assert_eq!(entry.slots(), &["header", "content", "footer"][..]);
3225        let knobs = knobs_for(entry);
3226        let mut tree = WidgetTree::new();
3227        let header = tree.add(TextWidget::new(lit!("H")));
3228        let content = tree.add(TextWidget::new(lit!("C")));
3229        let footer = tree.add(TextWidget::new(lit!("F")));
3230        let w = entry.build_with_children(
3231            "default",
3232            &knobs,
3233            vec![
3234                SlottedChild {
3235                    slot: Some("header".into()),
3236                    id: header,
3237                },
3238                SlottedChild {
3239                    slot: Some("content".into()),
3240                    id: content,
3241                },
3242                SlottedChild {
3243                    slot: Some("footer".into()),
3244                    id: footer,
3245                },
3246            ],
3247        );
3248        let id = tree.add_boxed(w);
3249        tree.layout(SizeProposal::exact(400.0, 300.0));
3250        let all = descendants(&tree, id);
3251        assert!(all.contains(&header), "header slot wired");
3252        assert!(all.contains(&content), "content slot wired");
3253        assert!(all.contains(&footer), "footer slot wired");
3254    }
3255
3256    #[test]
3257    fn curated_widgets_have_icons() {
3258        for id in [
3259            "vstack",
3260            "hstack",
3261            "zstack",
3262            "grid",
3263            "padding",
3264            "expand",
3265            "center",
3266            "spacer",
3267            "button",
3268            "text_widget",
3269            "checkbox",
3270            "text_input",
3271            "toggle",
3272            "combo_box",
3273            "slider",
3274            "card",
3275            "panel",
3276            "scroll_area",
3277        ] {
3278            let entry = find_by_id(id).unwrap_or_else(|| panic!("{id} registered"));
3279            assert!(entry.icon().is_some(), "{id} should have an icon");
3280        }
3281    }
3282}