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.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.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.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.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(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(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(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' interesting states are structural ("with header +
1566// footer", "expanded with content", "two segments"), so their
1567// `variants()` are hand-authored: a scenario builder where no flat knob
1568// surface fits (IconButton, Breadcrumb, Toolbar, StatusBar, RadioGroup,
1569// RadioTileGroup, SplitButton), knob presets where one does (Card,
1570// Panel, GroupBox, GroupHeader, Snackbar, Accordion, RadioTile).
1571
1572// ---------------------------------------------------------------------------
1573// Card
1574// ---------------------------------------------------------------------------
1575
1576fn sample_text(label: &str) -> TextWidget {
1577    TextWidget::new(lit!(label))
1578        .style(TextStyleRole::Body)
1579        .color(TextRole::Primary)
1580}
1581
1582impl WidgetCatalog for Card {
1583    fn id() -> &'static str {
1584        "card"
1585    }
1586    fn group() -> &'static str {
1587        "Containers"
1588    }
1589    fn display_name() -> &'static str {
1590        "Card"
1591    }
1592    fn knobs() -> KnobSpec {
1593        KnobSpec::new()
1594            .text("title", "Title", "Card title")
1595            .text(
1596                "body",
1597                "Body",
1598                "Card body text. Cards group related controls into a labelled rectangular region.",
1599            )
1600            .bool_("show_header", "Show header", true)
1601            .bool_("show_footer", "Show footer", false)
1602            .surface_role("background", "Background", SurfaceRole::Main)
1603            .f32_("corner_radius", "Corner radius", 8.0, 0.0, 32.0)
1604            .f32_("padding", "Padding", 16.0, 0.0, 48.0)
1605    }
1606    fn variants() -> Vec<PreviewVariant> {
1607        vec![
1608            PreviewVariant::defaults("default"),
1609            PreviewVariant::knobs(
1610                "with-footer",
1611                KnobOverrides::new()
1612                    .text("title", "Settings")
1613                    .text("body", "Configure the application settings here.")
1614                    .bool_("show_footer", true),
1615            ),
1616            PreviewVariant::knobs(
1617                "headerless",
1618                KnobOverrides::new().bool_("show_header", false),
1619            ),
1620            PreviewVariant::knobs(
1621                "raised",
1622                KnobOverrides::new().surface_role("background", SurfaceRole::Raised),
1623            ),
1624        ]
1625    }
1626    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
1627        let mut card = Card::new()
1628            .background(knobs.surface_role("background"))
1629            .corner_radius(knobs.f32_("corner_radius").get())
1630            .padding(knobs.f32_("padding").get())
1631            .content(
1632                TextWidget::new(lit!(knobs.text("body").get()))
1633                    .style(TextStyleRole::Body)
1634                    .color(TextRole::Primary),
1635            );
1636        if knobs.bool_("show_header").get() {
1637            card = card.header(
1638                TextWidget::new(lit!(knobs.text("title").get()))
1639                    .style(TextStyleRole::BodyBold)
1640                    .color(TextRole::Primary),
1641            );
1642        }
1643        if knobs.bool_("show_footer").get() {
1644            card = card.footer(
1645                HStack::new()
1646                    .spacing(8.0)
1647                    .child(Spacer::new())
1648                    .child(Button::new(lit!("Cancel")).variant(ButtonVariant::Plain))
1649                    .child(Button::new(lit!("Save")).variant(ButtonVariant::Filled)),
1650            );
1651        }
1652        Box::new(card)
1653    }
1654    fn icon() -> Option<Box<dyn Widget>> {
1655        Some(icons::card())
1656    }
1657    fn category() -> WidgetCategory {
1658        WidgetCategory::ContainerB
1659    }
1660    fn slots() -> &'static [&'static str] {
1661        &["header", "content", "footer"]
1662    }
1663    fn build_with_children(
1664        _variant: &str,
1665        knobs: &KnobValues,
1666        children: Vec<SlottedChild>,
1667    ) -> Box<dyn Widget> {
1668        let mut card = Card::new()
1669            .background(knobs.surface_role("background"))
1670            .corner_radius(knobs.f32_("corner_radius").get())
1671            .padding(knobs.f32_("padding").get());
1672        for c in children {
1673            match c.slot.as_deref() {
1674                Some("header") => card = card.header(c.id),
1675                Some("footer") => card = card.footer(c.id),
1676                _ => card = card.content(c.id),
1677            }
1678        }
1679        Box::new(card)
1680    }
1681}
1682register_widget_catalog_at!("crates/teksilo-widgets/src/card.rs", Card);
1683
1684// ---------------------------------------------------------------------------
1685// Panel
1686// ---------------------------------------------------------------------------
1687
1688impl WidgetCatalog for Panel {
1689    fn id() -> &'static str {
1690        "panel"
1691    }
1692    fn group() -> &'static str {
1693        "Containers"
1694    }
1695    fn display_name() -> &'static str {
1696        "Panel"
1697    }
1698    fn knobs() -> KnobSpec {
1699        KnobSpec::new()
1700            .surface_role("background", "Background", SurfaceRole::Raised)
1701            .border_role("border_color", "Border colour", BorderRole::Default)
1702            .f32_("border_width", "Border width", 1.0, 0.0, 4.0)
1703            .f32_("corner_radius", "Corner radius", 6.0, 0.0, 32.0)
1704            .f32_("padding", "Padding", 16.0, 0.0, 48.0)
1705            .text("content", "Sample content", "Panel content")
1706    }
1707    fn variants() -> Vec<PreviewVariant> {
1708        vec![
1709            PreviewVariant::defaults("default"),
1710            PreviewVariant::knobs(
1711                "accent",
1712                KnobOverrides::new()
1713                    .surface_role("background", SurfaceRole::AccentSubtle)
1714                    .border_role("border_color", BorderRole::Accent)
1715                    .text("content", "Accent panel"),
1716            ),
1717            PreviewVariant::knobs(
1718                "sunken",
1719                KnobOverrides::new()
1720                    .surface_role("background", SurfaceRole::Sunken)
1721                    .text("content", "Sunken panel"),
1722            ),
1723            PreviewVariant::knobs("no-border", KnobOverrides::new().f32_("border_width", 0.0)),
1724            PreviewVariant::knobs("rounded", KnobOverrides::new().f32_("corner_radius", 16.0)),
1725        ]
1726    }
1727    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
1728        Box::new(
1729            Panel::new()
1730                .background(knobs.surface_role("background"))
1731                .border_color(knobs.border_role("border_color"))
1732                .border_width(knobs.f32_("border_width").get())
1733                .corner_radius(knobs.f32_("corner_radius").get())
1734                .padding(knobs.f32_("padding").get())
1735                .child(sample_text(&knobs.text("content").get())),
1736        )
1737    }
1738    fn icon() -> Option<Box<dyn Widget>> {
1739        Some(icons::panel())
1740    }
1741    fn category() -> WidgetCategory {
1742        WidgetCategory::ContainerA
1743    }
1744    fn build_with_children(
1745        _variant: &str,
1746        knobs: &KnobValues,
1747        children: Vec<SlottedChild>,
1748    ) -> Box<dyn Widget> {
1749        let mut p = Panel::new()
1750            .background(knobs.surface_role("background"))
1751            .border_color(knobs.border_role("border_color"))
1752            .border_width(knobs.f32_("border_width").get())
1753            .corner_radius(knobs.f32_("corner_radius").get())
1754            .padding(knobs.f32_("padding").get());
1755        if let Some(c) = children.into_iter().next() {
1756            p = p.child(c.id);
1757        }
1758        Box::new(p)
1759    }
1760}
1761register_widget_catalog_at!("crates/teksilo-widgets/src/panel.rs", Panel);
1762
1763// ---------------------------------------------------------------------------
1764// GroupBox
1765// ---------------------------------------------------------------------------
1766
1767impl WidgetCatalog for GroupBox {
1768    fn id() -> &'static str {
1769        "group_box"
1770    }
1771    fn group() -> &'static str {
1772        "Containers"
1773    }
1774    fn display_name() -> &'static str {
1775        "GroupBox"
1776    }
1777    fn knobs() -> KnobSpec {
1778        KnobSpec::new().text("title", "Title", "Notifications")
1779    }
1780    fn variants() -> Vec<PreviewVariant> {
1781        vec![
1782            PreviewVariant::defaults("default"),
1783            PreviewVariant::knobs("alt-title", KnobOverrides::new().text("title", "Privacy")),
1784        ]
1785    }
1786    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
1787        Box::new(
1788            GroupBox::new(lit!(knobs.text("title").get(),)).child(
1789                VStack::new()
1790                    .spacing(8.0)
1791                    .child(Checkbox::new(Signal::new(true)).label(lit!("Sounds")))
1792                    .child(Checkbox::new(Signal::new(false)).label(lit!("Badges")))
1793                    .child(Checkbox::new(Signal::new(true)).label(lit!("Banners"))),
1794            ),
1795        )
1796    }
1797}
1798register_widget_catalog_at!("crates/teksilo-widgets/src/group_box.rs", GroupBox);
1799
1800// ---------------------------------------------------------------------------
1801// GroupHeader
1802// ---------------------------------------------------------------------------
1803
1804impl WidgetCatalog for GroupHeader {
1805    fn id() -> &'static str {
1806        "group_header"
1807    }
1808    fn group() -> &'static str {
1809        "Containers"
1810    }
1811    fn display_name() -> &'static str {
1812        "GroupHeader"
1813    }
1814    fn knobs() -> KnobSpec {
1815        KnobSpec::new().text("label", "Label", "Section title")
1816    }
1817    fn variants() -> Vec<PreviewVariant> {
1818        vec![
1819            PreviewVariant::defaults("default"),
1820            // Demonstrates the role-based styling API: a bold, accent-colored
1821            // header that tracks runtime theme changes.
1822            PreviewVariant::defaults("accent"),
1823        ]
1824    }
1825    fn build(variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
1826        let header = GroupHeader::new(lit!(knobs.text("label").get(),));
1827        match variant {
1828            "accent" => Box::new(
1829                header
1830                    .style(teksilo_tokens::TextStyleRole::BodyBold)
1831                    .color(teksilo_tokens::TextRole::Accent),
1832            ),
1833            _ => Box::new(header),
1834        }
1835    }
1836}
1837register_widget_catalog_at!("crates/teksilo-widgets/src/group_header.rs", GroupHeader);
1838
1839// ---------------------------------------------------------------------------
1840// IconButton
1841// ---------------------------------------------------------------------------
1842
1843impl WidgetCatalog for IconButton {
1844    fn id() -> &'static str {
1845        "icon_button"
1846    }
1847    fn group() -> &'static str {
1848        "Controls"
1849    }
1850    fn display_name() -> &'static str {
1851        "IconButton"
1852    }
1853    fn variants() -> Vec<PreviewVariant> {
1854        // Stand-alone scenarios (default visual mode).
1855        fn build_search_toolbar() -> Box<dyn Widget> {
1856            Box::new(IconButton::search().toolbar())
1857        }
1858        fn build_add_hero() -> Box<dyn Widget> {
1859            Box::new(IconButton::add().hero())
1860        }
1861        // Embedded scenarios — the JetBrains "built-in" dim look.
1862        fn build_browse_embedded() -> Box<dyn Widget> {
1863            Box::new(IconButton::browse().embedded())
1864        }
1865        fn build_clear_embedded_compact() -> Box<dyn Widget> {
1866            Box::new(IconButton::clear().embedded().size(IconButtonSize::Compact))
1867        }
1868        vec![
1869            PreviewVariant::scenario("search-toolbar", build_search_toolbar),
1870            PreviewVariant::scenario("add-hero", build_add_hero),
1871            PreviewVariant::scenario("browse-embedded", build_browse_embedded),
1872            PreviewVariant::scenario("clear-embedded-compact", build_clear_embedded_compact),
1873        ]
1874    }
1875    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
1876        scenario_for::<Self>(variant)
1877    }
1878}
1879register_widget_catalog_at!("crates/teksilo-widgets/src/icon_button.rs", IconButton);
1880
1881// ---------------------------------------------------------------------------
1882// Snackbar
1883// ---------------------------------------------------------------------------
1884
1885/// `Snackbar` in this codebase is an *overlay-trigger* pattern: a
1886/// labelled button that opens a popup with the supplied content when
1887/// clicked. The `label` argument names the trigger button; `.content()`
1888/// (required — `expect(...)` panics if missing) holds the popup body.
1889/// We populate both: the trigger reads from a `trigger_label` knob and
1890/// the popup body reads from a `message` knob, wrapped in a `Panel`
1891/// for readability when the popup opens.
1892impl WidgetCatalog for Snackbar {
1893    fn id() -> &'static str {
1894        "snackbar"
1895    }
1896    fn group() -> &'static str {
1897        "Feedback"
1898    }
1899    fn display_name() -> &'static str {
1900        "Snackbar"
1901    }
1902    fn knobs() -> KnobSpec {
1903        KnobSpec::new()
1904            .text("trigger_label", "Trigger label", "Show notification")
1905            .text("message", "Message", "File saved successfully.")
1906    }
1907    fn variants() -> Vec<PreviewVariant> {
1908        vec![
1909            PreviewVariant::defaults("default"),
1910            PreviewVariant::knobs(
1911                "long",
1912                KnobOverrides::new().text(
1913                    "message",
1914                    "The operation completed but with warnings — review the log for details.",
1915                ),
1916            ),
1917        ]
1918    }
1919    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
1920        let trigger_label = knobs.text("trigger_label").get();
1921        let message = knobs.text("message").get();
1922        let popup_content = Panel::new()
1923            .background(SurfaceRole::Raised)
1924            .border_color(BorderRole::Default)
1925            .border_width(1.0)
1926            .corner_radius(6.0)
1927            .padding(12.0)
1928            .child(
1929                TextWidget::new(lit!(message))
1930                    .style(TextStyleRole::Body)
1931                    .color(TextRole::Primary),
1932            );
1933        Box::new(Snackbar::new(lit!(trigger_label)).content(popup_content))
1934    }
1935}
1936register_widget_catalog_at!("crates/teksilo-widgets/src/snackbar.rs", Snackbar);
1937
1938// ---------------------------------------------------------------------------
1939// Breadcrumb
1940// ---------------------------------------------------------------------------
1941
1942impl WidgetCatalog for Breadcrumb {
1943    fn id() -> &'static str {
1944        "breadcrumb"
1945    }
1946    fn group() -> &'static str {
1947        "Containers"
1948    }
1949    fn display_name() -> &'static str {
1950        "Breadcrumb"
1951    }
1952    fn variants() -> Vec<PreviewVariant> {
1953        fn build_path() -> Box<dyn Widget> {
1954            Box::new(
1955                Breadcrumb::new()
1956                    .item(BreadcrumbItem::new(lit!("Home",)))
1957                    .item(BreadcrumbItem::new(lit!("Projects",)))
1958                    .item(BreadcrumbItem::new(lit!("Teksilo",)))
1959                    .item(BreadcrumbItem::new(lit!("crates",))),
1960            )
1961        }
1962        vec![PreviewVariant::scenario("path", build_path)]
1963    }
1964    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
1965        scenario_for::<Self>(variant)
1966    }
1967}
1968register_widget_catalog_at!("crates/teksilo-widgets/src/breadcrumb.rs", Breadcrumb);
1969
1970// ---------------------------------------------------------------------------
1971// Toolbar (the chrome widget — distinct from the previewer's toolbar pane)
1972// ---------------------------------------------------------------------------
1973
1974impl WidgetCatalog for Toolbar {
1975    fn id() -> &'static str {
1976        "toolbar"
1977    }
1978    fn group() -> &'static str {
1979        "Chrome"
1980    }
1981    fn display_name() -> &'static str {
1982        "Toolbar"
1983    }
1984    fn variants() -> Vec<PreviewVariant> {
1985        fn build_default() -> Box<dyn Widget> {
1986            Box::new(
1987                Toolbar::new()
1988                    .child(Button::new(lit!("New")).variant(ButtonVariant::Ghost))
1989                    .child(Button::new(lit!("Open…")).variant(ButtonVariant::Ghost))
1990                    .child(Button::new(lit!("Save")).variant(ButtonVariant::Ghost)),
1991            )
1992        }
1993        vec![PreviewVariant::scenario("default", build_default)]
1994    }
1995    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
1996        scenario_for::<Self>(variant)
1997    }
1998}
1999register_widget_catalog_at!("crates/teksilo-widgets/src/toolbar.rs", Toolbar);
2000
2001// ---------------------------------------------------------------------------
2002// StatusBar
2003// ---------------------------------------------------------------------------
2004
2005impl WidgetCatalog for StatusBar {
2006    fn id() -> &'static str {
2007        "status_bar"
2008    }
2009    fn group() -> &'static str {
2010        "Chrome"
2011    }
2012    fn display_name() -> &'static str {
2013        "StatusBar"
2014    }
2015    fn variants() -> Vec<PreviewVariant> {
2016        fn build_default() -> Box<dyn Widget> {
2017            Box::new(
2018                StatusBar::new()
2019                    .child(
2020                        TextWidget::new(lit!("Ready"))
2021                            .style(TextStyleRole::Tiny)
2022                            .color(TextRole::Secondary),
2023                    )
2024                    .child(Spacer::new())
2025                    .child(
2026                        TextWidget::new(lit!("Ln 42, Col 17"))
2027                            .style(TextStyleRole::Tiny)
2028                            .color(TextRole::Secondary),
2029                    ),
2030            )
2031        }
2032        vec![PreviewVariant::scenario("default", build_default)]
2033    }
2034    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2035        scenario_for::<Self>(variant)
2036    }
2037}
2038register_widget_catalog_at!("crates/teksilo-widgets/src/status_bar.rs", StatusBar);
2039
2040// ---------------------------------------------------------------------------
2041// Accordion
2042// ---------------------------------------------------------------------------
2043
2044impl WidgetCatalog for Accordion {
2045    fn id() -> &'static str {
2046        "accordion"
2047    }
2048    fn group() -> &'static str {
2049        "Containers"
2050    }
2051    fn display_name() -> &'static str {
2052        "Accordion"
2053    }
2054    fn knobs() -> KnobSpec {
2055        KnobSpec::new()
2056            .text("title", "Title", "Advanced")
2057            .bool_("expanded", "Expanded", false)
2058            .text("content", "Content body", "Hidden until expanded.")
2059    }
2060    fn variants() -> Vec<PreviewVariant> {
2061        vec![
2062            PreviewVariant::defaults("collapsed"),
2063            PreviewVariant::knobs(
2064                "expanded",
2065                KnobOverrides::new()
2066                    .bool_("expanded", true)
2067                    .text("content", "Now visible because the section is expanded."),
2068            ),
2069        ]
2070    }
2071    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
2072        let title = knobs.text("title").get();
2073        let body = knobs.text("content").get();
2074        // The accordion's `expanded` Signal IS the bool knob — clicking
2075        // the header in the canvas mutates the signal, the inspector
2076        // toggle reflects it.
2077        let expanded = knobs.bool_("expanded");
2078        Box::new(Accordion::new(lit!(title), expanded).content(sample_text(&body)))
2079    }
2080}
2081register_widget_catalog_at!("crates/teksilo-widgets/src/accordion.rs", Accordion);
2082
2083// ---------------------------------------------------------------------------
2084// RadioGroup
2085// ---------------------------------------------------------------------------
2086
2087impl WidgetCatalog for RadioGroup {
2088    fn id() -> &'static str {
2089        "radio_group"
2090    }
2091    fn group() -> &'static str {
2092        "Controls"
2093    }
2094    fn display_name() -> &'static str {
2095        "RadioGroup"
2096    }
2097    fn variants() -> Vec<PreviewVariant> {
2098        fn build_default() -> Box<dyn Widget> {
2099            let selected = Signal::new(0_usize);
2100            Box::new(
2101                RadioGroup::new()
2102                    .child(RadioButton::new(0, selected.clone()).label(lit!("First")))
2103                    .child(RadioButton::new(1, selected.clone()).label(lit!("Second")))
2104                    .child(RadioButton::new(2, selected).label(lit!("Third"))),
2105            )
2106        }
2107        vec![PreviewVariant::scenario("default", build_default)]
2108    }
2109    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2110        scenario_for::<Self>(variant)
2111    }
2112}
2113register_widget_catalog_at!("crates/teksilo-widgets/src/radio_group.rs", RadioGroup);
2114
2115// ---------------------------------------------------------------------------
2116// RadioTile
2117// ---------------------------------------------------------------------------
2118impl WidgetCatalog for RadioTile {
2119    fn id() -> &'static str {
2120        "radio_tile"
2121    }
2122    fn group() -> &'static str {
2123        "Controls"
2124    }
2125    fn display_name() -> &'static str {
2126        "RadioTile"
2127    }
2128    fn knobs() -> KnobSpec {
2129        KnobSpec::new()
2130            .choice("selected", "Selected", &["Yes", "No"], 0)
2131            .opt_text("title", "Title", Some("Single file"))
2132            .opt_text(
2133                "description",
2134                "Description",
2135                Some("One .skrib archive (zip). Portable, easy to back up."),
2136            )
2137            .bool_("enabled", "Enabled", true)
2138    }
2139    fn variants() -> Vec<PreviewVariant> {
2140        vec![
2141            PreviewVariant::defaults("selected"),
2142            PreviewVariant::knobs("unselected", KnobOverrides::new().choice("selected", 1)),
2143            PreviewVariant::knobs("disabled", KnobOverrides::new().bool_("enabled", false)),
2144            PreviewVariant::knobs(
2145                "no-description",
2146                KnobOverrides::new().opt_text("description", None),
2147            ),
2148        ]
2149    }
2150    fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
2151        let title = knobs.opt_text("title").get();
2152        let description = knobs.opt_text("description").get();
2153        let enabled = knobs.bool_("enabled").get();
2154        let mut tile = RadioTile::new()
2155            .selection(0, knobs.choice("selected"))
2156            .enabled(enabled);
2157        if let Some(title) = title {
2158            tile = tile.title(lit!(title));
2159        }
2160        if let Some(description) = description {
2161            tile = tile.description(lit!(description));
2162        }
2163        Box::new(FixedSize::new().width(300.0).child(tile))
2164    }
2165}
2166register_widget_catalog_at!("crates/teksilo-widgets/src/radio_tile.rs", RadioTile);
2167
2168// ---------------------------------------------------------------------------
2169// RadioTileGroup
2170// ---------------------------------------------------------------------------
2171impl WidgetCatalog for RadioTileGroup {
2172    fn id() -> &'static str {
2173        "radio_tile_group"
2174    }
2175    fn group() -> &'static str {
2176        "Controls"
2177    }
2178    fn display_name() -> &'static str {
2179        "RadioTileGroup"
2180    }
2181    fn variants() -> Vec<PreviewVariant> {
2182        fn build_row() -> Box<dyn Widget> {
2183            let selected = Signal::new(0_usize);
2184            Box::new(
2185                FixedSize::new().width(560.0).child(
2186                    RadioTileGroup::new(selected)
2187                        .tile(
2188                            RadioTile::new()
2189                                .title(lit!("Single file"))
2190                                .description(lit!(
2191                                    "One .skrib archive (zip). Portable, easy to back up."
2192                                )),
2193                        )
2194                        .tile(RadioTile::new().title(lit!("Bundle")).description(lit!(
2195                            "A folder holding every text & asset. Friendlier to version control."
2196                        )))
2197                        .layout(TileLayout::Row),
2198                ),
2199            )
2200        }
2201        fn build_grid() -> Box<dyn Widget> {
2202            let selected = Signal::new(1_usize);
2203            Box::new(
2204                FixedSize::new().width(560.0).child(
2205                    RadioTileGroup::new(selected)
2206                        .tiles((0..4).map(|i| {
2207                            RadioTile::new()
2208                                .title(lit!(format!("Option {}", i + 1)))
2209                                .description(lit!("A selectable option in the grid."))
2210                        }))
2211                        .layout(TileLayout::Grid {
2212                            min_tile_width: 240.0,
2213                        }),
2214                ),
2215            )
2216        }
2217        vec![
2218            PreviewVariant::scenario("row", build_row),
2219            PreviewVariant::scenario("grid", build_grid),
2220        ]
2221    }
2222    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2223        scenario_for::<Self>(variant)
2224    }
2225}
2226register_widget_catalog_at!(
2227    "crates/teksilo-widgets/src/radio_tile_group.rs",
2228    RadioTileGroup
2229);
2230
2231// ---------------------------------------------------------------------------
2232// SplitButton
2233// ---------------------------------------------------------------------------
2234
2235impl WidgetCatalog for SplitButton {
2236    fn id() -> &'static str {
2237        "split_button"
2238    }
2239    fn group() -> &'static str {
2240        "Controls"
2241    }
2242    fn display_name() -> &'static str {
2243        "SplitButton"
2244    }
2245    fn variants() -> Vec<PreviewVariant> {
2246        fn build_default() -> Box<dyn Widget> {
2247            Box::new(
2248                SplitButton::new_static()
2249                    .item(MenuItem::new(lit!("Save")))
2250                    .item(MenuItem::new(lit!("Save As…")))
2251                    .item(MenuItem::new(lit!("Save All"))),
2252            )
2253        }
2254        vec![PreviewVariant::scenario("default", build_default)]
2255    }
2256    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2257        scenario_for::<Self>(variant)
2258    }
2259}
2260register_widget_catalog_at!("crates/teksilo-widgets/src/split_button.rs", SplitButton);
2261
2262// =========================================================================
2263// Tier C — data-driven / structural
2264// =========================================================================
2265
2266// ---------------------------------------------------------------------------
2267// ListView
2268// ---------------------------------------------------------------------------
2269
2270impl WidgetCatalog for ListView<String> {
2271    fn id() -> &'static str {
2272        "list_view"
2273    }
2274    fn group() -> &'static str {
2275        "Data"
2276    }
2277    fn display_name() -> &'static str {
2278        "ListView"
2279    }
2280    fn variants() -> Vec<PreviewVariant> {
2281        fn build_short() -> Box<dyn Widget> {
2282            let model = teksilo_data::ListModel::from_vec(vec![
2283                "Apple".to_string(),
2284                "Banana".to_string(),
2285                "Cherry".to_string(),
2286                "Date".to_string(),
2287                "Elderberry".to_string(),
2288                "Fig".to_string(),
2289            ]);
2290            Box::new(
2291                FixedSize::new()
2292                    .width(280.0_f32)
2293                    .height(220.0_f32)
2294                    .child(ListView::new(model, |_idx, item, selected| {
2295                        Box::new(StandardListItem::new(lit!(item.clone())).selected(selected))
2296                    })),
2297            )
2298        }
2299        vec![PreviewVariant::scenario("short", build_short)]
2300    }
2301    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2302        scenario_for::<Self>(variant)
2303    }
2304}
2305register_widget_catalog_at!("crates/teksilo-widgets/src/list_view.rs", ListView<String>);
2306
2307// ---------------------------------------------------------------------------
2308// GridView
2309// ---------------------------------------------------------------------------
2310
2311impl WidgetCatalog for GridView<String> {
2312    fn id() -> &'static str {
2313        "grid_view"
2314    }
2315    fn group() -> &'static str {
2316        "Data"
2317    }
2318    fn display_name() -> &'static str {
2319        "GridView"
2320    }
2321    fn variants() -> Vec<PreviewVariant> {
2322        fn items(n: usize) -> teksilo_data::ListModel<String> {
2323            teksilo_data::ListModel::from_vec((0..n).map(|i| format!("Tile {i}")).collect())
2324        }
2325        // RectWidget is a leaf, so layer the label over it in a ZStack.
2326        fn tile_z(caption: &str, selected: bool) -> Box<dyn Widget> {
2327            let bg = if selected {
2328                SurfaceRole::AccentSubtle
2329            } else {
2330                SurfaceRole::Raised
2331            };
2332            Box::new(
2333                crate::primitives::ZStack::new()
2334                    .child(RectWidget::new().background(bg))
2335                    .child(Center::new().child(
2336                        TextWidget::new(lit!(caption.to_string())).color(TextRole::Primary),
2337                    )),
2338            )
2339        }
2340        fn framed(grid: GridView<String>) -> Box<dyn Widget> {
2341            Box::new(
2342                FixedSize::new()
2343                    .width(360.0_f32)
2344                    .height(320.0_f32)
2345                    .child(grid),
2346            )
2347        }
2348
2349        fn adaptive() -> Box<dyn Widget> {
2350            framed(
2351                GridView::new(items(40), |tc| tile_z(tc.item, tc.is_selected))
2352                    .sizing(GridSizing::Adaptive {
2353                        min_width: 90.0,
2354                        max_width: None,
2355                        height: 64.0,
2356                    })
2357                    .spacing(8.0),
2358            )
2359        }
2360        fn fixed_columns() -> Box<dyn Widget> {
2361            framed(
2362                GridView::new(items(40), |tc| tile_z(tc.item, tc.is_selected))
2363                    .column_count(4, 64.0)
2364                    .spacing(8.0),
2365            )
2366        }
2367        fn selectable() -> Box<dyn Widget> {
2368            use teksilo_data::{SelectionMode, SelectionModel};
2369            let sel = SelectionModel::new(SelectionMode::Multi);
2370            sel.select(2);
2371            framed(
2372                GridView::new(items(40), |tc| tile_z(tc.item, tc.is_selected))
2373                    .sizing(GridSizing::Adaptive {
2374                        min_width: 90.0,
2375                        max_width: None,
2376                        height: 64.0,
2377                    })
2378                    .spacing(8.0)
2379                    .selection(sel),
2380            )
2381        }
2382        fn waterfall() -> Box<dyn Widget> {
2383            // `.item_height` drives the exact per-item height, so the tile
2384            // widget itself can stay plain.
2385            framed(
2386                GridView::new(items(40), |tc| tile_z(tc.item, tc.is_selected))
2387                    .column_count(3, 64.0)
2388                    .waterfall(64.0)
2389                    .item_height(|i| 48.0 + (i % 5) as f32 * 18.0)
2390                    .spacing(8.0),
2391            )
2392        }
2393
2394        vec![
2395            PreviewVariant::scenario("adaptive", adaptive),
2396            PreviewVariant::scenario("fixed_columns", fixed_columns),
2397            PreviewVariant::scenario("selection", selectable),
2398            PreviewVariant::scenario("waterfall", waterfall),
2399        ]
2400    }
2401    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2402        scenario_for::<Self>(variant)
2403    }
2404}
2405register_widget_catalog_at!("crates/teksilo-widgets/src/grid_view.rs", GridView<String>);
2406
2407// ---------------------------------------------------------------------------
2408// TreeView
2409// ---------------------------------------------------------------------------
2410
2411impl WidgetCatalog for TreeView<String> {
2412    fn id() -> &'static str {
2413        "tree_view"
2414    }
2415    fn group() -> &'static str {
2416        "Data"
2417    }
2418    fn display_name() -> &'static str {
2419        "TreeView"
2420    }
2421    fn variants() -> Vec<PreviewVariant> {
2422        fn build_default() -> Box<dyn Widget> {
2423            let model = teksilo_data::TreeModel::<String>::new();
2424            let root = model.insert_root(0, "Project".to_string());
2425            let crates_node = model.insert_child(root, 0, "crates".to_string());
2426            model.insert_child(crates_node, 0, "teksilo-core".to_string());
2427            model.insert_child(crates_node, 1, "teksilo-widgets".to_string());
2428            model.insert_child(crates_node, 2, "teksilo-render".to_string());
2429            let docs = model.insert_child(root, 1, "docs".to_string());
2430            model.insert_child(docs, 0, "architecture.md".to_string());
2431            Box::new(FixedSize::new().width(280.0_f32).height(220.0_f32).child(
2432                TreeView::new_with_context(model, |item, entry, selected, ctx| {
2433                    Box::new(
2434                        StandardTreeItem::new(lit!(item.clone()))
2435                            .from_entry(entry)
2436                            .selected(selected)
2437                            .on_chevron_toggle_rc(ctx.toggle_callback()),
2438                    )
2439                }),
2440            ))
2441        }
2442        vec![PreviewVariant::scenario("default", build_default)]
2443    }
2444    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2445        scenario_for::<Self>(variant)
2446    }
2447}
2448register_widget_catalog_at!("crates/teksilo-widgets/src/tree_view.rs", TreeView<String>);
2449
2450// ---------------------------------------------------------------------------
2451// StandardListItem
2452// ---------------------------------------------------------------------------
2453
2454impl WidgetCatalog for StandardListItem {
2455    fn id() -> &'static str {
2456        "standard_list_item"
2457    }
2458    fn group() -> &'static str {
2459        "Data"
2460    }
2461    fn display_name() -> &'static str {
2462        "StandardListItem"
2463    }
2464    fn variants() -> Vec<PreviewVariant> {
2465        fn build_single_line() -> Box<dyn Widget> {
2466            Box::new(StandardListItem::new(lit!("Single-line item")))
2467        }
2468        fn build_with_all_primary_slots() -> Box<dyn Widget> {
2469            Box::new(
2470                StandardListItem::new(lit!("With every primary slot"))
2471                    .leading_slot(TextWidget::new(lit!("●")).color(TextRole::Accent))
2472                    .center_slot(TextWidget::new(lit!("•")).color(TextRole::Secondary))
2473                    .trailing_slot(TextWidget::new(lit!("12")).color(TextRole::Secondary)),
2474            )
2475        }
2476        fn build_two_line_with_subtitle_slots() -> Box<dyn Widget> {
2477            Box::new(
2478                StandardListItem::new(lit!("Title line"))
2479                    .subtitle(lit!("Subtitle line"))
2480                    .leading_slot(TextWidget::new(lit!("●")).color(TextRole::Accent))
2481                    .subtitle_leading_slot(TextWidget::new(lit!("•")).color(TextRole::Secondary))
2482                    .subtitle_trailing_slot(
2483                        TextWidget::new(lit!("just now")).color(TextRole::Secondary),
2484                    )
2485                    .trailing_slot(TextWidget::new(lit!("∗")).color(TextRole::Accent)),
2486            )
2487        }
2488        fn build_with_checkbox() -> Box<dyn Widget> {
2489            let checked = Signal::new(true);
2490            Box::new(StandardListItem::new(lit!("With two-state checkbox")).checkbox(checked))
2491        }
2492        fn build_with_tristate_checkbox() -> Box<dyn Widget> {
2493            use teksilo_data::CheckState;
2494            let s = Signal::new(CheckState::Indeterminate);
2495            Box::new(StandardListItem::new(lit!("With tristate checkbox")).tristate_checkbox(s))
2496        }
2497        fn build_selected() -> Box<dyn Widget> {
2498            Box::new(StandardListItem::new(lit!("Selected")).selected(true))
2499        }
2500        fn build_disabled() -> Box<dyn Widget> {
2501            Box::new(StandardListItem::new(lit!("Disabled")).enabled(false))
2502        }
2503        vec![
2504            PreviewVariant::scenario("single_line", build_single_line),
2505            PreviewVariant::scenario("all_primary_slots", build_with_all_primary_slots),
2506            PreviewVariant::scenario("two_line", build_two_line_with_subtitle_slots),
2507            PreviewVariant::scenario("checkbox", build_with_checkbox),
2508            PreviewVariant::scenario("tristate_checkbox", build_with_tristate_checkbox),
2509            PreviewVariant::scenario("selected", build_selected),
2510            PreviewVariant::scenario("disabled", build_disabled),
2511        ]
2512    }
2513    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2514        scenario_for::<Self>(variant)
2515    }
2516}
2517register_widget_catalog_at!(
2518    "crates/teksilo-widgets/src/standard_item.rs",
2519    StandardListItem
2520);
2521
2522// ---------------------------------------------------------------------------
2523// StandardTreeItem
2524// ---------------------------------------------------------------------------
2525
2526impl WidgetCatalog for StandardTreeItem {
2527    fn id() -> &'static str {
2528        "standard_tree_item"
2529    }
2530    fn group() -> &'static str {
2531        "Data"
2532    }
2533    fn display_name() -> &'static str {
2534        "StandardTreeItem"
2535    }
2536    fn variants() -> Vec<PreviewVariant> {
2537        fn build_collapsed_branch() -> Box<dyn Widget> {
2538            Box::new(
2539                StandardTreeItem::new(lit!("Folder (collapsed)"))
2540                    .depth(0)
2541                    .has_children(true)
2542                    .is_expanded(false),
2543            )
2544        }
2545        fn build_expanded_branch() -> Box<dyn Widget> {
2546            Box::new(
2547                StandardTreeItem::new(lit!("Folder (expanded)"))
2548                    .depth(0)
2549                    .has_children(true)
2550                    .is_expanded(true),
2551            )
2552        }
2553        fn build_leaf_indented() -> Box<dyn Widget> {
2554            Box::new(
2555                StandardTreeItem::new(lit!("Deep leaf"))
2556                    .depth(2)
2557                    .has_children(false),
2558            )
2559        }
2560        fn build_with_tristate_checkbox() -> Box<dyn Widget> {
2561            use teksilo_data::CheckState;
2562            let s = Signal::new(CheckState::Indeterminate);
2563            Box::new(
2564                StandardTreeItem::new(lit!("Folder with tristate"))
2565                    .depth(1)
2566                    .has_children(true)
2567                    .is_expanded(true)
2568                    .tristate_checkbox(s),
2569            )
2570        }
2571        fn build_two_line() -> Box<dyn Widget> {
2572            Box::new(
2573                StandardTreeItem::new(lit!("Folder"))
2574                    .subtitle(lit!("3 items · last week"))
2575                    .depth(0)
2576                    .has_children(true)
2577                    .is_expanded(false)
2578                    .subtitle_trailing_slot(TextWidget::new(lit!("3")).color(TextRole::Secondary)),
2579            )
2580        }
2581        vec![
2582            PreviewVariant::scenario("collapsed_branch", build_collapsed_branch),
2583            PreviewVariant::scenario("expanded_branch", build_expanded_branch),
2584            PreviewVariant::scenario("leaf_indented", build_leaf_indented),
2585            PreviewVariant::scenario("tristate_checkbox", build_with_tristate_checkbox),
2586            PreviewVariant::scenario("two_line", build_two_line),
2587        ]
2588    }
2589    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2590        scenario_for::<Self>(variant)
2591    }
2592}
2593register_widget_catalog_at!(
2594    "crates/teksilo-widgets/src/standard_item.rs",
2595    StandardTreeItem
2596);
2597
2598// ---------------------------------------------------------------------------
2599// MenuList
2600// ---------------------------------------------------------------------------
2601
2602impl WidgetCatalog for MenuList {
2603    fn id() -> &'static str {
2604        "menu_list"
2605    }
2606    fn group() -> &'static str {
2607        "Menus"
2608    }
2609    fn display_name() -> &'static str {
2610        "MenuList"
2611    }
2612    fn variants() -> Vec<PreviewVariant> {
2613        fn build_default() -> Box<dyn Widget> {
2614            Box::new(
2615                MenuList::new()
2616                    .item(MenuItem::new(lit!("New")))
2617                    .item(MenuItem::new(lit!("Open…")))
2618                    .item(MenuItem::new(lit!("Save")))
2619                    .item(MenuItem::new(lit!("Save As…")))
2620                    .item(MenuItem::new(lit!("Close"))),
2621            )
2622        }
2623        vec![PreviewVariant::scenario("default", build_default)]
2624    }
2625    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2626        scenario_for::<Self>(variant)
2627    }
2628}
2629register_widget_catalog_at!("crates/teksilo-widgets/src/menu_list.rs", MenuList);
2630
2631// ---------------------------------------------------------------------------
2632// ScrollArea
2633// ---------------------------------------------------------------------------
2634
2635impl WidgetCatalog for ScrollArea {
2636    fn id() -> &'static str {
2637        "scroll_area"
2638    }
2639    fn group() -> &'static str {
2640        "Data"
2641    }
2642    fn display_name() -> &'static str {
2643        "ScrollArea"
2644    }
2645    fn variants() -> Vec<PreviewVariant> {
2646        fn build_long_content() -> Box<dyn Widget> {
2647            let mut col = VStack::new().spacing(4.0);
2648            for i in 1..=40 {
2649                col = col.child(
2650                    Padding::symmetric(4.0, 8.0).child(
2651                        TextWidget::new(lit!(format!("Row {}", i)))
2652                            .style(TextStyleRole::Body)
2653                            .color(TextRole::Primary),
2654                    ),
2655                );
2656            }
2657            Box::new(
2658                FixedSize::new()
2659                    .width(280.0_f32)
2660                    .height(180.0_f32)
2661                    .child(ScrollArea::new().child(col)),
2662            )
2663        }
2664        vec![PreviewVariant::scenario("long-content", build_long_content)]
2665    }
2666    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2667        scenario_for::<Self>(variant)
2668    }
2669    fn icon() -> Option<Box<dyn Widget>> {
2670        Some(icons::scroll_area())
2671    }
2672    fn category() -> WidgetCategory {
2673        WidgetCategory::ContainerA
2674    }
2675    fn build_with_children(
2676        _variant: &str,
2677        _knobs: &KnobValues,
2678        children: Vec<SlottedChild>,
2679    ) -> Box<dyn Widget> {
2680        match children.into_iter().next() {
2681            Some(c) => Box::new(ScrollArea::from_id(c.id)),
2682            None => Box::new(ScrollArea::new()),
2683        }
2684    }
2685}
2686register_widget_catalog_at!("crates/teksilo-widgets/src/scroll_area.rs", ScrollArea);
2687
2688// ---------------------------------------------------------------------------
2689// Splitter
2690// ---------------------------------------------------------------------------
2691
2692impl WidgetCatalog for Splitter {
2693    fn id() -> &'static str {
2694        "splitter"
2695    }
2696    fn group() -> &'static str {
2697        "Containers"
2698    }
2699    fn display_name() -> &'static str {
2700        "Splitter"
2701    }
2702    fn variants() -> Vec<PreviewVariant> {
2703        fn build_horizontal() -> Box<dyn Widget> {
2704            let left = Panel::new()
2705                .background(SurfaceRole::Sunken)
2706                .padding(12.0)
2707                .child(sample_text("Left pane"));
2708            let right = Panel::new()
2709                .background(SurfaceRole::Raised)
2710                .padding(12.0)
2711                .child(sample_text("Right pane"));
2712            Box::new(
2713                FixedSize::new().width(420.0_f32).height(220.0_f32).child(
2714                    Splitter::new(SplitterModel::new(2, Orientation::Horizontal))
2715                        .pane(left)
2716                        .pane(right),
2717                ),
2718            )
2719        }
2720        fn build_three_pane() -> Box<dyn Widget> {
2721            let model = SplitterModel::from_panes(
2722                vec![
2723                    PaneDescriptor::new()
2724                        .size(120.0)
2725                        .collapsible(true)
2726                        .stretch(0.0),
2727                    PaneDescriptor::new().stretch(1.0),
2728                    PaneDescriptor::new()
2729                        .size(120.0)
2730                        .collapsible(true)
2731                        .stretch(0.0),
2732                ],
2733                Orientation::Horizontal,
2734            );
2735            let pane = |label: &str, role| {
2736                Panel::new()
2737                    .background(role)
2738                    .padding(12.0)
2739                    .child(sample_text(label))
2740            };
2741            Box::new(
2742                FixedSize::new().width(480.0_f32).height(220.0_f32).child(
2743                    Splitter::new(model)
2744                        .pane(pane("Sidebar", SurfaceRole::Sunken))
2745                        .pane(pane("Editor", SurfaceRole::Raised))
2746                        .pane(pane("Inspector", SurfaceRole::Sunken)),
2747                ),
2748            )
2749        }
2750        vec![
2751            PreviewVariant::scenario("horizontal", build_horizontal),
2752            PreviewVariant::scenario("three_pane", build_three_pane),
2753        ]
2754    }
2755    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2756        scenario_for::<Self>(variant)
2757    }
2758}
2759register_widget_catalog_at!("crates/teksilo-widgets/src/splitter.rs", Splitter);
2760
2761// ---------------------------------------------------------------------------
2762// TabWidget
2763// ---------------------------------------------------------------------------
2764
2765impl WidgetCatalog for TabWidget {
2766    fn id() -> &'static str {
2767        "tab_widget"
2768    }
2769    fn group() -> &'static str {
2770        "Containers"
2771    }
2772    fn display_name() -> &'static str {
2773        "TabWidget"
2774    }
2775    fn variants() -> Vec<PreviewVariant> {
2776        fn build_three_tabs() -> Box<dyn Widget> {
2777            use crate::tab_widget::{TabId, TabInfo};
2778            let selected: Signal<Option<TabId>> = Signal::new(None);
2779            Box::new(
2780                FixedSize::new().width(420.0_f32).height(220.0_f32).child(
2781                    TabWidget::new(selected)
2782                        .static_tab(
2783                            TabInfo::new().title(lit!("Overview")),
2784                            Center::new().child(sample_text("Overview tab content")),
2785                        )
2786                        .static_tab(
2787                            TabInfo::new().title(lit!("Details")),
2788                            Center::new().child(sample_text("Details tab content")),
2789                        )
2790                        .static_tab(
2791                            TabInfo::new().title(lit!("Settings")),
2792                            Center::new().child(sample_text("Settings tab content")),
2793                        ),
2794                ),
2795            )
2796        }
2797        vec![PreviewVariant::scenario("three-tabs", build_three_tabs)]
2798    }
2799    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2800        scenario_for::<Self>(variant)
2801    }
2802}
2803register_widget_catalog_at!("crates/teksilo-widgets/src/tab_widget.rs", TabWidget);
2804
2805// ---------------------------------------------------------------------------
2806// ToolBox
2807// ---------------------------------------------------------------------------
2808
2809impl WidgetCatalog for ToolBox {
2810    fn id() -> &'static str {
2811        "tool_box"
2812    }
2813    fn group() -> &'static str {
2814        "Containers"
2815    }
2816    fn display_name() -> &'static str {
2817        "ToolBox"
2818    }
2819    fn variants() -> Vec<PreviewVariant> {
2820        fn build_three_items() -> Box<dyn Widget> {
2821            let selected = Signal::new(0_usize);
2822            Box::new(
2823                FixedSize::new().width(280.0_f32).height(280.0_f32).child(
2824                    ToolBox::new(selected)
2825                        .item(
2826                            lit!("General"),
2827                            Padding::uniform(12.0).child(sample_text("General settings")),
2828                        )
2829                        .item(
2830                            lit!("Editor"),
2831                            Padding::uniform(12.0).child(sample_text("Editor settings")),
2832                        )
2833                        .item(
2834                            lit!("Keymap"),
2835                            Padding::uniform(12.0).child(sample_text("Keymap settings")),
2836                        ),
2837                ),
2838            )
2839        }
2840        vec![PreviewVariant::scenario("three-items", build_three_items)]
2841    }
2842    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2843        scenario_for::<Self>(variant)
2844    }
2845}
2846register_widget_catalog_at!("crates/teksilo-widgets/src/tool_box.rs", ToolBox);
2847
2848// ---------------------------------------------------------------------------
2849// Repeater
2850// ---------------------------------------------------------------------------
2851
2852impl WidgetCatalog for crate::Repeater<String> {
2853    fn id() -> &'static str {
2854        "repeater"
2855    }
2856    fn group() -> &'static str {
2857        "Data"
2858    }
2859    fn display_name() -> &'static str {
2860        "Repeater"
2861    }
2862    fn variants() -> Vec<PreviewVariant> {
2863        fn build_default() -> Box<dyn Widget> {
2864            let model = teksilo_data::ListModel::from_vec(vec![
2865                "Alpha".to_string(),
2866                "Beta".to_string(),
2867                "Gamma".to_string(),
2868                "Delta".to_string(),
2869            ]);
2870            Box::new(
2871                crate::Repeater::new(model, |item| {
2872                    Box::new(
2873                        Padding::symmetric(4.0, 8.0).child(
2874                            TextWidget::new(lit!(item.clone()))
2875                                .style(TextStyleRole::Body)
2876                                .color(TextRole::Primary),
2877                        ),
2878                    )
2879                })
2880                .spacing(4.0),
2881            )
2882        }
2883        vec![PreviewVariant::scenario("default", build_default)]
2884    }
2885    fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2886        scenario_for::<Self>(variant)
2887    }
2888}
2889register_widget_catalog_at!(
2890    "crates/teksilo-widgets/src/repeater.rs",
2891    crate::Repeater<String>
2892);
2893
2894// =========================================================================
2895// Helpers
2896// =========================================================================
2897
2898/// Resolve a variant name to its scenario builder result. Used by
2899/// every Tier B/C `build(...)` method whose variants are all
2900/// `Scenario`-shaped — saves ~6 lines per impl. Falls back to the
2901/// first declared variant when `name` is unknown.
2902fn scenario_for<W: WidgetCatalog>(name: &str) -> Box<dyn Widget> {
2903    let variants = W::variants();
2904    let chosen = variants
2905        .iter()
2906        .find(|v| v.name() == name)
2907        .or_else(|| variants.first());
2908    match chosen {
2909        Some(PreviewVariant::Scenario { builder, .. }) => builder(),
2910        _ => Box::new(
2911            TextWidget::new(lit!(format!("(no scenario for variant '{}')", name)))
2912                .style(TextStyleRole::Small)
2913                .color(TextRole::Secondary),
2914        ),
2915    }
2916}
2917
2918// =========================================================================
2919// Color picker family (HexColorInput, ColorPicker, ColorEdit)
2920// =========================================================================
2921
2922mod color_family {
2923    use super::*;
2924    use crate::{ColorEdit, ColorPicker, ColorPickerLayout, HexColorInput};
2925    use teksilo_tokens::Color;
2926
2927    impl WidgetCatalog for HexColorInput {
2928        fn id() -> &'static str {
2929            "hex-color-input"
2930        }
2931        fn group() -> &'static str {
2932            "Color"
2933        }
2934        fn display_name() -> &'static str {
2935            "HexColorInput"
2936        }
2937        fn knobs() -> KnobSpec {
2938            KnobSpec::new()
2939        }
2940        fn variants() -> Vec<PreviewVariant> {
2941            fn default_var() -> Box<dyn Widget> {
2942                Box::new(HexColorInput::new(Signal::new(Color::from_hex("#3584E4"))))
2943            }
2944            fn alpha_var() -> Box<dyn Widget> {
2945                Box::new(
2946                    HexColorInput::new(Signal::new(Color::from_rgba(1.0, 0.5, 0.0, 0.6)))
2947                        .alpha_enabled(true),
2948                )
2949            }
2950            vec![
2951                PreviewVariant::scenario("default", default_var),
2952                PreviewVariant::scenario("with-alpha", alpha_var),
2953            ]
2954        }
2955        fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
2956            scenario_for::<Self>(variant)
2957        }
2958    }
2959    register_widget_catalog_at!(
2960        "crates/teksilo-widgets/src/hex_color_input.rs",
2961        HexColorInput
2962    );
2963
2964    impl WidgetCatalog for ColorPicker {
2965        fn id() -> &'static str {
2966            "color-picker"
2967        }
2968        fn group() -> &'static str {
2969            "Color"
2970        }
2971        fn display_name() -> &'static str {
2972            "ColorPicker"
2973        }
2974        fn knobs() -> KnobSpec {
2975            KnobSpec::new()
2976        }
2977        fn variants() -> Vec<PreviewVariant> {
2978            fn default_var() -> Box<dyn Widget> {
2979                Box::new(ColorPicker::new(Signal::new(Color::from_hex("#3584E4"))))
2980            }
2981            fn with_alpha() -> Box<dyn Widget> {
2982                Box::new(
2983                    ColorPicker::new(Signal::new(Color::from_rgba(0.21, 0.66, 0.40, 0.5)))
2984                        .alpha_enabled(true),
2985                )
2986            }
2987            fn compact() -> Box<dyn Widget> {
2988                Box::new(
2989                    ColorPicker::new(Signal::new(Color::from_hex("#E91E63")))
2990                        .layout(ColorPickerLayout::Compact),
2991                )
2992            }
2993            fn wide() -> Box<dyn Widget> {
2994                Box::new(
2995                    ColorPicker::new(Signal::new(Color::from_hex("#FF9800")))
2996                        .alpha_enabled(true)
2997                        .layout(ColorPickerLayout::Wide)
2998                        .show_hsv_spinners(true),
2999                )
3000            }
3001            vec![
3002                PreviewVariant::scenario("default", default_var),
3003                PreviewVariant::scenario("with-alpha", with_alpha),
3004                PreviewVariant::scenario("compact", compact),
3005                PreviewVariant::scenario("wide", wide),
3006            ]
3007        }
3008        fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
3009            scenario_for::<Self>(variant)
3010        }
3011    }
3012    register_widget_catalog_at!("crates/teksilo-widgets/src/color_picker.rs", ColorPicker);
3013
3014    impl WidgetCatalog for ColorEdit {
3015        fn id() -> &'static str {
3016            "color-edit"
3017        }
3018        fn group() -> &'static str {
3019            "Color"
3020        }
3021        fn display_name() -> &'static str {
3022            "ColorEdit"
3023        }
3024        fn knobs() -> KnobSpec {
3025            KnobSpec::new()
3026        }
3027        fn variants() -> Vec<PreviewVariant> {
3028            fn default_var() -> Box<dyn Widget> {
3029                Box::new(ColorEdit::new(Signal::new(Color::from_hex("#3584E4"))))
3030            }
3031            fn with_alpha() -> Box<dyn Widget> {
3032                Box::new(
3033                    ColorEdit::new(Signal::new(Color::from_rgba(0.92, 0.27, 0.18, 0.6)))
3034                        .alpha_enabled(true),
3035                )
3036            }
3037            fn no_hex_in_trigger() -> Box<dyn Widget> {
3038                Box::new(
3039                    ColorEdit::new(Signal::new(Color::from_hex("#9C27B0")))
3040                        .show_hex_in_trigger(false),
3041                )
3042            }
3043            fn nullable_var() -> Box<dyn Widget> {
3044                let v: Signal<Option<Color>> = Signal::new(None);
3045                Box::new(ColorEdit::nullable(v))
3046            }
3047            vec![
3048                PreviewVariant::scenario("default", default_var),
3049                PreviewVariant::scenario("with-alpha", with_alpha),
3050                PreviewVariant::scenario("no-hex-in-trigger", no_hex_in_trigger),
3051                PreviewVariant::scenario("nullable", nullable_var),
3052            ]
3053        }
3054        fn build(variant: &str, _knobs: &KnobValues) -> Box<dyn Widget> {
3055            scenario_for::<Self>(variant)
3056        }
3057    }
3058    register_widget_catalog_at!("crates/teksilo-widgets/src/color_edit.rs", ColorEdit);
3059}
3060
3061// =========================================================================
3062// Secure input family (PasswordField)
3063// =========================================================================
3064
3065mod secure_input_family {
3066    use super::*;
3067    use crate::{EchoMode, PasswordField, RevealMode};
3068
3069    impl WidgetCatalog for PasswordField {
3070        fn id() -> &'static str {
3071            "password-field"
3072        }
3073        fn group() -> &'static str {
3074            "Inputs"
3075        }
3076        fn display_name() -> &'static str {
3077            "PasswordField"
3078        }
3079        fn knobs() -> KnobSpec {
3080            KnobSpec::new()
3081                .text("placeholder", "Placeholder", "Enter your password")
3082                .text("text", "Initial text", "hunter2")
3083                .choice(
3084                    "echo_mode",
3085                    "Echo mode",
3086                    &["Masked", "NoEcho", "RevealWhileTyping"],
3087                    0,
3088                )
3089                .choice(
3090                    "reveal_mode",
3091                    "Reveal button",
3092                    &["Toggle", "Hold", "None"],
3093                    0,
3094                )
3095                .bool_("enabled", "Enabled", true)
3096                .bool_("caps_warning", "Caps Lock warning", true)
3097        }
3098        fn variants() -> Vec<PreviewVariant> {
3099            vec![
3100                PreviewVariant::defaults("default"),
3101                PreviewVariant::knobs(
3102                    "reveal-while-typing",
3103                    KnobOverrides::new().choice("echo_mode", 2),
3104                ),
3105                PreviewVariant::knobs(
3106                    "hold-to-reveal",
3107                    KnobOverrides::new().choice("reveal_mode", 1),
3108                ),
3109                PreviewVariant::knobs("no-echo", KnobOverrides::new().choice("echo_mode", 1)),
3110                PreviewVariant::knobs(
3111                    "no-reveal-button",
3112                    KnobOverrides::new().choice("reveal_mode", 2),
3113                ),
3114                PreviewVariant::knobs("disabled", KnobOverrides::new().bool_("enabled", false)),
3115            ]
3116        }
3117        fn build(_variant: &str, knobs: &KnobValues) -> Box<dyn Widget> {
3118            let placeholder = knobs.text("placeholder").get();
3119            let initial = knobs.text("text").get();
3120            let echo = match knobs.choice("echo_mode").get() {
3121                1 => EchoMode::NoEcho,
3122                2 => EchoMode::RevealWhileTyping,
3123                _ => EchoMode::Masked,
3124            };
3125            let reveal = match knobs.choice("reveal_mode").get() {
3126                1 => RevealMode::Hold,
3127                2 => RevealMode::None,
3128                _ => RevealMode::Toggle,
3129            };
3130            let enabled = knobs.bool_("enabled").get();
3131            let caps = knobs.bool_("caps_warning").get();
3132            Box::new(
3133                PasswordField::new(Signal::new(initial))
3134                    .label(lit!("Password"))
3135                    .placeholder(lit!(placeholder))
3136                    .echo_mode(echo)
3137                    .reveal_mode(reveal)
3138                    .enabled(enabled)
3139                    .caps_lock_warning(caps),
3140            )
3141        }
3142    }
3143    register_widget_catalog_at!(
3144        "crates/teksilo-widgets/src/password_field.rs",
3145        PasswordField
3146    );
3147}
3148
3149#[cfg(all(test, feature = "preview"))]
3150mod build_with_children_tests {
3151    use super::*;
3152    use teksilo_canvas::SizeProposal;
3153    use teksilo_core::widget_tree::WidgetTree;
3154    use teksilo_preview::{CatalogEntry, KnobValues, SlottedChild, WidgetCategory, find_by_id};
3155
3156    fn knobs_for(entry: &dyn CatalogEntry) -> KnobValues {
3157        KnobValues::from_spec(&entry.knobs(), None)
3158    }
3159
3160    /// Collect every descendant id under `root` (exclusive).
3161    fn descendants(
3162        tree: &WidgetTree,
3163        root: teksilo_core::widget_id::WidgetId,
3164    ) -> Vec<teksilo_core::widget_id::WidgetId> {
3165        let mut out = Vec::new();
3166        let mut stack = vec![root];
3167        while let Some(n) = stack.pop() {
3168            for ch in tree.children(n) {
3169                out.push(ch);
3170                stack.push(ch);
3171            }
3172        }
3173        out
3174    }
3175
3176    #[test]
3177    fn leaf_button_default_ignores_children() {
3178        let entry = find_by_id("button").expect("button registered");
3179        assert_eq!(entry.category(), WidgetCategory::Leaf);
3180        assert!(entry.icon().is_some());
3181        let knobs = knobs_for(entry);
3182        let mut tree = WidgetTree::new();
3183        let stray = tree.add(TextWidget::new(lit!("x")));
3184        let w = entry.build_with_children(
3185            "default",
3186            &knobs,
3187            vec![SlottedChild {
3188                slot: None,
3189                id: stray,
3190            }],
3191        );
3192        let id = tree.add_boxed(w);
3193        tree.layout(SizeProposal::exact(400.0, 200.0));
3194        // The leaf default ignores injected children — the stray is not adopted.
3195        assert!(!descendants(&tree, id).contains(&stray));
3196    }
3197
3198    #[test]
3199    fn vstack_container_a_wires_ordered_children() {
3200        let entry = find_by_id("vstack").expect("vstack registered");
3201        assert_eq!(entry.category(), WidgetCategory::ContainerA);
3202        assert!(entry.icon().is_some());
3203        let knobs = knobs_for(entry);
3204        let mut tree = WidgetTree::new();
3205        let a = tree.add(TextWidget::new(lit!("A")));
3206        let b = tree.add(TextWidget::new(lit!("B")));
3207        let c = tree.add(TextWidget::new(lit!("C")));
3208        let w = entry.build_with_children(
3209            "default",
3210            &knobs,
3211            vec![
3212                SlottedChild { slot: None, id: a },
3213                SlottedChild { slot: None, id: b },
3214                SlottedChild { slot: None, id: c },
3215            ],
3216        );
3217        let id = tree.add_boxed(w);
3218        tree.layout(SizeProposal::exact(400.0, 300.0));
3219        assert_eq!(tree.children(id), vec![a, b, c]);
3220    }
3221
3222    #[test]
3223    fn card_container_b_routes_named_slots() {
3224        let entry = find_by_id("card").expect("card registered");
3225        assert_eq!(entry.category(), WidgetCategory::ContainerB);
3226        assert_eq!(entry.slots(), &["header", "content", "footer"][..]);
3227        let knobs = knobs_for(entry);
3228        let mut tree = WidgetTree::new();
3229        let header = tree.add(TextWidget::new(lit!("H")));
3230        let content = tree.add(TextWidget::new(lit!("C")));
3231        let footer = tree.add(TextWidget::new(lit!("F")));
3232        let w = entry.build_with_children(
3233            "default",
3234            &knobs,
3235            vec![
3236                SlottedChild {
3237                    slot: Some("header".into()),
3238                    id: header,
3239                },
3240                SlottedChild {
3241                    slot: Some("content".into()),
3242                    id: content,
3243                },
3244                SlottedChild {
3245                    slot: Some("footer".into()),
3246                    id: footer,
3247                },
3248            ],
3249        );
3250        let id = tree.add_boxed(w);
3251        tree.layout(SizeProposal::exact(400.0, 300.0));
3252        let all = descendants(&tree, id);
3253        assert!(all.contains(&header), "header slot wired");
3254        assert!(all.contains(&content), "content slot wired");
3255        assert!(all.contains(&footer), "footer slot wired");
3256    }
3257
3258    #[test]
3259    fn curated_widgets_have_icons() {
3260        for id in [
3261            "vstack",
3262            "hstack",
3263            "zstack",
3264            "grid",
3265            "padding",
3266            "expand",
3267            "center",
3268            "spacer",
3269            "button",
3270            "text_widget",
3271            "checkbox",
3272            "text_input",
3273            "toggle",
3274            "combo_box",
3275            "slider",
3276            "card",
3277            "panel",
3278            "scroll_area",
3279        ] {
3280            let entry = find_by_id(id).unwrap_or_else(|| panic!("{id} registered"));
3281            assert!(entry.icon().is_some(), "{id} should have an icon");
3282        }
3283    }
3284}