1use serde::{Deserialize, Serialize};
6use yewdux::prelude::*;
7
8#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
10pub enum NotificationLevel {
11 #[default]
12 Info,
13 Success,
14 Warning,
15 Error,
16}
17
18impl NotificationLevel {
19 pub fn as_str(&self) -> &'static str {
20 match self {
21 Self::Info => "info",
22 Self::Success => "success",
23 Self::Warning => "warning",
24 Self::Error => "error",
25 }
26 }
27
28 pub fn icon(&self) -> &'static str {
29 match self {
30 Self::Info => "info",
31 Self::Success => "check-circle",
32 Self::Warning => "alert-triangle",
33 Self::Error => "x-circle",
34 }
35 }
36}
37
38#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
40pub struct Notification {
41 pub id: String,
43 pub title: String,
45 pub message: String,
47 pub level: NotificationLevel,
49 pub auto_dismiss_ms: Option<u32>,
51 pub dismissible: bool,
53 pub action_text: Option<String>,
55 pub timestamp: String,
57}
58
59impl Notification {
60 pub fn info(title: impl Into<String>, message: impl Into<String>) -> Self {
61 Self::new(title, message, NotificationLevel::Info)
62 }
63
64 pub fn success(title: impl Into<String>, message: impl Into<String>) -> Self {
65 Self::new(title, message, NotificationLevel::Success)
66 }
67
68 pub fn warning(title: impl Into<String>, message: impl Into<String>) -> Self {
69 Self::new(title, message, NotificationLevel::Warning)
70 }
71
72 pub fn error(title: impl Into<String>, message: impl Into<String>) -> Self {
73 Self::new(title, message, NotificationLevel::Error)
74 }
75
76 fn new(title: impl Into<String>, message: impl Into<String>, level: NotificationLevel) -> Self {
77 Self {
78 id: uuid::Uuid::new_v4().to_string(),
79 title: title.into(),
80 message: message.into(),
81 level,
82 auto_dismiss_ms: Some(5000),
83 dismissible: true,
84 action_text: None,
85 timestamp: chrono::Utc::now().to_rfc3339(),
86 }
87 }
88
89 pub fn persistent(mut self) -> Self {
90 self.auto_dismiss_ms = None;
91 self
92 }
93
94 pub fn with_action(mut self, text: impl Into<String>) -> Self {
95 self.action_text = Some(text.into());
96 self
97 }
98}
99
100#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
102pub struct ModalState {
103 pub open: bool,
105 pub modal_type: Option<ModalType>,
107 pub data: Option<String>,
109}
110
111#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
113pub enum ModalType {
114 Confirm,
116 InstallSkill,
118 UninstallSkill,
120 ConfigureInstance,
122 ExecutionDetails,
124 Export,
126 Import,
128 About,
130}
131
132#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
134pub struct CommandPaletteState {
135 pub open: bool,
137 pub query: String,
139 pub selected_index: usize,
141}
142
143#[derive(Clone, Debug, Default, PartialEq, Store)]
145pub struct UiStore {
146 pub sidebar_open: bool,
148 pub sidebar_collapsed: bool,
150 pub notifications: Vec<Notification>,
152 pub modal: ModalState,
154 pub command_palette: CommandPaletteState,
156 pub focused_element: Option<String>,
158 pub dark_mode: bool,
160 pub loading_overlay: bool,
162 pub loading_message: Option<String>,
164 pub is_mobile: bool,
166 pub is_touch_device: bool,
168}
169
170impl UiStore {
171 const MAX_NOTIFICATIONS: usize = 5;
173
174 pub fn visible_notifications(&self) -> &[Notification] {
176 let end = self.notifications.len().min(Self::MAX_NOTIFICATIONS);
177 &self.notifications[..end]
178 }
179
180 pub fn has_modal(&self) -> bool {
182 self.modal.open
183 }
184
185 pub fn is_command_palette_open(&self) -> bool {
187 self.command_palette.open
188 }
189}
190
191pub enum UiAction {
193 ToggleSidebar,
195 SetSidebarOpen(bool),
196 ToggleSidebarCollapsed,
197 SetSidebarCollapsed(bool),
198 AddNotification(Notification),
200 DismissNotification(String),
201 ClearNotifications,
202 OpenModal(ModalType, Option<String>),
204 CloseModal,
205 OpenCommandPalette,
207 CloseCommandPalette,
208 SetCommandPaletteQuery(String),
209 SelectCommandPaletteItem(usize),
210 CommandPaletteUp,
211 CommandPaletteDown,
212 ShowLoading(Option<String>),
214 HideLoading,
215 SetIsMobile(bool),
217 SetIsTouchDevice(bool),
218 SetDarkMode(bool),
220 SetFocusedElement(Option<String>),
222}
223
224impl Reducer<UiStore> for UiAction {
225 fn apply(self, mut store: std::rc::Rc<UiStore>) -> std::rc::Rc<UiStore> {
226 let state = std::rc::Rc::make_mut(&mut store);
227
228 match self {
229 UiAction::ToggleSidebar => {
231 state.sidebar_open = !state.sidebar_open;
232 }
233 UiAction::SetSidebarOpen(open) => {
234 state.sidebar_open = open;
235 }
236 UiAction::ToggleSidebarCollapsed => {
237 state.sidebar_collapsed = !state.sidebar_collapsed;
238 }
239 UiAction::SetSidebarCollapsed(collapsed) => {
240 state.sidebar_collapsed = collapsed;
241 }
242 UiAction::AddNotification(notification) => {
244 state.notifications.insert(0, notification);
246 if state.notifications.len() > 20 {
248 state.notifications.truncate(20);
249 }
250 }
251 UiAction::DismissNotification(id) => {
252 state.notifications.retain(|n| n.id != id);
253 }
254 UiAction::ClearNotifications => {
255 state.notifications.clear();
256 }
257 UiAction::OpenModal(modal_type, data) => {
259 state.modal = ModalState {
260 open: true,
261 modal_type: Some(modal_type),
262 data,
263 };
264 }
265 UiAction::CloseModal => {
266 state.modal = ModalState::default();
267 }
268 UiAction::OpenCommandPalette => {
270 state.command_palette = CommandPaletteState {
271 open: true,
272 query: String::new(),
273 selected_index: 0,
274 };
275 }
276 UiAction::CloseCommandPalette => {
277 state.command_palette = CommandPaletteState::default();
278 }
279 UiAction::SetCommandPaletteQuery(query) => {
280 state.command_palette.query = query;
281 state.command_palette.selected_index = 0;
282 }
283 UiAction::SelectCommandPaletteItem(index) => {
284 state.command_palette.selected_index = index;
285 }
286 UiAction::CommandPaletteUp => {
287 if state.command_palette.selected_index > 0 {
288 state.command_palette.selected_index -= 1;
289 }
290 }
291 UiAction::CommandPaletteDown => {
292 state.command_palette.selected_index += 1;
293 }
294 UiAction::ShowLoading(message) => {
296 state.loading_overlay = true;
297 state.loading_message = message;
298 }
299 UiAction::HideLoading => {
300 state.loading_overlay = false;
301 state.loading_message = None;
302 }
303 UiAction::SetIsMobile(is_mobile) => {
305 state.is_mobile = is_mobile;
306 if is_mobile {
308 state.sidebar_open = false;
309 }
310 }
311 UiAction::SetIsTouchDevice(is_touch) => {
312 state.is_touch_device = is_touch;
313 }
314 UiAction::SetDarkMode(dark) => {
316 state.dark_mode = dark;
317 }
318 UiAction::SetFocusedElement(element) => {
320 state.focused_element = element;
321 }
322 }
323
324 store
325 }
326}