cronixui 1.0.6 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/README.md +20 -5
  2. package/package.json +20 -5
  3. package/packages/flutter/lib/cronixui.dart +41 -0
  4. package/packages/flutter/lib/src/tokens/colors.dart +34 -0
  5. package/packages/flutter/lib/src/tokens/spacing.dart +54 -0
  6. package/packages/flutter/lib/src/tokens/theme.dart +174 -0
  7. package/packages/flutter/lib/src/widgets/cn_accordion.dart +254 -0
  8. package/packages/flutter/lib/src/widgets/cn_alert.dart +137 -0
  9. package/packages/flutter/lib/src/widgets/cn_avatar.dart +98 -0
  10. package/packages/flutter/lib/src/widgets/cn_badge.dart +80 -0
  11. package/packages/flutter/lib/src/widgets/cn_breadcrumb.dart +88 -0
  12. package/packages/flutter/lib/src/widgets/cn_button.dart +137 -0
  13. package/packages/flutter/lib/src/widgets/cn_card.dart +99 -0
  14. package/packages/flutter/lib/src/widgets/cn_checkbox.dart +77 -0
  15. package/packages/flutter/lib/src/widgets/cn_command_palette.dart +299 -0
  16. package/packages/flutter/lib/src/widgets/cn_container.dart +131 -0
  17. package/packages/flutter/lib/src/widgets/cn_dropdown.dart +149 -0
  18. package/packages/flutter/lib/src/widgets/cn_file_input.dart +113 -0
  19. package/packages/flutter/lib/src/widgets/cn_footer.dart +108 -0
  20. package/packages/flutter/lib/src/widgets/cn_header.dart +173 -0
  21. package/packages/flutter/lib/src/widgets/cn_input.dart +142 -0
  22. package/packages/flutter/lib/src/widgets/cn_list.dart +150 -0
  23. package/packages/flutter/lib/src/widgets/cn_modal.dart +213 -0
  24. package/packages/flutter/lib/src/widgets/cn_nav.dart +157 -0
  25. package/packages/flutter/lib/src/widgets/cn_pagination.dart +193 -0
  26. package/packages/flutter/lib/src/widgets/cn_progress.dart +146 -0
  27. package/packages/flutter/lib/src/widgets/cn_radio.dart +133 -0
  28. package/packages/flutter/lib/src/widgets/cn_search.dart +183 -0
  29. package/packages/flutter/lib/src/widgets/cn_select.dart +244 -0
  30. package/packages/flutter/lib/src/widgets/cn_sidebar.dart +207 -0
  31. package/packages/flutter/lib/src/widgets/cn_skeleton.dart +136 -0
  32. package/packages/flutter/lib/src/widgets/cn_slider.dart +141 -0
  33. package/packages/flutter/lib/src/widgets/cn_spinner.dart +85 -0
  34. package/packages/flutter/lib/src/widgets/cn_stat.dart +135 -0
  35. package/packages/flutter/lib/src/widgets/cn_table.dart +136 -0
  36. package/packages/flutter/lib/src/widgets/cn_tabs.dart +229 -0
  37. package/packages/flutter/lib/src/widgets/cn_tag.dart +185 -0
  38. package/packages/flutter/lib/src/widgets/cn_textarea.dart +143 -0
  39. package/packages/flutter/lib/src/widgets/cn_toast.dart +121 -0
  40. package/packages/flutter/lib/src/widgets/cn_toggle.dart +78 -0
  41. package/packages/flutter/lib/src/widgets/cn_tooltip.dart +118 -0
  42. package/packages/flutter/pubspec.yaml +20 -0
  43. package/packages/go/cronixui/cronixui.go +784 -237
  44. package/packages/go/cronixui/go.mod +32 -0
  45. package/packages/go/cronixui/go.sum +666 -0
  46. package/packages/python/cronixui/__init__.py +59 -3
  47. package/packages/python/cronixui/alert.py +61 -0
  48. package/packages/python/cronixui/avatar.py +50 -0
  49. package/packages/python/cronixui/badge.py +46 -0
  50. package/packages/python/cronixui/button.py +64 -0
  51. package/packages/python/cronixui/card.py +62 -0
  52. package/packages/python/cronixui/form.py +255 -0
  53. package/packages/python/cronixui/layout.py +143 -0
  54. package/packages/python/cronixui/list.py +51 -0
  55. package/packages/python/cronixui/loading.py +36 -0
  56. package/packages/python/cronixui/progress.py +90 -0
  57. package/packages/python/cronixui/table.py +48 -0
  58. package/packages/python/cronixui/tooltip.py +28 -0
  59. package/packages/react/src/components/Accordion.tsx +82 -0
  60. package/packages/react/src/components/Alert.tsx +80 -0
  61. package/packages/react/src/components/Avatar.tsx +54 -0
  62. package/packages/react/src/components/Badge.tsx +32 -0
  63. package/packages/react/src/components/Breadcrumb.tsx +50 -0
  64. package/packages/react/src/components/Button.tsx +47 -0
  65. package/packages/react/src/components/Card.tsx +69 -0
  66. package/packages/react/src/components/Checkbox.tsx +30 -0
  67. package/packages/react/src/components/CommandPalette.tsx +131 -0
  68. package/packages/react/src/components/Container.tsx +26 -0
  69. package/packages/react/src/components/Dropdown.tsx +88 -0
  70. package/packages/react/src/components/FileInput.tsx +86 -0
  71. package/packages/react/src/components/Footer.tsx +36 -0
  72. package/packages/react/src/components/FormGroup.tsx +36 -0
  73. package/packages/react/src/components/Header.tsx +29 -0
  74. package/packages/react/src/components/Input.tsx +54 -0
  75. package/packages/react/src/components/List.tsx +55 -0
  76. package/packages/react/src/components/Modal.tsx +89 -0
  77. package/packages/react/src/components/Nav.tsx +63 -0
  78. package/packages/react/src/components/Pagination.tsx +107 -0
  79. package/packages/react/src/components/Progress.tsx +49 -0
  80. package/packages/react/src/components/Radio.tsx +64 -0
  81. package/packages/react/src/components/Search.tsx +95 -0
  82. package/packages/react/src/components/Select.tsx +41 -0
  83. package/packages/react/src/components/Sidebar.tsx +64 -0
  84. package/packages/react/src/components/Skeleton.tsx +39 -0
  85. package/packages/react/src/components/Slider.tsx +32 -0
  86. package/packages/react/src/components/Spinner.tsx +24 -0
  87. package/packages/react/src/components/Stack.tsx +69 -0
  88. package/packages/react/src/components/Stat.tsx +35 -0
  89. package/packages/react/src/components/Table.tsx +90 -0
  90. package/packages/react/src/components/Tabs.tsx +85 -0
  91. package/packages/react/src/components/Tag.tsx +30 -0
  92. package/packages/react/src/components/Textarea.tsx +21 -0
  93. package/packages/react/src/components/Toast.tsx +134 -0
  94. package/packages/react/src/components/Toggle.tsx +58 -0
  95. package/packages/react/src/components/Tooltip.tsx +31 -0
  96. package/packages/react/src/components/Typography.tsx +66 -0
  97. package/packages/react/src/index.ts +40 -0
  98. package/packages/react/src/styles.css +2039 -0
  99. package/packages/react/src/tokens/index.ts +94 -0
  100. package/packages/rust/cronixui/src/colors.rs +135 -0
  101. package/packages/rust/cronixui/src/components/accordion.rs +47 -0
  102. package/packages/rust/cronixui/src/components/alert.rs +95 -0
  103. package/packages/rust/cronixui/src/components/avatar.rs +85 -0
  104. package/packages/rust/cronixui/src/components/badge.rs +35 -0
  105. package/packages/rust/cronixui/src/components/breadcrumb.rs +58 -0
  106. package/packages/rust/cronixui/src/components/button.rs +70 -0
  107. package/packages/rust/cronixui/src/components/card.rs +259 -0
  108. package/packages/rust/cronixui/src/components/command_palette.rs +254 -0
  109. package/packages/rust/cronixui/src/components/dropdown.rs +179 -0
  110. package/packages/rust/cronixui/src/components/file_input.rs +74 -0
  111. package/packages/rust/cronixui/src/components/input.rs +21 -0
  112. package/packages/rust/cronixui/src/components/list.rs +38 -0
  113. package/packages/rust/cronixui/src/components/mod.rs +51 -0
  114. package/packages/rust/cronixui/src/{modal.rs → components/modal.rs} +15 -1
  115. package/packages/rust/cronixui/src/components/nav.rs +19 -0
  116. package/packages/rust/cronixui/src/{pagination.rs → components/pagination.rs} +14 -13
  117. package/packages/rust/cronixui/src/components/progress.rs +50 -0
  118. package/packages/rust/cronixui/src/components/search.rs +185 -0
  119. package/packages/rust/cronixui/src/components/skeleton.rs +63 -0
  120. package/packages/rust/cronixui/src/components/spinner.rs +21 -0
  121. package/packages/rust/cronixui/src/components/table.rs +56 -0
  122. package/packages/rust/cronixui/src/components/tabs.rs +43 -0
  123. package/packages/rust/cronixui/src/components/toast.rs +69 -0
  124. package/packages/rust/cronixui/src/{toggle.rs → components/toggle.rs} +7 -5
  125. package/packages/rust/cronixui/src/components/tooltip.rs +11 -0
  126. package/packages/rust/cronixui/src/lib.rs +111 -64
  127. package/packages/rust/cronixui/src/tokens.rs +97 -127
  128. package/packages/web/src/variables.css +81 -81
  129. package/packages/go/cronixui/tokens.go +0 -129
  130. package/packages/python/cronixui/pyproject.toml +0 -11
  131. package/packages/react/src/components/Accordion.jsx +0 -50
  132. package/packages/react/src/components/Alert.jsx +0 -62
  133. package/packages/react/src/components/Avatar.jsx +0 -34
  134. package/packages/react/src/components/Badge.jsx +0 -15
  135. package/packages/react/src/components/Breadcrumb.jsx +0 -27
  136. package/packages/react/src/components/Button.jsx +0 -21
  137. package/packages/react/src/components/Card.jsx +0 -23
  138. package/packages/react/src/components/Checkbox.jsx +0 -27
  139. package/packages/react/src/components/CommandPalette.jsx +0 -93
  140. package/packages/react/src/components/Dropdown.jsx +0 -48
  141. package/packages/react/src/components/FileInput.jsx +0 -44
  142. package/packages/react/src/components/Input.jsx +0 -22
  143. package/packages/react/src/components/List.jsx +0 -29
  144. package/packages/react/src/components/Modal.jsx +0 -65
  145. package/packages/react/src/components/Nav.jsx +0 -50
  146. package/packages/react/src/components/Pagination.jsx +0 -81
  147. package/packages/react/src/components/Progress.jsx +0 -23
  148. package/packages/react/src/components/Radio.jsx +0 -50
  149. package/packages/react/src/components/Search.jsx +0 -70
  150. package/packages/react/src/components/Select.jsx +0 -33
  151. package/packages/react/src/components/Skeleton.jsx +0 -15
  152. package/packages/react/src/components/Slider.jsx +0 -29
  153. package/packages/react/src/components/Spinner.jsx +0 -5
  154. package/packages/react/src/components/Stat.jsx +0 -19
  155. package/packages/react/src/components/Table.jsx +0 -48
  156. package/packages/react/src/components/Tabs.jsx +0 -65
  157. package/packages/react/src/components/Tag.jsx +0 -19
  158. package/packages/react/src/components/Textarea.jsx +0 -17
  159. package/packages/react/src/components/Toast.jsx +0 -78
  160. package/packages/react/src/components/Toggle.jsx +0 -34
  161. package/packages/react/src/components/Tooltip.jsx +0 -12
  162. package/packages/react/src/index.d.ts +0 -103
  163. package/packages/react/src/index.js +0 -33
  164. package/packages/rust/cronixui/src/accordion.rs +0 -49
  165. package/packages/rust/cronixui/src/command_palette.rs +0 -62
  166. package/packages/rust/cronixui/src/dropdown.rs +0 -31
  167. package/packages/rust/cronixui/src/search.rs +0 -49
  168. package/packages/rust/cronixui/src/tabs.rs +0 -23
  169. package/packages/rust/cronixui/src/toast.rs +0 -70
@@ -0,0 +1,259 @@
1
+ //! Card component
2
+
3
+ use egui::*;
4
+ use crate::{colors::*, tokens::*};
5
+
6
+ /// Icon component for Card header
7
+ pub struct CardIcon {
8
+ pub icon: String,
9
+ pub size: f32,
10
+ }
11
+
12
+ impl CardIcon {
13
+ pub fn new(icon: impl Into<String>) -> Self {
14
+ Self { icon: icon.into(), size: tokens::FONT_SIZE_LG }
15
+ }
16
+
17
+ pub fn size(mut self, size: f32) -> Self {
18
+ self.size = size;
19
+ self
20
+ }
21
+
22
+ pub fn show(&self, ui: &mut Ui) {
23
+ let colors = Colors::default();
24
+ ui.label(
25
+ RichText::new(&self.icon)
26
+ .size(self.size)
27
+ .color(colors.text_muted),
28
+ );
29
+ }
30
+ }
31
+
32
+ /// Title component
33
+ pub struct CardTitle {
34
+ pub text: String,
35
+ pub size: f32,
36
+ }
37
+
38
+ impl CardTitle {
39
+ pub fn new(text: impl Into<String>) -> Self {
40
+ Self { text: text.into(), size: tokens::FONT_SIZE_MD }
41
+ }
42
+
43
+ pub fn size(mut self, size: f32) -> Self {
44
+ self.size = size;
45
+ self
46
+ }
47
+
48
+ pub fn show(&self, ui: &mut Ui) {
49
+ let colors = Colors::default();
50
+ ui.label(
51
+ RichText::new(&self.text)
52
+ .size(self.size)
53
+ .color(colors.text)
54
+ .strong(),
55
+ );
56
+ }
57
+ }
58
+
59
+ /// Subtitle component
60
+ pub struct CardSubtitle {
61
+ pub text: String,
62
+ pub size: f32,
63
+ }
64
+
65
+ impl CardSubtitle {
66
+ pub fn new(text: impl Into<String>) -> Self {
67
+ Self { text: text.into(), size: tokens::FONT_SIZE_SM }
68
+ }
69
+
70
+ pub fn size(mut self, size: f32) -> Self {
71
+ self.size = size;
72
+ self
73
+ }
74
+
75
+ pub fn show(&self, ui: &mut Ui) {
76
+ let colors = Colors::default();
77
+ ui.label(
78
+ RichText::new(&self.text)
79
+ .size(self.size)
80
+ .color(colors.text_muted),
81
+ );
82
+ }
83
+ }
84
+
85
+ /// Card header component (combines icon, title, subtitle)
86
+ pub struct CardHeader {
87
+ pub icon: Option<CardIcon>,
88
+ pub title: Option<CardTitle>,
89
+ pub subtitle: Option<CardSubtitle>,
90
+ }
91
+
92
+ impl CardHeader {
93
+ pub fn new() -> Self {
94
+ Self { icon: None, title: None, subtitle: None }
95
+ }
96
+
97
+ pub fn icon(mut self, icon: CardIcon) -> Self {
98
+ self.icon = Some(icon);
99
+ self
100
+ }
101
+
102
+ pub fn title(mut self, title: CardTitle) -> Self {
103
+ self.title = Some(title);
104
+ self
105
+ }
106
+
107
+ pub fn subtitle(mut self, subtitle: CardSubtitle) -> Self {
108
+ self.subtitle = Some(subtitle);
109
+ self
110
+ }
111
+
112
+ pub fn show(&self, ui: &mut Ui) {
113
+ ui.horizontal(|ui| {
114
+ if let Some(icon) = &self.icon {
115
+ icon.show(ui);
116
+ ui.add_space(tokens::SPACE_2);
117
+ }
118
+
119
+ ui.vertical(|ui| {
120
+ if let Some(title) = &self.title {
121
+ title.show(ui);
122
+ }
123
+ if let Some(subtitle) = &self.subtitle {
124
+ subtitle.show(ui);
125
+ }
126
+ });
127
+ });
128
+ }
129
+ }
130
+
131
+ impl Default for CardHeader {
132
+ fn default() -> Self {
133
+ Self::new()
134
+ }
135
+ }
136
+
137
+ /// Card body component
138
+ pub struct CardBody;
139
+
140
+ impl CardBody {
141
+ pub fn new() -> Self {
142
+ Self
143
+ }
144
+
145
+ pub fn show<R>(&self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) {
146
+ add_contents(ui);
147
+ }
148
+ }
149
+
150
+ impl Default for CardBody {
151
+ fn default() -> Self {
152
+ Self::new()
153
+ }
154
+ }
155
+
156
+ /// Card footer component
157
+ pub struct CardFooter {
158
+ pub separator: bool,
159
+ }
160
+
161
+ impl CardFooter {
162
+ pub fn new() -> Self {
163
+ Self { separator: true }
164
+ }
165
+
166
+ pub fn separator(mut self, separator: bool) -> Self {
167
+ self.separator = separator;
168
+ self
169
+ }
170
+
171
+ pub fn show<R>(&self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) {
172
+ if self.separator {
173
+ ui.separator();
174
+ ui.add_space(tokens::SPACE_2);
175
+ }
176
+ add_contents(ui);
177
+ }
178
+ }
179
+
180
+ impl Default for CardFooter {
181
+ fn default() -> Self {
182
+ Self::new()
183
+ }
184
+ }
185
+
186
+ /// Main Card component with builder pattern
187
+ pub struct Card {
188
+ pub header: Option<CardHeader>,
189
+ pub clickable: bool,
190
+ }
191
+
192
+ impl Card {
193
+ pub fn new() -> Self {
194
+ Self { header: None, clickable: false }
195
+ }
196
+
197
+ pub fn title(mut self, title: impl Into<String>) -> Self {
198
+ let title_comp = CardTitle::new(title);
199
+ self.header = Some(
200
+ self.header.unwrap_or_else(CardHeader::new).title(title_comp),
201
+ );
202
+ self
203
+ }
204
+
205
+ pub fn subtitle(mut self, subtitle: impl Into<String>) -> Self {
206
+ let subtitle_comp = CardSubtitle::new(subtitle);
207
+ self.header = Some(
208
+ self.header.unwrap_or_else(CardHeader::new).subtitle(subtitle_comp),
209
+ );
210
+ self
211
+ }
212
+
213
+ pub fn icon(mut self, icon: impl Into<String>) -> Self {
214
+ let icon_comp = CardIcon::new(icon);
215
+ self.header = Some(
216
+ self.header.unwrap_or_else(CardHeader::new).icon(icon_comp),
217
+ );
218
+ self
219
+ }
220
+
221
+ pub fn header(mut self, header: CardHeader) -> Self {
222
+ self.header = Some(header);
223
+ self
224
+ }
225
+
226
+ pub fn clickable(mut self, clickable: bool) -> Self {
227
+ self.clickable = clickable;
228
+ self
229
+ }
230
+
231
+ pub fn show<R>(&self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> Response {
232
+ let colors = Colors::default();
233
+
234
+ let frame = egui::Frame::none()
235
+ .fill(colors.surface)
236
+ .stroke(Stroke::new(1.0, colors.border))
237
+ .rounding(Rounding::same(RADIUS_LG))
238
+ .inner_margin(SPACE_5);
239
+
240
+ let inner_response = frame.show(ui, |ui| {
241
+ // Render header if present
242
+ if let Some(header) = &self.header {
243
+ header.show(ui);
244
+ ui.add_space(SPACE_4);
245
+ }
246
+
247
+ // Render body
248
+ add_contents(ui)
249
+ });
250
+
251
+ inner_response.response
252
+ }
253
+ }
254
+
255
+ impl Default for Card {
256
+ fn default() -> Self {
257
+ Self::new()
258
+ }
259
+ }
@@ -0,0 +1,254 @@
1
+ //! Command palette component
2
+
3
+ use egui::*;
4
+ use crate::{colors::*, tokens::*};
5
+
6
+ pub struct CommandItem {
7
+ pub title: String,
8
+ pub subtitle: Option<String>,
9
+ pub kbd: Option<String>,
10
+ }
11
+
12
+ impl CommandItem {
13
+ pub fn new(title: impl Into<String>) -> Self {
14
+ Self { title: title.into(), subtitle: None, kbd: None }
15
+ }
16
+
17
+ pub fn subtitle(mut self, subtitle: impl Into<String>) -> Self {
18
+ self.subtitle = Some(subtitle.into());
19
+ self
20
+ }
21
+
22
+ pub fn kbd(mut self, kbd: impl Into<String>) -> Self {
23
+ self.kbd = Some(kbd.into());
24
+ self
25
+ }
26
+ }
27
+
28
+ pub struct CommandPalette {
29
+ pub items: Vec<CommandItem>,
30
+ pub query: String,
31
+ pub open: bool,
32
+ pub selected_index: usize,
33
+ id: Id,
34
+ }
35
+
36
+ impl CommandPalette {
37
+ pub fn new(id: impl Into<Id>) -> Self {
38
+ Self { items: Vec::new(), query: String::new(), open: false, selected_index: 0, id: id.into() }
39
+ }
40
+
41
+ pub fn item(mut self, item: CommandItem) -> Self {
42
+ self.items.push(item);
43
+ self
44
+ }
45
+
46
+ pub fn set_items(&mut self, items: Vec<CommandItem>) {
47
+ self.items = items;
48
+ }
49
+
50
+ pub fn open(&mut self) {
51
+ self.open = true;
52
+ self.query.clear();
53
+ self.selected_index = 0;
54
+ }
55
+
56
+ pub fn close(&mut self) {
57
+ self.open = false;
58
+ self.query.clear();
59
+ self.selected_index = 0;
60
+ }
61
+
62
+ pub fn toggle(&mut self) {
63
+ if self.open {
64
+ self.close();
65
+ } else {
66
+ self.open();
67
+ }
68
+ }
69
+
70
+ pub fn filter(&self) -> Vec<&CommandItem> {
71
+ if self.query.is_empty() {
72
+ return self.items.iter().collect();
73
+ }
74
+ self.items
75
+ .iter()
76
+ .filter(|item| item.title.to_lowercase().contains(&self.query.to_lowercase()))
77
+ .collect()
78
+ }
79
+
80
+ /// Render the command palette as a modal dialog
81
+ /// Returns the index of the selected command (into the filtered list)
82
+ pub fn show(&mut self, ctx: &egui::Context) -> Option<usize> {
83
+ if !self.open {
84
+ return None;
85
+ }
86
+
87
+ let colors = Colors::default();
88
+ let mut result = None;
89
+
90
+ // Modal backdrop
91
+ egui::Area::new(self.id.with("backdrop"))
92
+ .order(Order::Foreground)
93
+ .interactable(true)
94
+ .show(ctx, |ui| {
95
+ let screen_size = ctx.screen_rect().size();
96
+ ui.set_min_size(screen_size);
97
+
98
+ // Semi-transparent backdrop
99
+ let painter = ui.painter_at(ui.max_rect());
100
+ painter.rect_filled(
101
+ ui.max_rect(),
102
+ Rounding::ZERO,
103
+ Color32::from_black_alpha(150),
104
+ );
105
+
106
+ // Command palette panel centered on screen
107
+ let panel_width = 500.0;
108
+ let panel_height = 400.0;
109
+
110
+ egui::Frame::none()
111
+ .fill(colors.surface)
112
+ .stroke(egui::Stroke::new(1.0, colors.border))
113
+ .rounding(Rounding::same(tokens::RADIUS_LG))
114
+ .shadow(egui::Shadow {
115
+ offset: [0.0, 8.0].into(),
116
+ blur: 24.0,
117
+ spread: 0.0,
118
+ color: Color32::from_black_alpha(100),
119
+ })
120
+ .show(ui, |ui| {
121
+ ui.set_min_size(vec2(panel_width, panel_height));
122
+
123
+ // Search input at top
124
+ ui.horizontal(|ui| {
125
+ ui.label("⌘");
126
+ let response = ui.add(
127
+ TextEdit::singleline(&mut self.query)
128
+ .hint_text("Type a command...")
129
+ .desired_width(f32::INFINITY)
130
+ .font(FontId::new(tokens::FONT_SIZE_BASE, FontFamily::Proportional)),
131
+ );
132
+
133
+ if response.has_focus() {
134
+ // Handle keyboard navigation
135
+ if ui.input(|i| i.key_pressed(Key::ArrowDown)) {
136
+ let filtered = self.filter();
137
+ if !filtered.is_empty() {
138
+ self.selected_index = (self.selected_index + 1) % filtered.len();
139
+ }
140
+ }
141
+ if ui.input(|i| i.key_pressed(Key::ArrowUp)) {
142
+ let filtered = self.filter();
143
+ if !filtered.is_empty() {
144
+ self.selected_index = if self.selected_index == 0 {
145
+ filtered.len() - 1
146
+ } else {
147
+ self.selected_index - 1
148
+ };
149
+ }
150
+ }
151
+ if ui.input(|i| i.key_pressed(Key::Enter)) {
152
+ result = Some(self.selected_index);
153
+ }
154
+ if ui.input(|i| i.key_pressed(Key::Escape)) {
155
+ self.close();
156
+ }
157
+ }
158
+ });
159
+
160
+ ui.add_space(SPACE_2);
161
+
162
+ // Separator
163
+ ui.separator();
164
+
165
+ ui.add_space(SPACE_2);
166
+
167
+ // Command list
168
+ let filtered = self.filter();
169
+ if !filtered.is_empty() {
170
+ // Ensure selected_index is valid
171
+ if self.selected_index >= filtered.len() {
172
+ self.selected_index = 0;
173
+ }
174
+
175
+ egui::ScrollArea::vertical()
176
+ .max_height(280.0)
177
+ .show(ui, |ui| {
178
+ for (idx, item) in filtered.iter().enumerate() {
179
+ let is_selected = idx == self.selected_index;
180
+
181
+ let response = ui.horizontal(|ui| {
182
+ if is_selected {
183
+ ui.label(
184
+ RichText::new("→")
185
+ .size(tokens::FONT_SIZE_BASE)
186
+ .color(colors.accent_text),
187
+ );
188
+ } else {
189
+ ui.add_space(SPACE_4);
190
+ }
191
+
192
+ ui.label(
193
+ RichText::new(&item.title)
194
+ .size(tokens::FONT_SIZE_BASE)
195
+ .color(if is_selected { colors.accent_text } else { colors.text }),
196
+ );
197
+
198
+ if let Some(subtitle) = &item.subtitle {
199
+ ui.label(
200
+ RichText::new(subtitle)
201
+ .size(tokens::FONT_SIZE_SM)
202
+ .color(colors.text_muted),
203
+ );
204
+ }
205
+
206
+ if let Some(kbd) = &item.kbd {
207
+ ui.with_layout(egui::Layout::right_to_left(Align::Center), |ui| {
208
+ egui::Frame::none()
209
+ .fill(colors.surface_3)
210
+ .stroke(egui::Stroke::new(1.0, colors.border))
211
+ .rounding(Rounding::same(tokens::RADIUS_SM))
212
+ .inner_margin(vec2(SPACE_2, SPACE_1))
213
+ .show(ui, |ui| {
214
+ ui.label(
215
+ RichText::new(kbd)
216
+ .size(tokens::FONT_SIZE_XS)
217
+ .color(colors.text_muted)
218
+ .font(FontFamily::Monospace),
219
+ );
220
+ });
221
+ });
222
+ }
223
+ });
224
+
225
+ if response.response.clicked() {
226
+ result = Some(idx);
227
+ }
228
+ }
229
+ });
230
+ } else if !self.query.is_empty() {
231
+ ui.centered_and_justified(|ui| {
232
+ ui.label(
233
+ RichText::new("No commands found")
234
+ .size(tokens::FONT_SIZE_SM)
235
+ .color(colors.text_muted),
236
+ );
237
+ });
238
+ }
239
+ });
240
+ });
241
+
242
+ if result.is_some() {
243
+ self.close();
244
+ }
245
+
246
+ result
247
+ }
248
+ }
249
+
250
+ impl Default for CommandPalette {
251
+ fn default() -> Self {
252
+ Self::new("default_command_palette")
253
+ }
254
+ }
@@ -0,0 +1,179 @@
1
+ //! Dropdown component
2
+
3
+ use egui::*;
4
+ use crate::{colors::*, tokens::*};
5
+
6
+ /// Dropdown component with full rendering
7
+ pub struct Dropdown {
8
+ pub items: Vec<String>,
9
+ pub selected: Option<usize>,
10
+ pub open: bool,
11
+ id: Id,
12
+ }
13
+
14
+ impl Dropdown {
15
+ pub fn new(id: impl Into<Id>, items: impl IntoIterator<Item = impl Into<String>>) -> Self {
16
+ Self {
17
+ id: id.into(),
18
+ items: items.into_iter().map(|s| s.into()).collect(),
19
+ selected: None,
20
+ open: false,
21
+ }
22
+ }
23
+
24
+ pub fn selected(mut self, index: usize) -> Self {
25
+ if index < self.items.len() {
26
+ self.selected = Some(index);
27
+ }
28
+ self
29
+ }
30
+
31
+ pub fn select(&mut self, index: usize) {
32
+ if index < self.items.len() {
33
+ self.selected = Some(index);
34
+ }
35
+ }
36
+
37
+ pub fn toggle(&mut self) {
38
+ self.open = !self.open;
39
+ }
40
+
41
+ pub fn close(&mut self) {
42
+ self.open = false;
43
+ }
44
+
45
+ pub fn selected_text(&self) -> Option<&str> {
46
+ self.selected.map(|i| self.items[i].as_str())
47
+ }
48
+
49
+ /// Render the dropdown trigger button and popup.
50
+ /// Returns Some(index) when an item is selected.
51
+ pub fn show(&mut self, ui: &mut Ui) -> Option<usize> {
52
+ let colors = Colors::default();
53
+ let label = self.selected_text().unwrap_or("Select an option...");
54
+
55
+ let response = ui.add_sized(
56
+ [ui.available_width(), 28.0],
57
+ egui::Button::new(label)
58
+ .fill(colors.surface_2)
59
+ .stroke(egui::Stroke::new(1.0, colors.border))
60
+ .rounding(Rounding::same(tokens::RADIUS)),
61
+ );
62
+
63
+ if response.clicked() {
64
+ self.open = !self.open;
65
+ }
66
+
67
+ // Sync open state with egui memory
68
+ if self.open {
69
+ ui.memory_mut(|mem| mem.open_popup(self.id));
70
+ } else {
71
+ ui.memory_mut(|mem| mem.close_popup());
72
+ }
73
+
74
+ if !self.open {
75
+ return None;
76
+ }
77
+
78
+ let items = &self.items;
79
+ let selected = self.selected;
80
+ let popup_id = self.id;
81
+
82
+ egui::popup::popup_below_widget(ui, popup_id, &response, |ui| {
83
+ // popup_below_widget already wraps in Frame::popup
84
+ // Customize the style colors for our theme
85
+ for (i, item) in items.iter().enumerate() {
86
+ let is_selected = selected == Some(i);
87
+ let text = if is_selected {
88
+ format!("✓ {}", item)
89
+ } else {
90
+ item.clone()
91
+ };
92
+
93
+ if ui.selectable_label(is_selected, text).clicked() {
94
+ ui.memory_mut(|m| {
95
+ m.data.insert_temp(popup_id.with("chosen"), Some(i));
96
+ m.close_popup();
97
+ });
98
+ }
99
+ }
100
+ });
101
+
102
+ // Check if popup is still open
103
+ let still_open = ui.memory(|mem| mem.is_popup_open(popup_id));
104
+ if !still_open {
105
+ self.open = false;
106
+ let chosen = ui.memory_mut(|m| m.data.get_temp::<usize>(popup_id.with("chosen")));
107
+ if let Some(idx) = chosen {
108
+ self.selected = Some(idx);
109
+ ui.memory_mut(|m| m.data.remove::<usize>(popup_id.with("chosen")));
110
+ return Some(idx);
111
+ }
112
+ }
113
+
114
+ // Also check result for chosen item stored in memory
115
+ let chosen = ui.memory_mut(|m| m.data.get_temp::<usize>(popup_id.with("chosen")));
116
+ if let Some(idx) = chosen {
117
+ self.selected = Some(idx);
118
+ self.open = false;
119
+ ui.memory_mut(|m| m.data.remove::<usize>(popup_id.with("chosen")));
120
+ return Some(idx);
121
+ }
122
+
123
+ None
124
+ }
125
+ }
126
+
127
+ /// Functional dropdown helper
128
+ pub fn dropdown(ui: &mut Ui, id: impl Into<Id>, items: &[String], selected: &mut Option<usize>) {
129
+ let colors = Colors::default();
130
+ let popup_id = id.into();
131
+
132
+ let label = selected
133
+ .and_then(|i| items.get(i))
134
+ .map(|s| s.as_str())
135
+ .unwrap_or("Select an option...");
136
+
137
+ let response = ui.add_sized(
138
+ [ui.available_width(), 28.0],
139
+ egui::Button::new(label)
140
+ .fill(colors.surface_2)
141
+ .stroke(egui::Stroke::new(1.0, colors.border))
142
+ .rounding(Rounding::same(tokens::RADIUS)),
143
+ );
144
+
145
+ if response.clicked() {
146
+ ui.memory_mut(|mem| {
147
+ if mem.is_popup_open(popup_id) {
148
+ mem.close_popup();
149
+ } else {
150
+ mem.open_popup(popup_id);
151
+ }
152
+ });
153
+ }
154
+
155
+ let is_open = ui.memory(|mem| mem.is_popup_open(popup_id));
156
+
157
+ if is_open {
158
+ let current_selected = *selected;
159
+ egui::popup::popup_below_widget(ui, popup_id, &response, |ui| {
160
+ for (i, item) in items.iter().enumerate() {
161
+ let is_sel = current_selected == Some(i);
162
+ let text = if is_sel { format!("✓ {}", item) } else { item.clone() };
163
+ if ui.selectable_label(is_sel, text).clicked() {
164
+ ui.memory_mut(|m| {
165
+ m.data.insert_temp(popup_id.with("result"), i);
166
+ m.close_popup();
167
+ });
168
+ }
169
+ }
170
+ });
171
+
172
+ // Check result
173
+ let result = ui.memory_mut(|m| m.data.get_temp::<usize>(popup_id.with("result")));
174
+ if let Some(idx) = result {
175
+ *selected = Some(idx);
176
+ ui.memory_mut(|m| m.data.remove::<usize>(popup_id.with("result")));
177
+ }
178
+ }
179
+ }