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.
- package/README.md +20 -5
- package/package.json +20 -5
- package/packages/flutter/lib/cronixui.dart +41 -0
- package/packages/flutter/lib/src/tokens/colors.dart +34 -0
- package/packages/flutter/lib/src/tokens/spacing.dart +54 -0
- package/packages/flutter/lib/src/tokens/theme.dart +174 -0
- package/packages/flutter/lib/src/widgets/cn_accordion.dart +254 -0
- package/packages/flutter/lib/src/widgets/cn_alert.dart +137 -0
- package/packages/flutter/lib/src/widgets/cn_avatar.dart +98 -0
- package/packages/flutter/lib/src/widgets/cn_badge.dart +80 -0
- package/packages/flutter/lib/src/widgets/cn_breadcrumb.dart +88 -0
- package/packages/flutter/lib/src/widgets/cn_button.dart +137 -0
- package/packages/flutter/lib/src/widgets/cn_card.dart +99 -0
- package/packages/flutter/lib/src/widgets/cn_checkbox.dart +77 -0
- package/packages/flutter/lib/src/widgets/cn_command_palette.dart +299 -0
- package/packages/flutter/lib/src/widgets/cn_container.dart +131 -0
- package/packages/flutter/lib/src/widgets/cn_dropdown.dart +149 -0
- package/packages/flutter/lib/src/widgets/cn_file_input.dart +113 -0
- package/packages/flutter/lib/src/widgets/cn_footer.dart +108 -0
- package/packages/flutter/lib/src/widgets/cn_header.dart +173 -0
- package/packages/flutter/lib/src/widgets/cn_input.dart +142 -0
- package/packages/flutter/lib/src/widgets/cn_list.dart +150 -0
- package/packages/flutter/lib/src/widgets/cn_modal.dart +213 -0
- package/packages/flutter/lib/src/widgets/cn_nav.dart +157 -0
- package/packages/flutter/lib/src/widgets/cn_pagination.dart +193 -0
- package/packages/flutter/lib/src/widgets/cn_progress.dart +146 -0
- package/packages/flutter/lib/src/widgets/cn_radio.dart +133 -0
- package/packages/flutter/lib/src/widgets/cn_search.dart +183 -0
- package/packages/flutter/lib/src/widgets/cn_select.dart +244 -0
- package/packages/flutter/lib/src/widgets/cn_sidebar.dart +207 -0
- package/packages/flutter/lib/src/widgets/cn_skeleton.dart +136 -0
- package/packages/flutter/lib/src/widgets/cn_slider.dart +141 -0
- package/packages/flutter/lib/src/widgets/cn_spinner.dart +85 -0
- package/packages/flutter/lib/src/widgets/cn_stat.dart +135 -0
- package/packages/flutter/lib/src/widgets/cn_table.dart +136 -0
- package/packages/flutter/lib/src/widgets/cn_tabs.dart +229 -0
- package/packages/flutter/lib/src/widgets/cn_tag.dart +185 -0
- package/packages/flutter/lib/src/widgets/cn_textarea.dart +143 -0
- package/packages/flutter/lib/src/widgets/cn_toast.dart +121 -0
- package/packages/flutter/lib/src/widgets/cn_toggle.dart +78 -0
- package/packages/flutter/lib/src/widgets/cn_tooltip.dart +118 -0
- package/packages/flutter/pubspec.yaml +20 -0
- package/packages/go/cronixui/cronixui.go +784 -237
- package/packages/go/cronixui/go.mod +32 -0
- package/packages/go/cronixui/go.sum +666 -0
- package/packages/python/cronixui/__init__.py +59 -3
- package/packages/python/cronixui/alert.py +61 -0
- package/packages/python/cronixui/avatar.py +50 -0
- package/packages/python/cronixui/badge.py +46 -0
- package/packages/python/cronixui/button.py +64 -0
- package/packages/python/cronixui/card.py +62 -0
- package/packages/python/cronixui/form.py +255 -0
- package/packages/python/cronixui/layout.py +143 -0
- package/packages/python/cronixui/list.py +51 -0
- package/packages/python/cronixui/loading.py +36 -0
- package/packages/python/cronixui/progress.py +90 -0
- package/packages/python/cronixui/table.py +48 -0
- package/packages/python/cronixui/tooltip.py +28 -0
- package/packages/react/src/components/Accordion.tsx +82 -0
- package/packages/react/src/components/Alert.tsx +80 -0
- package/packages/react/src/components/Avatar.tsx +54 -0
- package/packages/react/src/components/Badge.tsx +32 -0
- package/packages/react/src/components/Breadcrumb.tsx +50 -0
- package/packages/react/src/components/Button.tsx +47 -0
- package/packages/react/src/components/Card.tsx +69 -0
- package/packages/react/src/components/Checkbox.tsx +30 -0
- package/packages/react/src/components/CommandPalette.tsx +131 -0
- package/packages/react/src/components/Container.tsx +26 -0
- package/packages/react/src/components/Dropdown.tsx +88 -0
- package/packages/react/src/components/FileInput.tsx +86 -0
- package/packages/react/src/components/Footer.tsx +36 -0
- package/packages/react/src/components/FormGroup.tsx +36 -0
- package/packages/react/src/components/Header.tsx +29 -0
- package/packages/react/src/components/Input.tsx +54 -0
- package/packages/react/src/components/List.tsx +55 -0
- package/packages/react/src/components/Modal.tsx +89 -0
- package/packages/react/src/components/Nav.tsx +63 -0
- package/packages/react/src/components/Pagination.tsx +107 -0
- package/packages/react/src/components/Progress.tsx +49 -0
- package/packages/react/src/components/Radio.tsx +64 -0
- package/packages/react/src/components/Search.tsx +95 -0
- package/packages/react/src/components/Select.tsx +41 -0
- package/packages/react/src/components/Sidebar.tsx +64 -0
- package/packages/react/src/components/Skeleton.tsx +39 -0
- package/packages/react/src/components/Slider.tsx +32 -0
- package/packages/react/src/components/Spinner.tsx +24 -0
- package/packages/react/src/components/Stack.tsx +69 -0
- package/packages/react/src/components/Stat.tsx +35 -0
- package/packages/react/src/components/Table.tsx +90 -0
- package/packages/react/src/components/Tabs.tsx +85 -0
- package/packages/react/src/components/Tag.tsx +30 -0
- package/packages/react/src/components/Textarea.tsx +21 -0
- package/packages/react/src/components/Toast.tsx +134 -0
- package/packages/react/src/components/Toggle.tsx +58 -0
- package/packages/react/src/components/Tooltip.tsx +31 -0
- package/packages/react/src/components/Typography.tsx +66 -0
- package/packages/react/src/index.ts +40 -0
- package/packages/react/src/styles.css +2039 -0
- package/packages/react/src/tokens/index.ts +94 -0
- package/packages/rust/cronixui/src/colors.rs +135 -0
- package/packages/rust/cronixui/src/components/accordion.rs +47 -0
- package/packages/rust/cronixui/src/components/alert.rs +95 -0
- package/packages/rust/cronixui/src/components/avatar.rs +85 -0
- package/packages/rust/cronixui/src/components/badge.rs +35 -0
- package/packages/rust/cronixui/src/components/breadcrumb.rs +58 -0
- package/packages/rust/cronixui/src/components/button.rs +70 -0
- package/packages/rust/cronixui/src/components/card.rs +259 -0
- package/packages/rust/cronixui/src/components/command_palette.rs +254 -0
- package/packages/rust/cronixui/src/components/dropdown.rs +179 -0
- package/packages/rust/cronixui/src/components/file_input.rs +74 -0
- package/packages/rust/cronixui/src/components/input.rs +21 -0
- package/packages/rust/cronixui/src/components/list.rs +38 -0
- package/packages/rust/cronixui/src/components/mod.rs +51 -0
- package/packages/rust/cronixui/src/{modal.rs → components/modal.rs} +15 -1
- package/packages/rust/cronixui/src/components/nav.rs +19 -0
- package/packages/rust/cronixui/src/{pagination.rs → components/pagination.rs} +14 -13
- package/packages/rust/cronixui/src/components/progress.rs +50 -0
- package/packages/rust/cronixui/src/components/search.rs +185 -0
- package/packages/rust/cronixui/src/components/skeleton.rs +63 -0
- package/packages/rust/cronixui/src/components/spinner.rs +21 -0
- package/packages/rust/cronixui/src/components/table.rs +56 -0
- package/packages/rust/cronixui/src/components/tabs.rs +43 -0
- package/packages/rust/cronixui/src/components/toast.rs +69 -0
- package/packages/rust/cronixui/src/{toggle.rs → components/toggle.rs} +7 -5
- package/packages/rust/cronixui/src/components/tooltip.rs +11 -0
- package/packages/rust/cronixui/src/lib.rs +111 -64
- package/packages/rust/cronixui/src/tokens.rs +97 -127
- package/packages/web/src/variables.css +81 -81
- package/packages/go/cronixui/tokens.go +0 -129
- package/packages/python/cronixui/pyproject.toml +0 -11
- package/packages/react/src/components/Accordion.jsx +0 -50
- package/packages/react/src/components/Alert.jsx +0 -62
- package/packages/react/src/components/Avatar.jsx +0 -34
- package/packages/react/src/components/Badge.jsx +0 -15
- package/packages/react/src/components/Breadcrumb.jsx +0 -27
- package/packages/react/src/components/Button.jsx +0 -21
- package/packages/react/src/components/Card.jsx +0 -23
- package/packages/react/src/components/Checkbox.jsx +0 -27
- package/packages/react/src/components/CommandPalette.jsx +0 -93
- package/packages/react/src/components/Dropdown.jsx +0 -48
- package/packages/react/src/components/FileInput.jsx +0 -44
- package/packages/react/src/components/Input.jsx +0 -22
- package/packages/react/src/components/List.jsx +0 -29
- package/packages/react/src/components/Modal.jsx +0 -65
- package/packages/react/src/components/Nav.jsx +0 -50
- package/packages/react/src/components/Pagination.jsx +0 -81
- package/packages/react/src/components/Progress.jsx +0 -23
- package/packages/react/src/components/Radio.jsx +0 -50
- package/packages/react/src/components/Search.jsx +0 -70
- package/packages/react/src/components/Select.jsx +0 -33
- package/packages/react/src/components/Skeleton.jsx +0 -15
- package/packages/react/src/components/Slider.jsx +0 -29
- package/packages/react/src/components/Spinner.jsx +0 -5
- package/packages/react/src/components/Stat.jsx +0 -19
- package/packages/react/src/components/Table.jsx +0 -48
- package/packages/react/src/components/Tabs.jsx +0 -65
- package/packages/react/src/components/Tag.jsx +0 -19
- package/packages/react/src/components/Textarea.jsx +0 -17
- package/packages/react/src/components/Toast.jsx +0 -78
- package/packages/react/src/components/Toggle.jsx +0 -34
- package/packages/react/src/components/Tooltip.jsx +0 -12
- package/packages/react/src/index.d.ts +0 -103
- package/packages/react/src/index.js +0 -33
- package/packages/rust/cronixui/src/accordion.rs +0 -49
- package/packages/rust/cronixui/src/command_palette.rs +0 -62
- package/packages/rust/cronixui/src/dropdown.rs +0 -31
- package/packages/rust/cronixui/src/search.rs +0 -49
- package/packages/rust/cronixui/src/tabs.rs +0 -23
- 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
|
+
}
|