Skip to main content

rae/
catalog.rs

1use std::collections::HashSet;
2
3use crate::{
4    built_in_recipe_kinds, built_in_token_presets, sanitize_str, sanitize_title, AppRecipeKind,
5    ComponentRole, ComponentSize, DesignTokenSet, PlatformStyle, WidgetControlKind, WidgetState,
6    WidgetVariant,
7};
8
9#[cfg(feature = "serde")]
10use serde::{Deserialize, Serialize};
11
12pub const WIDGET_CATALOG_MAX_WIDGETS: usize = 256;
13pub const WIDGET_DESCRIPTOR_MAX_EXAMPLES: usize = 16;
14pub const WIDGET_CATALOG_QUERY_MAX_RESULTS: usize = 128;
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
17#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
18#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
19pub enum WidgetCategory {
20    Action,
21    Input,
22    Selection,
23    Navigation,
24    Feedback,
25    DataDisplay,
26    Layout,
27    Overlay,
28    Ai,
29    Code,
30    ProAudio,
31    Media,
32    Shell,
33}
34
35impl WidgetCategory {
36    pub fn label(self) -> &'static str {
37        match self {
38            Self::Action => "action",
39            Self::Input => "input",
40            Self::Selection => "selection",
41            Self::Navigation => "navigation",
42            Self::Feedback => "feedback",
43            Self::DataDisplay => "data_display",
44            Self::Layout => "layout",
45            Self::Overlay => "overlay",
46            Self::Ai => "ai",
47            Self::Code => "code",
48            Self::ProAudio => "pro_audio",
49            Self::Media => "media",
50            Self::Shell => "shell",
51        }
52    }
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
57#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
58pub enum WidgetComplexity {
59    Primitive,
60    Stateful,
61    Composite,
62    Surface,
63    Workflow,
64}
65
66impl WidgetComplexity {
67    pub fn label(self) -> &'static str {
68        match self {
69            Self::Primitive => "primitive",
70            Self::Stateful => "stateful",
71            Self::Composite => "composite",
72            Self::Surface => "surface",
73            Self::Workflow => "workflow",
74        }
75    }
76}
77
78#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
79#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
80#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
81pub enum WidgetUseCase {
82    GeneralDesktop,
83    AiWorkspace,
84    DeveloperTools,
85    AudioProduction,
86    Dashboard,
87    DocumentEditing,
88    BrowserShell,
89    CreativeTools,
90}
91
92impl WidgetUseCase {
93    pub fn label(self) -> &'static str {
94        match self {
95            Self::GeneralDesktop => "general_desktop",
96            Self::AiWorkspace => "ai_workspace",
97            Self::DeveloperTools => "developer_tools",
98            Self::AudioProduction => "audio_production",
99            Self::Dashboard => "dashboard",
100            Self::DocumentEditing => "document_editing",
101            Self::BrowserShell => "browser_shell",
102            Self::CreativeTools => "creative_tools",
103        }
104    }
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Eq)]
108#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
109pub struct WidgetCategoryCount {
110    pub category: WidgetCategory,
111    pub count: usize,
112}
113
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
116pub struct WidgetUseCaseCount {
117    pub use_case: WidgetUseCase,
118    pub count: usize,
119}
120
121#[derive(Clone, Copy, Debug, PartialEq, Eq)]
122#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
123pub struct WidgetComplexityCount {
124    pub complexity: WidgetComplexity,
125    pub count: usize,
126}
127
128#[derive(Clone, Debug, PartialEq, Eq)]
129#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
130pub struct WidgetCatalogSummary {
131    pub widget_count: usize,
132    pub token_preset_count: usize,
133    pub platform_style_count: usize,
134    pub recipe_kind_count: usize,
135    pub example_count: usize,
136    pub category_counts: Vec<WidgetCategoryCount>,
137    pub use_case_counts: Vec<WidgetUseCaseCount>,
138    pub complexity_counts: Vec<WidgetComplexityCount>,
139}
140
141#[derive(Clone, Copy, Debug, PartialEq, Eq)]
142#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
143pub struct WidgetStateSet {
144    pub base: bool,
145    pub hovered: bool,
146    pub pressed: bool,
147    pub focused: bool,
148    pub disabled: bool,
149    pub selected: bool,
150    pub checked: bool,
151    pub loading: bool,
152    pub error: bool,
153}
154
155impl WidgetStateSet {
156    pub const fn passive() -> Self {
157        Self {
158            base: true,
159            hovered: false,
160            pressed: false,
161            focused: false,
162            disabled: false,
163            selected: false,
164            checked: false,
165            loading: false,
166            error: false,
167        }
168    }
169
170    pub const fn interactive() -> Self {
171        Self {
172            base: true,
173            hovered: true,
174            pressed: true,
175            focused: true,
176            disabled: true,
177            selected: false,
178            checked: false,
179            loading: false,
180            error: false,
181        }
182    }
183
184    pub const fn selectable() -> Self {
185        Self {
186            selected: true,
187            checked: true,
188            ..Self::interactive()
189        }
190    }
191
192    pub const fn editable() -> Self {
193        Self {
194            error: true,
195            ..Self::interactive()
196        }
197    }
198
199    pub const fn async_surface() -> Self {
200        Self {
201            loading: true,
202            error: true,
203            selected: true,
204            ..Self::interactive()
205        }
206    }
207
208    pub fn state_count(self) -> usize {
209        [
210            self.base,
211            self.hovered,
212            self.pressed,
213            self.focused,
214            self.disabled,
215            self.selected,
216            self.checked,
217            self.loading,
218            self.error,
219        ]
220        .into_iter()
221        .filter(|enabled| *enabled)
222        .count()
223    }
224}
225
226#[derive(Clone, Debug, PartialEq)]
227#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
228pub struct WidgetExample {
229    pub id: String,
230    pub title: String,
231    pub description: String,
232    pub state: WidgetState,
233    pub variant: WidgetVariant,
234    pub size: ComponentSize,
235    pub role: ComponentRole,
236}
237
238impl WidgetExample {
239    pub fn new(
240        id: impl Into<String>,
241        title: impl Into<String>,
242        description: impl Into<String>,
243    ) -> Self {
244        Self {
245            id: sanitize_catalog_id(&id.into(), "example"),
246            title: sanitize_title(&title.into(), 96),
247            description: sanitize_title(&description.into(), 220),
248            state: WidgetState::new(),
249            variant: WidgetVariant::Filled,
250            size: ComponentSize::Regular,
251            role: ComponentRole::Primary,
252        }
253    }
254
255    pub fn with_state(mut self, state: WidgetState) -> Self {
256        self.state = state;
257        self
258    }
259
260    pub fn with_variant(mut self, variant: WidgetVariant) -> Self {
261        self.variant = variant;
262        self
263    }
264
265    pub fn with_size(mut self, size: ComponentSize) -> Self {
266        self.size = size;
267        self
268    }
269
270    pub fn with_role(mut self, role: ComponentRole) -> Self {
271        self.role = role;
272        self
273    }
274
275    pub fn sanitized(mut self) -> Self {
276        self.id = sanitize_catalog_id(&self.id, "example");
277        self.title = sanitize_title(&self.title, 96);
278        self.description = sanitize_title(&self.description, 220);
279        self
280    }
281}
282
283#[derive(Clone, Debug, PartialEq)]
284#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
285pub struct WidgetDescriptor {
286    pub id: String,
287    pub name: String,
288    pub category: WidgetCategory,
289    pub summary: String,
290    pub control_kind: Option<WidgetControlKind>,
291    pub complexity: WidgetComplexity,
292    pub variants: Vec<WidgetVariant>,
293    pub states: WidgetStateSet,
294    pub sizes: Vec<ComponentSize>,
295    pub roles: Vec<ComponentRole>,
296    pub use_cases: Vec<WidgetUseCase>,
297    pub examples: Vec<WidgetExample>,
298    pub token_refs: Vec<String>,
299    pub renderer_notes: String,
300}
301
302impl WidgetDescriptor {
303    pub fn supports_use_case(&self, use_case: WidgetUseCase) -> bool {
304        self.use_cases.contains(&use_case)
305    }
306
307    pub fn matches_query_text(&self, text: &str) -> bool {
308        let text = sanitize_query_text(text);
309        if text.is_empty() {
310            return true;
311        }
312        let haystack = self.search_text();
313        text.split_whitespace()
314            .all(|token| haystack.contains(token))
315    }
316
317    pub fn sanitized(mut self) -> Self {
318        self.id = sanitize_catalog_id(&self.id, "widget");
319        self.name = sanitize_title(&self.name, 96);
320        self.summary = sanitize_title(&self.summary, 260);
321        self.examples.truncate(WIDGET_DESCRIPTOR_MAX_EXAMPLES);
322        self.examples
323            .iter_mut()
324            .for_each(|example| *example = example.clone().sanitized());
325        self.token_refs = self
326            .token_refs
327            .into_iter()
328            .map(|token| sanitize_catalog_id(&token, "token"))
329            .filter(|token| !token.is_empty())
330            .collect();
331        self.renderer_notes = sanitize_title(&self.renderer_notes, 260);
332        if self.variants.is_empty() {
333            self.variants.push(WidgetVariant::Filled);
334        }
335        if self.sizes.is_empty() {
336            self.sizes.push(ComponentSize::Regular);
337        }
338        if self.roles.is_empty() {
339            self.roles.push(ComponentRole::Neutral);
340        }
341        self
342    }
343
344    fn search_text(&self) -> String {
345        let mut text = format!(
346            "{} {} {} {} {:?}",
347            self.id,
348            self.name,
349            self.summary,
350            self.category.label(),
351            self.control_kind
352        )
353        .to_ascii_lowercase();
354        for use_case in &self.use_cases {
355            text.push(' ');
356            text.push_str(use_case.label());
357        }
358        for example in &self.examples {
359            text.push(' ');
360            text.push_str(&example.title.to_ascii_lowercase());
361            text.push(' ');
362            text.push_str(&example.description.to_ascii_lowercase());
363        }
364        text
365    }
366}
367
368#[derive(Clone, Debug, Default, PartialEq, Eq)]
369#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
370pub struct WidgetCatalogQuery {
371    pub text: String,
372    pub category: Option<WidgetCategory>,
373    pub use_case: Option<WidgetUseCase>,
374    pub complexity: Option<WidgetComplexity>,
375    pub control_kind: Option<WidgetControlKind>,
376    pub role: Option<ComponentRole>,
377    pub variant: Option<WidgetVariant>,
378    pub limit: usize,
379}
380
381impl WidgetCatalogQuery {
382    pub fn new() -> Self {
383        Self {
384            limit: WIDGET_CATALOG_QUERY_MAX_RESULTS,
385            ..Self::default()
386        }
387    }
388
389    pub fn text(mut self, text: impl AsRef<str>) -> Self {
390        self.text = sanitize_query_text(text.as_ref());
391        self
392    }
393
394    pub fn category(mut self, category: WidgetCategory) -> Self {
395        self.category = Some(category);
396        self
397    }
398
399    pub fn use_case(mut self, use_case: WidgetUseCase) -> Self {
400        self.use_case = Some(use_case);
401        self
402    }
403
404    pub fn complexity(mut self, complexity: WidgetComplexity) -> Self {
405        self.complexity = Some(complexity);
406        self
407    }
408
409    pub fn control_kind(mut self, control_kind: WidgetControlKind) -> Self {
410        self.control_kind = Some(control_kind);
411        self
412    }
413
414    pub fn role(mut self, role: ComponentRole) -> Self {
415        self.role = Some(role);
416        self
417    }
418
419    pub fn variant(mut self, variant: WidgetVariant) -> Self {
420        self.variant = Some(variant);
421        self
422    }
423
424    pub fn limit(mut self, limit: usize) -> Self {
425        self.limit = limit.min(WIDGET_CATALOG_QUERY_MAX_RESULTS);
426        self
427    }
428
429    pub fn sanitized(mut self) -> Self {
430        self.text = sanitize_query_text(&self.text);
431        self.limit = if self.limit == 0 {
432            WIDGET_CATALOG_QUERY_MAX_RESULTS
433        } else {
434            self.limit.min(WIDGET_CATALOG_QUERY_MAX_RESULTS)
435        };
436        self
437    }
438
439    fn matches(&self, widget: &WidgetDescriptor) -> bool {
440        self.category
441            .map_or(true, |category| widget.category == category)
442            && self
443                .use_case
444                .map_or(true, |use_case| widget.supports_use_case(use_case))
445            && self
446                .complexity
447                .map_or(true, |complexity| widget.complexity == complexity)
448            && self.control_kind.map_or(true, |control_kind| {
449                widget.control_kind == Some(control_kind)
450            })
451            && self.role.map_or(true, |role| widget.roles.contains(&role))
452            && self
453                .variant
454                .map_or(true, |variant| widget.variants.contains(&variant))
455            && widget.matches_query_text(&self.text)
456    }
457}
458
459#[derive(Clone, Debug, PartialEq)]
460#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
461pub struct WidgetCatalog {
462    pub name: String,
463    pub version: String,
464    pub token_presets: Vec<DesignTokenSet>,
465    pub platform_styles: Vec<PlatformStyle>,
466    pub recipe_kinds: Vec<AppRecipeKind>,
467    pub widgets: Vec<WidgetDescriptor>,
468}
469
470impl WidgetCatalog {
471    pub fn find(&self, id: &str) -> Option<&WidgetDescriptor> {
472        let id = sanitize_catalog_id(id, "widget");
473        self.widgets.iter().find(|widget| widget.id == id)
474    }
475
476    pub fn by_category(&self, category: WidgetCategory) -> Vec<&WidgetDescriptor> {
477        self.widgets
478            .iter()
479            .filter(|widget| widget.category == category)
480            .collect()
481    }
482
483    pub fn by_use_case(&self, use_case: WidgetUseCase) -> Vec<&WidgetDescriptor> {
484        self.widgets
485            .iter()
486            .filter(|widget| widget.supports_use_case(use_case))
487            .collect()
488    }
489
490    pub fn category_counts(&self) -> Vec<WidgetCategoryCount> {
491        all_widget_categories()
492            .into_iter()
493            .map(|category| WidgetCategoryCount {
494                category,
495                count: self.by_category(category).len(),
496            })
497            .collect()
498    }
499
500    pub fn use_case_counts(&self) -> Vec<WidgetUseCaseCount> {
501        all_widget_use_cases()
502            .into_iter()
503            .map(|use_case| WidgetUseCaseCount {
504                use_case,
505                count: self.by_use_case(use_case).len(),
506            })
507            .collect()
508    }
509
510    pub fn complexity_counts(&self) -> Vec<WidgetComplexityCount> {
511        all_widget_complexities()
512            .into_iter()
513            .map(|complexity| WidgetComplexityCount {
514                complexity,
515                count: self
516                    .widgets
517                    .iter()
518                    .filter(|widget| widget.complexity == complexity)
519                    .count(),
520            })
521            .collect()
522    }
523
524    pub fn summary(&self) -> WidgetCatalogSummary {
525        WidgetCatalogSummary {
526            widget_count: self.widgets.len(),
527            token_preset_count: self.token_presets.len(),
528            platform_style_count: self.platform_styles.len(),
529            recipe_kind_count: self.recipe_kinds.len(),
530            example_count: self
531                .widgets
532                .iter()
533                .map(|widget| widget.examples.len())
534                .sum(),
535            category_counts: self.category_counts(),
536            use_case_counts: self.use_case_counts(),
537            complexity_counts: self.complexity_counts(),
538        }
539    }
540
541    pub fn query(&self, query: WidgetCatalogQuery) -> Vec<&WidgetDescriptor> {
542        let query = query.sanitized();
543        self.widgets
544            .iter()
545            .filter(|widget| query.matches(widget))
546            .take(query.limit)
547            .collect()
548    }
549
550    pub fn query_ids(&self, query: WidgetCatalogQuery) -> Vec<&str> {
551        self.query(query)
552            .into_iter()
553            .map(|widget| widget.id.as_str())
554            .collect()
555    }
556
557    pub fn token_preset(&self, id: &str) -> Option<&DesignTokenSet> {
558        let id = sanitize_catalog_id(id, "token");
559        self.token_presets.iter().find(|preset| preset.id == id)
560    }
561
562    pub fn validate(&self) -> Result<(), CatalogValidationError> {
563        if self.widgets.is_empty() {
564            return Err(CatalogValidationError::new("catalog has no widgets"));
565        }
566        if self.token_presets.is_empty() {
567            return Err(CatalogValidationError::new("catalog has no token presets"));
568        }
569        if self.recipe_kinds.is_empty() {
570            return Err(CatalogValidationError::new("catalog has no layout recipes"));
571        }
572
573        let mut widget_ids = HashSet::new();
574        for widget in &self.widgets {
575            if widget.id.is_empty() {
576                return Err(CatalogValidationError::new("widget has empty id"));
577            }
578            if !widget_ids.insert(widget.id.as_str()) {
579                return Err(CatalogValidationError::new(format!(
580                    "duplicate widget id {}",
581                    widget.id
582                )));
583            }
584            if widget.name.is_empty() || widget.summary.is_empty() {
585                return Err(CatalogValidationError::new(format!(
586                    "widget {} has empty display text",
587                    widget.id
588                )));
589            }
590            if widget.examples.is_empty() {
591                return Err(CatalogValidationError::new(format!(
592                    "widget {} has no examples",
593                    widget.id
594                )));
595            }
596            if !widget.states.base {
597                return Err(CatalogValidationError::new(format!(
598                    "widget {} does not include a base state",
599                    widget.id
600                )));
601            }
602        }
603
604        let mut token_ids = HashSet::new();
605        for preset in &self.token_presets {
606            if !token_ids.insert(preset.id.as_str()) {
607                return Err(CatalogValidationError::new(format!(
608                    "duplicate token id {}",
609                    preset.id
610                )));
611            }
612        }
613
614        Ok(())
615    }
616}
617
618#[derive(Clone, Debug, PartialEq, Eq)]
619pub struct CatalogValidationError {
620    pub message: String,
621}
622
623impl CatalogValidationError {
624    pub fn new(message: impl Into<String>) -> Self {
625        Self {
626            message: sanitize_title(&message.into(), 240),
627        }
628    }
629}
630
631impl std::fmt::Display for CatalogValidationError {
632    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
633        formatter.write_str(&self.message)
634    }
635}
636
637impl std::error::Error for CatalogValidationError {}
638
639pub fn built_in_widget_catalog() -> WidgetCatalog {
640    let catalog = WidgetCatalog {
641        name: "rae professional desktop widget catalog".to_string(),
642        version: env!("CARGO_PKG_VERSION").to_string(),
643        token_presets: built_in_token_presets(),
644        platform_styles: vec![
645            PlatformStyle::CrossPlatform,
646            PlatformStyle::Macos,
647            PlatformStyle::Windows,
648            PlatformStyle::Linux,
649            PlatformStyle::Web,
650        ],
651        recipe_kinds: built_in_recipe_kinds(),
652        widgets: widget_descriptors()
653            .into_iter()
654            .take(WIDGET_CATALOG_MAX_WIDGETS)
655            .map(WidgetDescriptor::sanitized)
656            .collect(),
657    };
658    debug_assert!(catalog.validate().is_ok());
659    catalog
660}
661
662fn all_widget_categories() -> [WidgetCategory; 13] {
663    [
664        WidgetCategory::Action,
665        WidgetCategory::Input,
666        WidgetCategory::Selection,
667        WidgetCategory::Navigation,
668        WidgetCategory::Feedback,
669        WidgetCategory::DataDisplay,
670        WidgetCategory::Layout,
671        WidgetCategory::Overlay,
672        WidgetCategory::Ai,
673        WidgetCategory::Code,
674        WidgetCategory::ProAudio,
675        WidgetCategory::Media,
676        WidgetCategory::Shell,
677    ]
678}
679
680fn all_widget_use_cases() -> [WidgetUseCase; 8] {
681    [
682        WidgetUseCase::GeneralDesktop,
683        WidgetUseCase::AiWorkspace,
684        WidgetUseCase::DeveloperTools,
685        WidgetUseCase::AudioProduction,
686        WidgetUseCase::Dashboard,
687        WidgetUseCase::DocumentEditing,
688        WidgetUseCase::BrowserShell,
689        WidgetUseCase::CreativeTools,
690    ]
691}
692
693fn all_widget_complexities() -> [WidgetComplexity; 5] {
694    [
695        WidgetComplexity::Primitive,
696        WidgetComplexity::Stateful,
697        WidgetComplexity::Composite,
698        WidgetComplexity::Surface,
699        WidgetComplexity::Workflow,
700    ]
701}
702
703fn widget_descriptors() -> Vec<WidgetDescriptor> {
704    vec![
705        descriptor(
706            "button",
707            "Button",
708            WidgetCategory::Action,
709            "Primary command surface for committing user intent.",
710            Some(WidgetControlKind::Button),
711            WidgetComplexity::Primitive,
712            WidgetStateSet::interactive(),
713            &[WidgetUseCase::GeneralDesktop],
714        ),
715        descriptor(
716            "icon_button",
717            "Icon Button",
718            WidgetCategory::Action,
719            "Compact glyph-first command with tooltip and focus-ring states.",
720            Some(WidgetControlKind::IconButton),
721            WidgetComplexity::Primitive,
722            WidgetStateSet::interactive(),
723            &[WidgetUseCase::GeneralDesktop, WidgetUseCase::CreativeTools],
724        ),
725        descriptor(
726            "toggle",
727            "Toggle",
728            WidgetCategory::Selection,
729            "Binary switch for persisted or immediate settings.",
730            Some(WidgetControlKind::Toggle),
731            WidgetComplexity::Stateful,
732            WidgetStateSet::selectable(),
733            &[
734                WidgetUseCase::GeneralDesktop,
735                WidgetUseCase::AudioProduction,
736            ],
737        ),
738        descriptor(
739            "checkbox",
740            "Checkbox",
741            WidgetCategory::Selection,
742            "Checkable row control with checked, focused, disabled, and error variants.",
743            Some(WidgetControlKind::Checkbox),
744            WidgetComplexity::Stateful,
745            WidgetStateSet::selectable(),
746            &[
747                WidgetUseCase::GeneralDesktop,
748                WidgetUseCase::DocumentEditing,
749            ],
750        ),
751        descriptor(
752            "text_input",
753            "Text Input",
754            WidgetCategory::Input,
755            "Editable single-line field backed by sanitized prompt/text behavior.",
756            Some(WidgetControlKind::TextInput),
757            WidgetComplexity::Stateful,
758            WidgetStateSet::editable(),
759            &[WidgetUseCase::GeneralDesktop, WidgetUseCase::DeveloperTools],
760        ),
761        descriptor(
762            "slider",
763            "Slider",
764            WidgetCategory::Input,
765            "Normalized scalar input for levels, intensity, position, or thresholds.",
766            Some(WidgetControlKind::Slider),
767            WidgetComplexity::Stateful,
768            WidgetStateSet::interactive(),
769            &[
770                WidgetUseCase::GeneralDesktop,
771                WidgetUseCase::AudioProduction,
772                WidgetUseCase::CreativeTools,
773            ],
774        ),
775        descriptor(
776            "select",
777            "Select",
778            WidgetCategory::Selection,
779            "Choice-list trigger with capped sanitized options and disabled-item skipping.",
780            Some(WidgetControlKind::Select),
781            WidgetComplexity::Stateful,
782            WidgetStateSet::interactive(),
783            &[
784                WidgetUseCase::GeneralDesktop,
785                WidgetUseCase::DocumentEditing,
786            ],
787        ),
788        descriptor(
789            "list_item",
790            "List Item",
791            WidgetCategory::Selection,
792            "Selectable collection row for menus, trees, palettes, and result lists.",
793            Some(WidgetControlKind::ListItem),
794            WidgetComplexity::Stateful,
795            WidgetStateSet::selectable(),
796            &[WidgetUseCase::GeneralDesktop, WidgetUseCase::DeveloperTools],
797        ),
798        descriptor(
799            "label",
800            "Label",
801            WidgetCategory::DataDisplay,
802            "Text label with muted, strong, helper, and diagnostic roles.",
803            Some(WidgetControlKind::Label),
804            WidgetComplexity::Primitive,
805            WidgetStateSet::passive(),
806            &[WidgetUseCase::GeneralDesktop],
807        ),
808        descriptor(
809            "divider",
810            "Divider",
811            WidgetCategory::Layout,
812            "Hairline or semantic separator with token-driven spacing.",
813            Some(WidgetControlKind::Divider),
814            WidgetComplexity::Primitive,
815            WidgetStateSet::passive(),
816            &[WidgetUseCase::GeneralDesktop],
817        ),
818        descriptor(
819            "card",
820            "Card",
821            WidgetCategory::Layout,
822            "Container surface with filled, tonal, outline, ghost, or glass material.",
823            Some(WidgetControlKind::Card),
824            WidgetComplexity::Surface,
825            WidgetStateSet::interactive(),
826            &[WidgetUseCase::GeneralDesktop, WidgetUseCase::Dashboard],
827        ),
828        descriptor(
829            "tab_bar",
830            "Tab Bar",
831            WidgetCategory::Navigation,
832            "Keyboard-friendly tab model with capped entries and disabled-tab skipping.",
833            None,
834            WidgetComplexity::Composite,
835            WidgetStateSet::selectable(),
836            &[WidgetUseCase::GeneralDesktop, WidgetUseCase::BrowserShell],
837        ),
838        descriptor(
839            "scroll_area",
840            "Scroll Area",
841            WidgetCategory::Layout,
842            "Viewport model with visible ranges, scroll anchoring, and scrollbar geometry.",
843            None,
844            WidgetComplexity::Composite,
845            WidgetStateSet::interactive(),
846            &[WidgetUseCase::GeneralDesktop, WidgetUseCase::DeveloperTools],
847        ),
848        descriptor(
849            "pane",
850            "Pane",
851            WidgetCategory::Shell,
852            "Desktop pane chrome for focused, split, grid, and stacked work surfaces.",
853            None,
854            WidgetComplexity::Surface,
855            WidgetStateSet::interactive(),
856            &[WidgetUseCase::GeneralDesktop, WidgetUseCase::DeveloperTools],
857        ),
858        descriptor(
859            "overlay",
860            "Overlay",
861            WidgetCategory::Overlay,
862            "Z-ordered popover, palette, menu, tooltip, or modal layer model.",
863            None,
864            WidgetComplexity::Surface,
865            WidgetStateSet::async_surface(),
866            &[WidgetUseCase::GeneralDesktop],
867        ),
868        descriptor(
869            "prompt",
870            "Prompt",
871            WidgetCategory::Ai,
872            "AI/developer composer model with sanitized cursor and selection behavior.",
873            None,
874            WidgetComplexity::Stateful,
875            WidgetStateSet::editable(),
876            &[WidgetUseCase::AiWorkspace, WidgetUseCase::DeveloperTools],
877        ),
878        descriptor(
879            "transcript",
880            "Transcript",
881            WidgetCategory::Ai,
882            "Role-aware conversation output rows with bounded viewport materialization.",
883            None,
884            WidgetComplexity::Composite,
885            WidgetStateSet::async_surface(),
886            &[WidgetUseCase::AiWorkspace],
887        ),
888        descriptor(
889            "command_palette",
890            "Command Palette",
891            WidgetCategory::Overlay,
892            "Searchable action surface with bounded matching and keyboard selection.",
893            None,
894            WidgetComplexity::Composite,
895            WidgetStateSet::interactive(),
896            &[WidgetUseCase::GeneralDesktop, WidgetUseCase::DeveloperTools],
897        ),
898        descriptor(
899            "code_block",
900            "Code Block",
901            WidgetCategory::Code,
902            "Sanitized fenced-code renderer model with token and summary analysis.",
903            None,
904            WidgetComplexity::Composite,
905            WidgetStateSet::passive(),
906            &[WidgetUseCase::DeveloperTools, WidgetUseCase::AiWorkspace],
907        ),
908        descriptor(
909            "code_editor_shell",
910            "Code Editor Shell",
911            WidgetCategory::Code,
912            "Recipe-level editor pane with files, diagnostics, output, and assistant surfaces.",
913            None,
914            WidgetComplexity::Workflow,
915            WidgetStateSet::async_surface(),
916            &[WidgetUseCase::DeveloperTools],
917        ),
918        descriptor(
919            "diagnostics_panel",
920            "Diagnostics Panel",
921            WidgetCategory::Code,
922            "Grouped warning, error, and info rows with source location metadata.",
923            None,
924            WidgetComplexity::Composite,
925            WidgetStateSet::async_surface(),
926            &[WidgetUseCase::DeveloperTools, WidgetUseCase::AiWorkspace],
927        ),
928        descriptor(
929            "symbol_outline",
930            "Symbol Outline",
931            WidgetCategory::Code,
932            "Navigable function, type, and section index for code or document buffers.",
933            None,
934            WidgetComplexity::Composite,
935            WidgetStateSet::selectable(),
936            &[
937                WidgetUseCase::DeveloperTools,
938                WidgetUseCase::DocumentEditing,
939            ],
940        ),
941        descriptor(
942            "code_minimap",
943            "Code Minimap",
944            WidgetCategory::Code,
945            "Dense buffer overview with viewport, diagnostics, and search hit markers.",
946            None,
947            WidgetComplexity::Composite,
948            WidgetStateSet::interactive(),
949            &[WidgetUseCase::DeveloperTools],
950        ),
951        descriptor(
952            "inline_completion",
953            "Inline Completion",
954            WidgetCategory::Code,
955            "Ghost-text suggestion surface for completions, rewrites, and assistant edits.",
956            None,
957            WidgetComplexity::Stateful,
958            WidgetStateSet::async_surface(),
959            &[WidgetUseCase::DeveloperTools, WidgetUseCase::AiWorkspace],
960        ),
961        descriptor(
962            "diff_viewer",
963            "Diff Viewer",
964            WidgetCategory::Code,
965            "Side-by-side or unified change review surface with hunks and annotations.",
966            None,
967            WidgetComplexity::Composite,
968            WidgetStateSet::selectable(),
969            &[
970                WidgetUseCase::DeveloperTools,
971                WidgetUseCase::DocumentEditing,
972            ],
973        ),
974        descriptor(
975            "metric_card",
976            "Metric Card",
977            WidgetCategory::DataDisplay,
978            "Dashboard KPI surface with trend, status, and semantic role coloring.",
979            None,
980            WidgetComplexity::Composite,
981            WidgetStateSet::passive(),
982            &[WidgetUseCase::Dashboard],
983        ),
984        descriptor(
985            "data_table",
986            "Data Table",
987            WidgetCategory::DataDisplay,
988            "Dense desktop table metadata for rows, columns, selection, and scroll regions.",
989            None,
990            WidgetComplexity::Composite,
991            WidgetStateSet::selectable(),
992            &[WidgetUseCase::Dashboard, WidgetUseCase::GeneralDesktop],
993        ),
994        descriptor(
995            "chart_panel",
996            "Chart Panel",
997            WidgetCategory::DataDisplay,
998            "Token-aware chart container for retained or immediate renderer plots.",
999            None,
1000            WidgetComplexity::Surface,
1001            WidgetStateSet::passive(),
1002            &[WidgetUseCase::Dashboard],
1003        ),
1004        descriptor(
1005            "audio_meter",
1006            "Audio Meter",
1007            WidgetCategory::ProAudio,
1008            "Peak, RMS, or loudness meter model with standard green/yellow/red segments.",
1009            None,
1010            WidgetComplexity::Stateful,
1011            WidgetStateSet::passive(),
1012            &[WidgetUseCase::AudioProduction],
1013        ),
1014        descriptor(
1015            "audio_fader",
1016            "Audio Fader",
1017            WidgetCategory::ProAudio,
1018            "Decibel fader model with min/max/unity values and mute/solo/arm state.",
1019            None,
1020            WidgetComplexity::Stateful,
1021            WidgetStateSet::interactive(),
1022            &[WidgetUseCase::AudioProduction],
1023        ),
1024        descriptor(
1025            "audio_knob",
1026            "Audio Knob",
1027            WidgetCategory::ProAudio,
1028            "Continuous or bipolar rotary control for gain, pan, frequency, and tone.",
1029            None,
1030            WidgetComplexity::Stateful,
1031            WidgetStateSet::interactive(),
1032            &[WidgetUseCase::AudioProduction],
1033        ),
1034        descriptor(
1035            "audio_waveform",
1036            "Audio Waveform",
1037            WidgetCategory::ProAudio,
1038            "Capped sample waveform model for clips, previews, loopers, and editors.",
1039            None,
1040            WidgetComplexity::Composite,
1041            WidgetStateSet::passive(),
1042            &[WidgetUseCase::AudioProduction, WidgetUseCase::CreativeTools],
1043        ),
1044        descriptor(
1045            "audio_spectrum",
1046            "Audio Spectrum",
1047            WidgetCategory::ProAudio,
1048            "Bounded spectral-band data for analyzers and EQ visualizations.",
1049            None,
1050            WidgetComplexity::Composite,
1051            WidgetStateSet::passive(),
1052            &[WidgetUseCase::AudioProduction],
1053        ),
1054        descriptor(
1055            "audio_transport",
1056            "Audio Transport",
1057            WidgetCategory::ProAudio,
1058            "Playback/record/loop transport state with BPM, sample-rate, and timeline data.",
1059            None,
1060            WidgetComplexity::Composite,
1061            WidgetStateSet::interactive(),
1062            &[WidgetUseCase::AudioProduction],
1063        ),
1064        descriptor(
1065            "audio_plugin_panel",
1066            "Audio Plugin Panel",
1067            WidgetCategory::ProAudio,
1068            "Plugin macro-control surface with bypass, latency, and automation metadata.",
1069            None,
1070            WidgetComplexity::Surface,
1071            WidgetStateSet::interactive(),
1072            &[WidgetUseCase::AudioProduction],
1073        ),
1074        descriptor(
1075            "audio_mixer_strip",
1076            "Audio Mixer Strip",
1077            WidgetCategory::ProAudio,
1078            "Channel strip model composed from meter, fader, pan, and plugin controls.",
1079            None,
1080            WidgetComplexity::Composite,
1081            WidgetStateSet::interactive(),
1082            &[WidgetUseCase::AudioProduction],
1083        ),
1084        descriptor(
1085            "media_preview",
1086            "Media Preview",
1087            WidgetCategory::Media,
1088            "Preview surface for images, video frames, waveforms, or shader thumbnails.",
1089            None,
1090            WidgetComplexity::Surface,
1091            WidgetStateSet::async_surface(),
1092            &[WidgetUseCase::CreativeTools],
1093        ),
1094        descriptor(
1095            "app_shell",
1096            "App Shell",
1097            WidgetCategory::Shell,
1098            "High-level shell surface coordinated with reusable layout recipes.",
1099            None,
1100            WidgetComplexity::Workflow,
1101            WidgetStateSet::interactive(),
1102            &[WidgetUseCase::GeneralDesktop, WidgetUseCase::AiWorkspace],
1103        ),
1104        descriptor(
1105            "widget_catalog_browser",
1106            "Widget Catalog Browser",
1107            WidgetCategory::Shell,
1108            "Browsable component inventory with categories, use cases, variants, and examples.",
1109            None,
1110            WidgetComplexity::Workflow,
1111            WidgetStateSet::interactive(),
1112            &[WidgetUseCase::DeveloperTools, WidgetUseCase::Dashboard],
1113        ),
1114        descriptor(
1115            "design_token_palette",
1116            "Design Token Palette",
1117            WidgetCategory::DataDisplay,
1118            "Color, spacing, radius, type, material, motion, and density token preview surface.",
1119            None,
1120            WidgetComplexity::Composite,
1121            WidgetStateSet::passive(),
1122            &[WidgetUseCase::DeveloperTools, WidgetUseCase::CreativeTools],
1123        ),
1124        descriptor(
1125            "component_state_matrix",
1126            "Component State Matrix",
1127            WidgetCategory::DataDisplay,
1128            "Variant, role, size, and interaction-state comparison grid for component QA.",
1129            None,
1130            WidgetComplexity::Composite,
1131            WidgetStateSet::selectable(),
1132            &[WidgetUseCase::DeveloperTools, WidgetUseCase::Dashboard],
1133        ),
1134        descriptor(
1135            "theme_gallery",
1136            "Theme Gallery",
1137            WidgetCategory::DataDisplay,
1138            "Multi-theme preview board for dark, light, dense, spacious, and specialized presets.",
1139            None,
1140            WidgetComplexity::Composite,
1141            WidgetStateSet::selectable(),
1142            &[WidgetUseCase::DeveloperTools, WidgetUseCase::CreativeTools],
1143        ),
1144        descriptor(
1145            "layout_recipe_preview",
1146            "Layout Recipe Preview",
1147            WidgetCategory::Layout,
1148            "Responsive slot preview for app shells, dashboards, editors, palettes, and mixers.",
1149            None,
1150            WidgetComplexity::Composite,
1151            WidgetStateSet::interactive(),
1152            &[WidgetUseCase::DeveloperTools, WidgetUseCase::GeneralDesktop],
1153        ),
1154        descriptor(
1155            "contrast_audit_panel",
1156            "Contrast Audit Panel",
1157            WidgetCategory::Feedback,
1158            "Accessibility report for text, muted text, focus rings, semantic roles, and tokens.",
1159            None,
1160            WidgetComplexity::Composite,
1161            WidgetStateSet::async_surface(),
1162            &[WidgetUseCase::DeveloperTools, WidgetUseCase::DocumentEditing],
1163        ),
1164        descriptor(
1165            "material_preview_panel",
1166            "Material Preview Panel",
1167            WidgetCategory::Layout,
1168            "Surface preview for filled, tonal, outline, ghost, glass, blur, and elevation choices.",
1169            None,
1170            WidgetComplexity::Surface,
1171            WidgetStateSet::interactive(),
1172            &[WidgetUseCase::DeveloperTools, WidgetUseCase::CreativeTools],
1173        ),
1174        descriptor(
1175            "motion_curve_panel",
1176            "Motion Curve Panel",
1177            WidgetCategory::DataDisplay,
1178            "Duration and easing preview for instant, fast, standard, slow, and expressive motion.",
1179            None,
1180            WidgetComplexity::Composite,
1181            WidgetStateSet::interactive(),
1182            &[WidgetUseCase::DeveloperTools, WidgetUseCase::CreativeTools],
1183        ),
1184        descriptor(
1185            "progress_loader",
1186            "Progress Loader",
1187            WidgetCategory::Feedback,
1188            "Progress-bound loader model for bars, meters, gauges, uploads, downloads, and step flows.",
1189            None,
1190            WidgetComplexity::Stateful,
1191            WidgetStateSet::async_surface(),
1192            &[WidgetUseCase::GeneralDesktop, WidgetUseCase::Dashboard],
1193        ),
1194        descriptor(
1195            "inline_loader",
1196            "Inline Loader",
1197            WidgetCategory::Feedback,
1198            "Compact text-adjacent loading affordance for buttons, rows, commands, and async labels.",
1199            None,
1200            WidgetComplexity::Primitive,
1201            WidgetStateSet::async_surface(),
1202            &[WidgetUseCase::GeneralDesktop, WidgetUseCase::AiWorkspace],
1203        ),
1204        descriptor(
1205            "skeleton_loader",
1206            "Skeleton Loader",
1207            WidgetCategory::Feedback,
1208            "Placeholder surface family for pending cards, tables, documents, and assistant output.",
1209            None,
1210            WidgetComplexity::Composite,
1211            WidgetStateSet::async_surface(),
1212            &[WidgetUseCase::GeneralDesktop, WidgetUseCase::Dashboard],
1213        ),
1214        descriptor(
1215            "scanner_loader",
1216            "Scanner Loader",
1217            WidgetCategory::Feedback,
1218            "Sweeping scanner activity model for search, indexing, diagnostics, and health checks.",
1219            None,
1220            WidgetComplexity::Stateful,
1221            WidgetStateSet::async_surface(),
1222            &[WidgetUseCase::DeveloperTools, WidgetUseCase::AiWorkspace],
1223        ),
1224        descriptor(
1225            "matrix_loader",
1226            "Matrix Loader",
1227            WidgetCategory::Media,
1228            "Effect-style activity surface for dense character rain and terminal-inspired panels.",
1229            None,
1230            WidgetComplexity::Surface,
1231            WidgetStateSet::async_surface(),
1232            &[WidgetUseCase::DeveloperTools, WidgetUseCase::CreativeTools],
1233        ),
1234        descriptor(
1235            "laser_etch_loader",
1236            "Laser Etch Loader",
1237            WidgetCategory::Media,
1238            "High-energy reveal treatment for generated code, previews, badges, and command results.",
1239            None,
1240            WidgetComplexity::Surface,
1241            WidgetStateSet::async_surface(),
1242            &[WidgetUseCase::AiWorkspace, WidgetUseCase::CreativeTools],
1243        ),
1244        descriptor(
1245            "rings_loader",
1246            "Rings Loader",
1247            WidgetCategory::Feedback,
1248            "Radial multi-ring activity model for syncing, scanning, orbiting, and processing states.",
1249            None,
1250            WidgetComplexity::Surface,
1251            WidgetStateSet::async_surface(),
1252            &[WidgetUseCase::GeneralDesktop, WidgetUseCase::AudioProduction],
1253        ),
1254        descriptor(
1255            "vortex_loader",
1256            "Vortex Loader",
1257            WidgetCategory::Media,
1258            "Centered swirl activity surface for expensive processing, rendering, and synthesis work.",
1259            None,
1260            WidgetComplexity::Surface,
1261            WidgetStateSet::async_surface(),
1262            &[WidgetUseCase::CreativeTools, WidgetUseCase::Dashboard],
1263        ),
1264        descriptor(
1265            "grid_pulse_loader",
1266            "Grid Pulse Loader",
1267            WidgetCategory::DataDisplay,
1268            "Cell-grid pulse treatment for dashboards, agent swarms, cluster status, and batch jobs.",
1269            None,
1270            WidgetComplexity::Composite,
1271            WidgetStateSet::async_surface(),
1272            &[WidgetUseCase::Dashboard, WidgetUseCase::AiWorkspace],
1273        ),
1274        descriptor(
1275            "glow_surface",
1276            "Glow Surface",
1277            WidgetCategory::Layout,
1278            "Container model that resolves focus, hover, selected, disabled, and pulsing glow effects.",
1279            None,
1280            WidgetComplexity::Surface,
1281            WidgetStateSet::interactive(),
1282            &[WidgetUseCase::GeneralDesktop, WidgetUseCase::CreativeTools],
1283        ),
1284        descriptor(
1285            "focus_glow_ring",
1286            "Focus Glow Ring",
1287            WidgetCategory::Feedback,
1288            "Accessible focus treatment with renderer-neutral glow, elevation, and focus-ring values.",
1289            None,
1290            WidgetComplexity::Primitive,
1291            WidgetStateSet::interactive(),
1292            &[WidgetUseCase::GeneralDesktop, WidgetUseCase::DeveloperTools],
1293        ),
1294        descriptor(
1295            "status_beacon",
1296            "Status Beacon",
1297            WidgetCategory::Feedback,
1298            "Small pulsing status indicator for live jobs, connection state, and background agents.",
1299            None,
1300            WidgetComplexity::Primitive,
1301            WidgetStateSet::async_surface(),
1302            &[WidgetUseCase::GeneralDesktop, WidgetUseCase::AiWorkspace],
1303        ),
1304        descriptor(
1305            "shimmer_panel",
1306            "Shimmer Panel",
1307            WidgetCategory::Feedback,
1308            "Large async placeholder with moving sheen, muted content slots, and reduced-motion fallback.",
1309            None,
1310            WidgetComplexity::Surface,
1311            WidgetStateSet::async_surface(),
1312            &[WidgetUseCase::GeneralDesktop, WidgetUseCase::DocumentEditing],
1313        ),
1314        descriptor(
1315            "activity_beams",
1316            "Activity Beams",
1317            WidgetCategory::Media,
1318            "Animated beam surface for hero panels, empty states, and background processing contexts.",
1319            None,
1320            WidgetComplexity::Surface,
1321            WidgetStateSet::async_surface(),
1322            &[WidgetUseCase::CreativeTools, WidgetUseCase::AiWorkspace],
1323        ),
1324        descriptor(
1325            "effect_timeline",
1326            "Effect Timeline",
1327            WidgetCategory::Media,
1328            "Composable timeline model for previewing reveal, wipe, sweep, smoke, and beam treatments.",
1329            None,
1330            WidgetComplexity::Composite,
1331            WidgetStateSet::interactive(),
1332            &[WidgetUseCase::CreativeTools, WidgetUseCase::DeveloperTools],
1333        ),
1334        descriptor(
1335            "shader_effect_stack",
1336            "Shader Effect Stack",
1337            WidgetCategory::Media,
1338            "Workflow surface for layering glow, aura, caustic, ripple, and laser shader treatments.",
1339            None,
1340            WidgetComplexity::Workflow,
1341            WidgetStateSet::async_surface(),
1342            &[WidgetUseCase::CreativeTools, WidgetUseCase::DeveloperTools],
1343        ),
1344        descriptor(
1345            "studio_widget_gallery",
1346            "Studio Widget Gallery",
1347            WidgetCategory::Shell,
1348            "Searchable widget showcase model with hover glow, selection, tags, groups, and preview overlays.",
1349            None,
1350            WidgetComplexity::Workflow,
1351            WidgetStateSet::interactive(),
1352            &[WidgetUseCase::DeveloperTools, WidgetUseCase::CreativeTools],
1353        ),
1354        descriptor(
1355            "widget_search_field",
1356            "Widget Search Field",
1357            WidgetCategory::Input,
1358            "Bounded search surface for filtering widget galleries by title, group, description, and tags.",
1359            Some(WidgetControlKind::TextInput),
1360            WidgetComplexity::Stateful,
1361            WidgetStateSet::editable(),
1362            &[WidgetUseCase::DeveloperTools, WidgetUseCase::GeneralDesktop],
1363        ),
1364        descriptor(
1365            "hover_preview_overlay",
1366            "Hover Preview Overlay",
1367            WidgetCategory::Overlay,
1368            "Pointer-driven preview layer for widget details, shader skins, shortcuts, and interaction notes.",
1369            None,
1370            WidgetComplexity::Surface,
1371            WidgetStateSet::interactive(),
1372            &[WidgetUseCase::DeveloperTools, WidgetUseCase::CreativeTools],
1373        ),
1374        descriptor(
1375            "cupertino_window_chrome",
1376            "Cupertino Window Chrome",
1377            WidgetCategory::Shell,
1378            "macOS-style titlebar model with traffic lights, vibrancy, toolbar items, and focused window states.",
1379            None,
1380            WidgetComplexity::Surface,
1381            WidgetStateSet::interactive(),
1382            &[WidgetUseCase::GeneralDesktop, WidgetUseCase::CreativeTools],
1383        ),
1384        descriptor(
1385            "cupertino_toolbar",
1386            "Cupertino Toolbar",
1387            WidgetCategory::Navigation,
1388            "macOS toolbar composition for navigation, search, segmented controls, status, and action items.",
1389            None,
1390            WidgetComplexity::Composite,
1391            WidgetStateSet::interactive(),
1392            &[WidgetUseCase::GeneralDesktop, WidgetUseCase::DeveloperTools],
1393        ),
1394        descriptor(
1395            "cupertino_source_list",
1396            "Cupertino Source List",
1397            WidgetCategory::Navigation,
1398            "Sidebar source-list model with symbols, badges, selection, hover, and bounded filtering.",
1399            None,
1400            WidgetComplexity::Composite,
1401            WidgetStateSet::selectable(),
1402            &[WidgetUseCase::GeneralDesktop, WidgetUseCase::DocumentEditing],
1403        ),
1404        descriptor(
1405            "cupertino_segmented_control",
1406            "Cupertino Segmented Control",
1407            WidgetCategory::Selection,
1408            "macOS-style segmented control model with disabled-item skipping and renderer-neutral slots.",
1409            None,
1410            WidgetComplexity::Stateful,
1411            WidgetStateSet::selectable(),
1412            &[WidgetUseCase::GeneralDesktop, WidgetUseCase::CreativeTools],
1413        ),
1414        descriptor(
1415            "cupertino_inspector_panel",
1416            "Cupertino Inspector Panel",
1417            WidgetCategory::Overlay,
1418            "Right-side translucent inspector surface for metadata, controls, popovers, and design audits.",
1419            None,
1420            WidgetComplexity::Surface,
1421            WidgetStateSet::interactive(),
1422            &[WidgetUseCase::CreativeTools, WidgetUseCase::DeveloperTools],
1423        ),
1424        descriptor(
1425            "cupertino_sheet",
1426            "Cupertino Sheet",
1427            WidgetCategory::Overlay,
1428            "Window-scoped sheet presentation model with detents, vibrancy, modal scrims, and action content.",
1429            None,
1430            WidgetComplexity::Surface,
1431            WidgetStateSet::interactive(),
1432            &[WidgetUseCase::GeneralDesktop, WidgetUseCase::DocumentEditing],
1433        ),
1434        descriptor(
1435            "cupertino_popover",
1436            "Cupertino Popover",
1437            WidgetCategory::Overlay,
1438            "Anchored popover and menu presentation model with hover state, arrows, and compact content surfaces.",
1439            None,
1440            WidgetComplexity::Surface,
1441            WidgetStateSet::interactive(),
1442            &[WidgetUseCase::GeneralDesktop, WidgetUseCase::DeveloperTools],
1443        ),
1444        descriptor(
1445            "shader_preview_panel",
1446            "Shader Preview Panel",
1447            WidgetCategory::Media,
1448            "Renderer-owned shader thumbnail surface for backgrounds, panes, prompts, and effects.",
1449            None,
1450            WidgetComplexity::Surface,
1451            WidgetStateSet::async_surface(),
1452            &[WidgetUseCase::CreativeTools, WidgetUseCase::DeveloperTools],
1453        ),
1454    ]
1455}
1456
1457#[allow(clippy::too_many_arguments)]
1458fn descriptor(
1459    id: &str,
1460    name: &str,
1461    category: WidgetCategory,
1462    summary: &str,
1463    control_kind: Option<WidgetControlKind>,
1464    complexity: WidgetComplexity,
1465    states: WidgetStateSet,
1466    use_cases: &[WidgetUseCase],
1467) -> WidgetDescriptor {
1468    let variants = match category {
1469        WidgetCategory::Layout | WidgetCategory::Shell | WidgetCategory::Overlay => {
1470            vec![
1471                WidgetVariant::Filled,
1472                WidgetVariant::Tonal,
1473                WidgetVariant::Glass,
1474            ]
1475        }
1476        WidgetCategory::ProAudio => vec![
1477            WidgetVariant::Tonal,
1478            WidgetVariant::Outline,
1479            WidgetVariant::Glass,
1480        ],
1481        WidgetCategory::DataDisplay | WidgetCategory::Code | WidgetCategory::Ai => {
1482            vec![
1483                WidgetVariant::Tonal,
1484                WidgetVariant::Outline,
1485                WidgetVariant::Ghost,
1486            ]
1487        }
1488        _ => vec![
1489            WidgetVariant::Filled,
1490            WidgetVariant::Tonal,
1491            WidgetVariant::Outline,
1492            WidgetVariant::Ghost,
1493        ],
1494    };
1495    let roles = match category {
1496        WidgetCategory::ProAudio => vec![
1497            ComponentRole::Neutral,
1498            ComponentRole::Success,
1499            ComponentRole::Warning,
1500            ComponentRole::Danger,
1501        ],
1502        WidgetCategory::Feedback => vec![
1503            ComponentRole::Success,
1504            ComponentRole::Warning,
1505            ComponentRole::Danger,
1506        ],
1507        _ => vec![
1508            ComponentRole::Neutral,
1509            ComponentRole::Primary,
1510            ComponentRole::Accent,
1511        ],
1512    };
1513
1514    WidgetDescriptor {
1515        id: id.to_string(),
1516        name: name.to_string(),
1517        category,
1518        summary: summary.to_string(),
1519        control_kind,
1520        complexity,
1521        variants,
1522        states,
1523        sizes: vec![ComponentSize::Small, ComponentSize::Regular, ComponentSize::Large],
1524        roles,
1525        use_cases: if use_cases.is_empty() {
1526            vec![WidgetUseCase::GeneralDesktop]
1527        } else {
1528            use_cases.to_vec()
1529        },
1530        examples: descriptor_examples(id, states),
1531        token_refs: vec!["colors".to_string(), "spacing".to_string(), "radius".to_string(), "motion".to_string()],
1532        renderer_notes: "Renderer owns drawing, hit dispatch, font shaping, and GPU resources; rae owns reusable model metadata.".to_string(),
1533    }
1534}
1535
1536fn descriptor_examples(id: &str, states: WidgetStateSet) -> Vec<WidgetExample> {
1537    let mut examples = vec![WidgetExample::new(
1538        format!("{id}.base"),
1539        "Base",
1540        "Default renderer-neutral model state.",
1541    )];
1542    if states.hovered {
1543        examples.push(
1544            WidgetExample::new(format!("{id}.hover"), "Hover", "Pointer hover affordance.")
1545                .with_state(WidgetState::new().hovered(true))
1546                .with_variant(WidgetVariant::Tonal),
1547        );
1548    }
1549    if states.focused {
1550        examples.push(
1551            WidgetExample::new(format!("{id}.focus"), "Focus", "Keyboard focus-ring state.")
1552                .with_state(WidgetState::new().focused(true))
1553                .with_role(ComponentRole::Info),
1554        );
1555    }
1556    if states.selected || states.checked {
1557        examples.push(
1558            WidgetExample::new(
1559                format!("{id}.selected"),
1560                "Selected",
1561                "Selected or checked state.",
1562            )
1563            .with_state(WidgetState::new().selected(true))
1564            .with_variant(WidgetVariant::Filled)
1565            .with_role(ComponentRole::Accent),
1566        );
1567    }
1568    if states.loading {
1569        examples.push(
1570            WidgetExample::new(
1571                format!("{id}.loading"),
1572                "Loading",
1573                "Async loading or processing state.",
1574            )
1575            .with_state(WidgetState::new().selected(true))
1576            .with_variant(WidgetVariant::Glass)
1577            .with_role(ComponentRole::Info),
1578        );
1579    }
1580    if states.error {
1581        examples.push(
1582            WidgetExample::new(
1583                format!("{id}.error"),
1584                "Error",
1585                "Validation or failed async state.",
1586            )
1587            .with_state(WidgetState::new().focused(true))
1588            .with_role(ComponentRole::Danger),
1589        );
1590    }
1591    examples
1592}
1593
1594fn sanitize_catalog_id(input: &str, fallback: &str) -> String {
1595    let id = sanitize_str(input, 96).trim().replace(' ', "_");
1596    if id.is_empty() {
1597        fallback.to_string()
1598    } else {
1599        id
1600    }
1601}
1602
1603fn sanitize_query_text(input: &str) -> String {
1604    sanitize_str(input, 160).trim().to_ascii_lowercase()
1605}
1606
1607#[cfg(test)]
1608mod tests {
1609    use super::*;
1610
1611    #[test]
1612    fn built_in_catalog_is_complete_and_valid() {
1613        let catalog = built_in_widget_catalog();
1614
1615        assert!(catalog.validate().is_ok());
1616        assert!(catalog.widgets.len() >= 30);
1617        assert!(catalog.token_presets.len() >= 8);
1618        assert!(catalog.recipe_kinds.len() >= 10);
1619        assert!(catalog.by_category(WidgetCategory::ProAudio).len() >= 8);
1620        assert!(catalog.by_use_case(WidgetUseCase::AudioProduction).len() >= 8);
1621        assert!(catalog.find("prompt").is_some());
1622        assert!(catalog.find("progress_loader").is_some());
1623        assert!(catalog.find("glow_surface").is_some());
1624        assert!(catalog.find("studio_widget_gallery").is_some());
1625        assert!(catalog.find("cupertino_window_chrome").is_some());
1626        assert!(catalog.token_preset("audio_console_dark").is_some());
1627    }
1628
1629    #[test]
1630    fn catalog_summary_counts_categories_use_cases_and_complexity() {
1631        let catalog = built_in_widget_catalog();
1632        let summary = catalog.summary();
1633
1634        assert_eq!(summary.widget_count, catalog.widgets.len());
1635        assert_eq!(summary.token_preset_count, catalog.token_presets.len());
1636        assert_eq!(summary.recipe_kind_count, catalog.recipe_kinds.len());
1637        assert!(summary.example_count >= summary.widget_count);
1638        assert_eq!(summary.category_counts.len(), 13);
1639        assert_eq!(summary.use_case_counts.len(), 8);
1640        assert_eq!(summary.complexity_counts.len(), 5);
1641        assert!(summary
1642            .category_counts
1643            .iter()
1644            .any(|count| count.category == WidgetCategory::Code && count.count >= 7));
1645        assert!(summary
1646            .category_counts
1647            .iter()
1648            .any(|count| count.category == WidgetCategory::Feedback && count.count >= 8));
1649        assert!(summary
1650            .use_case_counts
1651            .iter()
1652            .any(|count| count.use_case == WidgetUseCase::DeveloperTools && count.count >= 10));
1653        assert_eq!(WidgetComplexity::Workflow.label(), "workflow");
1654    }
1655
1656    #[test]
1657    fn widget_ids_are_unique_and_descriptors_have_examples() {
1658        let catalog = built_in_widget_catalog();
1659        let mut ids = HashSet::new();
1660
1661        for widget in catalog.widgets {
1662            assert!(ids.insert(widget.id.clone()), "{}", widget.id);
1663            assert!(!widget.examples.is_empty(), "{}", widget.id);
1664            assert!(widget.states.state_count() >= 1, "{}", widget.id);
1665            assert!(!widget.variants.is_empty(), "{}", widget.id);
1666            assert!(!widget.sizes.is_empty(), "{}", widget.id);
1667            assert!(!widget.roles.is_empty(), "{}", widget.id);
1668        }
1669    }
1670
1671    #[test]
1672    fn validation_reports_duplicate_widget_ids() {
1673        let mut catalog = built_in_widget_catalog();
1674        let duplicate = catalog.widgets[0].clone();
1675        catalog.widgets.push(duplicate);
1676
1677        let error = catalog.validate().unwrap_err();
1678
1679        assert!(error.message.contains("duplicate widget id"));
1680    }
1681
1682    #[test]
1683    fn catalog_search_uses_sanitized_ids() {
1684        let catalog = built_in_widget_catalog();
1685
1686        assert_eq!(
1687            catalog
1688                .find("prompt\u{200f}")
1689                .map(|widget| widget.id.as_str()),
1690            Some("prompt")
1691        );
1692        let code_widgets = catalog.by_category(WidgetCategory::Code);
1693        assert!(code_widgets.len() >= 7);
1694        assert!(catalog.find("diagnostics_panel").is_some());
1695        assert!(catalog.find("inline_completion").is_some());
1696        assert!(catalog.find("widget_catalog_browser").is_some());
1697        assert!(catalog.find("component_state_matrix").is_some());
1698        assert!(catalog.find("contrast_audit_panel").is_some());
1699        assert!(catalog.find("matrix_loader").is_some());
1700        assert!(catalog.find("shader_effect_stack").is_some());
1701        assert!(catalog.find("hover_preview_overlay").is_some());
1702        assert!(catalog.find("widget_search_field").is_some());
1703        assert!(catalog.find("cupertino_source_list").is_some());
1704        assert!(catalog.find("cupertino_segmented_control").is_some());
1705        assert!(catalog.find("cupertino_sheet").is_some());
1706        assert!(catalog.find("cupertino_popover").is_some());
1707    }
1708
1709    #[test]
1710    fn catalog_query_filters_text_and_metadata_with_limits() {
1711        let catalog = built_in_widget_catalog();
1712
1713        let ids = catalog.query_ids(
1714            WidgetCatalogQuery::new()
1715                .text("audio meter")
1716                .use_case(WidgetUseCase::AudioProduction)
1717                .category(WidgetCategory::ProAudio)
1718                .limit(4),
1719        );
1720
1721        assert!(ids.contains(&"audio_meter"));
1722        assert!(ids.len() <= 4);
1723        assert!(ids.iter().all(|id| id.starts_with("audio_")));
1724    }
1725
1726    #[test]
1727    fn catalog_query_sanitizes_public_text_and_zero_limits() {
1728        let catalog = built_in_widget_catalog();
1729
1730        let ids = catalog.query_ids(WidgetCatalogQuery {
1731            text: "Prompt\u{200f}\x1b[31m focus".to_string(),
1732            category: Some(WidgetCategory::Ai),
1733            limit: 0,
1734            ..WidgetCatalogQuery::default()
1735        });
1736
1737        assert_eq!(ids, ["prompt"]);
1738    }
1739
1740    #[cfg(feature = "serde")]
1741    #[test]
1742    fn widget_catalog_round_trips_through_serde() {
1743        let catalog = built_in_widget_catalog();
1744
1745        let encoded = serde_json::to_string(&catalog).unwrap();
1746        let decoded: WidgetCatalog = serde_json::from_str(&encoded).unwrap();
1747
1748        assert_eq!(decoded.widgets.len(), catalog.widgets.len());
1749        assert!(decoded.validate().is_ok());
1750    }
1751
1752    #[cfg(feature = "serde")]
1753    #[test]
1754    fn widget_catalog_summary_round_trips_through_serde() {
1755        let summary = built_in_widget_catalog().summary();
1756
1757        let encoded = serde_json::to_string(&summary).unwrap();
1758        let decoded: WidgetCatalogSummary = serde_json::from_str(&encoded).unwrap();
1759
1760        assert_eq!(decoded, summary);
1761    }
1762
1763    #[cfg(feature = "serde")]
1764    #[test]
1765    fn widget_catalog_queries_round_trip_through_serde() {
1766        let query = WidgetCatalogQuery::new()
1767            .text("audio")
1768            .category(WidgetCategory::ProAudio)
1769            .use_case(WidgetUseCase::AudioProduction)
1770            .variant(WidgetVariant::Glass)
1771            .limit(3);
1772
1773        let encoded = serde_json::to_string(&query).unwrap();
1774        let decoded: WidgetCatalogQuery = serde_json::from_str(&encoded).unwrap();
1775        let decoded = decoded.sanitized();
1776
1777        assert_eq!(decoded.limit, 3);
1778        assert_eq!(decoded.category, Some(WidgetCategory::ProAudio));
1779    }
1780}