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}