From ef3a0e82b8f670bb2782ccca235626fa73e772f9 Mon Sep 17 00:00:00 2001 From: despiegk Date: Fri, 8 Aug 2025 09:02:39 +0200 Subject: [PATCH] ... --- Cargo.toml | 3 +- index.scss | 222 +------------------ src/kanban.rs | 494 +++++++++++++++++++++++++++++++++++++++++-- styles/comments.scss | 250 ++++++++++++++++++++++ styles/forms.scss | 210 ++++++++++++++++++ styles/modal.scss | 215 +++++++++++++++++++ 6 files changed, 1167 insertions(+), 227 deletions(-) create mode 100644 styles/comments.scss create mode 100644 styles/forms.scss create mode 100644 styles/modal.scss diff --git a/Cargo.toml b/Cargo.toml index 51ab2cb..b315c7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,4 +30,5 @@ gloo-utils = "0.1" gloo-storage = "0.2" gloo-net = "0.4" wasm-bindgen-futures = "0.4" -serde = { version = "1.0", features = ["derive"] } \ No newline at end of file +serde = { version = "1.0", features = ["derive"] } +chrono = { version = "0.4", features = ["serde", "wasmbind"] } \ No newline at end of file diff --git a/index.scss b/index.scss index f5d8966..aab6577 100644 --- a/index.scss +++ b/index.scss @@ -1,3 +1,8 @@ +// Import component styles +@import 'styles/modal.scss'; +@import 'styles/comments.scss'; +@import 'styles/forms.scss'; + // CSS Custom Properties for theming :root { --bs-body-bg: #ffffff; @@ -14,6 +19,12 @@ --bs-footer-bg: #e9ecef; --bs-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); --bs-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175); + --bs-primary: #0d6efd; + --bs-primary-rgb: 13, 110, 253; + --bs-secondary: #6c757d; + --bs-danger: #dc3545; + --bs-danger-rgb: 220, 53, 69; + --bs-success: #198754; } // Dark theme variables @@ -543,156 +554,6 @@ body { } } -// Modal styles -.modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - backdrop-filter: blur(4px); -} - -.modal-content { - background-color: var(--bs-card-bg); - border-radius: 0.75rem; - box-shadow: var(--bs-shadow-lg); - max-width: 600px; - width: 90%; - max-height: 90vh; - overflow-y: auto; - border: 1px solid var(--bs-card-border); -} - -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1.5rem; - border-bottom: 1px solid var(--bs-card-border); - - h2 { - margin: 0; - color: var(--bs-body-color); - font-weight: 600; - } - - .btn-close { - background: none; - border: none; - font-size: 1.25rem; - color: var(--bs-navbar-color); - cursor: pointer; - padding: 0.5rem; - border-radius: 0.25rem; - transition: all 0.2s ease; - - &:hover { - background-color: rgba(var(--bs-danger-rgb), 0.1); - color: var(--bs-danger); - } - } -} - -.modal-body { - padding: 1.5rem; -} - -.modal-footer { - display: flex; - justify-content: flex-end; - gap: 0.75rem; - padding: 1.5rem; - border-top: 1px solid var(--bs-card-border); -} - -.form-group { - margin-bottom: 1.25rem; - - label { - display: block; - margin-bottom: 0.5rem; - font-weight: 500; - color: var(--bs-body-color); - } - - .form-control { - width: 100%; - padding: 0.75rem; - border: 1px solid var(--bs-card-border); - border-radius: 0.375rem; - background-color: var(--bs-body-bg); - color: var(--bs-body-color); - font-size: 0.875rem; - transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; - - &:focus { - outline: none; - border-color: var(--bs-primary); - box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25); - } - } - - textarea.form-control { - resize: vertical; - min-height: 100px; - } -} - -.form-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1rem; - - @media (max-width: 576px) { - grid-template-columns: 1fr; - } -} - -.stat-display { - display: flex; - align-items: center; - padding: 0.75rem; - background-color: var(--bs-feature-bg); - border-radius: 0.375rem; - color: var(--bs-navbar-color); - font-weight: 500; -} - -.checklist-display { - display: flex; - align-items: center; - gap: 1rem; - padding: 0.75rem; - background-color: var(--bs-feature-bg); - border-radius: 0.375rem; - - .progress-bar-large { - flex: 1; - height: 8px; - background-color: var(--bs-card-border); - border-radius: 4px; - overflow: hidden; - - .progress-fill { - height: 100%; - background-color: #198754; - transition: width 0.3s ease; - } - } - - .progress-text-large { - font-size: 0.875rem; - color: var(--bs-navbar-color); - font-weight: 500; - white-space: nowrap; - } -} // Save popup styles .save-popup { @@ -763,64 +624,3 @@ body { border-radius: 0.5rem; } } - -// Button styles -.btn { - padding: 0.5rem 1rem; - border-radius: 0.375rem; - font-weight: 500; - text-decoration: none; - display: inline-flex; - align-items: center; - justify-content: center; - transition: all 0.2s ease; - cursor: pointer; - border: 1px solid transparent; - - &.btn-primary { - background-color: var(--bs-primary); - color: white; - - &:hover { - background-color: var(--bs-primary); - filter: brightness(0.9); - transform: translateY(-1px); - } - } - - &.btn-secondary { - background-color: var(--bs-secondary); - color: white; - - &:hover { - background-color: var(--bs-secondary); - filter: brightness(0.9); - transform: translateY(-1px); - } - } -} - -// Responsive modal adjustments -@media (max-width: 768px) { - .modal-content { - width: 95%; - margin: 1rem; - } - - .modal-header, - .modal-body, - .modal-footer { - padding: 1rem; - } - - .save-popup { - top: 1rem; - right: 1rem; - left: 1rem; - - .save-popup-content { - padding: 0.75rem 1rem; - font-size: 0.875rem; - } - } -} \ No newline at end of file diff --git a/src/kanban.rs b/src/kanban.rs index 84b70ce..57eccaa 100644 --- a/src/kanban.rs +++ b/src/kanban.rs @@ -2,6 +2,7 @@ use yew::prelude::*; use serde::{Deserialize, Serialize}; use web_sys::{DragEvent, HtmlInputElement, HtmlTextAreaElement, HtmlSelectElement, MouseEvent}; use wasm_bindgen::JsCast; +use chrono::Utc; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct KanbanData { @@ -31,6 +32,16 @@ pub struct KanbanCard { pub attachments: u32, pub comments: u32, pub checklist: ChecklistInfo, + #[serde(rename = "commentsList")] + pub comments_list: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Comment { + pub id: String, + pub author: String, + pub content: String, + pub timestamp: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -60,6 +71,20 @@ fn get_sample_data() -> KanbanData { attachments: 2, comments: 3, checklist: ChecklistInfo { completed: 1, total: 4 }, + comments_list: vec![ + Comment { + id: "comment-1".to_string(), + author: "Bob Smith".to_string(), + content: "Looking good! Can we add more color variations?".to_string(), + timestamp: "2024-01-10T10:30:00Z".to_string(), + }, + Comment { + id: "comment-2".to_string(), + author: "Alice Johnson".to_string(), + content: "Sure, I'll work on that today.".to_string(), + timestamp: "2024-01-10T11:15:00Z".to_string(), + }, + ], }, KanbanCard { id: "card-2".to_string(), @@ -72,6 +97,14 @@ fn get_sample_data() -> KanbanData { attachments: 0, comments: 1, checklist: ChecklistInfo { completed: 0, total: 3 }, + comments_list: vec![ + Comment { + id: "comment-3".to_string(), + author: "Charlie Brown".to_string(), + content: "I found some interesting data on competitor pricing.".to_string(), + timestamp: "2024-01-09T14:20:00Z".to_string(), + }, + ], }, ], }, @@ -91,6 +124,20 @@ fn get_sample_data() -> KanbanData { attachments: 1, comments: 5, checklist: ChecklistInfo { completed: 2, total: 5 }, + comments_list: vec![ + Comment { + id: "comment-4".to_string(), + author: "Diana Prince".to_string(), + content: "Make sure to use bcrypt for password hashing.".to_string(), + timestamp: "2024-01-08T09:00:00Z".to_string(), + }, + Comment { + id: "comment-5".to_string(), + author: "Charlie Brown".to_string(), + content: "Already on it! Also implementing 2FA.".to_string(), + timestamp: "2024-01-08T09:30:00Z".to_string(), + }, + ], }, KanbanCard { id: "card-4".to_string(), @@ -103,6 +150,14 @@ fn get_sample_data() -> KanbanData { attachments: 3, comments: 2, checklist: ChecklistInfo { completed: 3, total: 6 }, + comments_list: vec![ + Comment { + id: "comment-6".to_string(), + author: "Eve Wilson".to_string(), + content: "Don't forget to backup before migration!".to_string(), + timestamp: "2024-01-07T16:45:00Z".to_string(), + }, + ], }, ], }, @@ -122,6 +177,14 @@ fn get_sample_data() -> KanbanData { attachments: 2, comments: 4, checklist: ChecklistInfo { completed: 4, total: 4 }, + comments_list: vec![ + Comment { + id: "comment-7".to_string(), + author: "Frank Miller".to_string(), + content: "Great work on the examples!".to_string(), + timestamp: "2024-01-06T13:20:00Z".to_string(), + }, + ], }, ], }, @@ -141,6 +204,14 @@ fn get_sample_data() -> KanbanData { attachments: 1, comments: 2, checklist: ChecklistInfo { completed: 3, total: 3 }, + comments_list: vec![ + Comment { + id: "comment-8".to_string(), + author: "Grace Lee".to_string(), + content: "Environment is working perfectly!".to_string(), + timestamp: "2024-01-04T12:00:00Z".to_string(), + }, + ], }, KanbanCard { id: "card-7".to_string(), @@ -153,6 +224,14 @@ fn get_sample_data() -> KanbanData { attachments: 4, comments: 8, checklist: ChecklistInfo { completed: 5, total: 5 }, + comments_list: vec![ + Comment { + id: "comment-9".to_string(), + author: "Alice Johnson".to_string(), + content: "Timeline looks realistic. Good planning!".to_string(), + timestamp: "2024-01-02T15:30:00Z".to_string(), + }, + ], }, ], }, @@ -164,6 +243,7 @@ fn get_sample_data() -> KanbanData { pub fn kanban_board() -> Html { let data = use_state(|| get_sample_data()); let editing_card = use_state(|| None::); + let comments_card = use_state(|| None::); let show_save_popup = use_state(|| false); let dragging_card = use_state(|| None::); @@ -174,6 +254,13 @@ pub fn kanban_board() -> Html { }) }; + let on_comments_click = { + let comments_card = comments_card.clone(); + Callback::from(move |card: KanbanCard| { + comments_card.set(Some(card)); + }) + }; + let on_close_edit = { let editing_card = editing_card.clone(); Callback::from(move |_| { @@ -181,6 +268,13 @@ pub fn kanban_board() -> Html { }) }; + let on_close_comments = { + let comments_card = comments_card.clone(); + Callback::from(move |_| { + comments_card.set(None); + }) + }; + let on_save_card = { let data = data.clone(); let editing_card = editing_card.clone(); @@ -263,6 +357,7 @@ pub fn kanban_board() -> Html { @@ -296,6 +391,18 @@ pub fn kanban_board() -> Html { } else { html! {} }} + + // Comments modal + {if let Some(card) = (*comments_card).clone() { + html! { + + } + } else { + html! {} + }} } @@ -305,6 +412,7 @@ pub fn kanban_board() -> Html { pub struct KanbanColumnProps { pub column: KanbanColumn, pub on_edit_card: Callback, + pub on_comments_click: Callback, pub on_card_drag_start: Callback, pub on_card_drop: Callback<(String, usize)>, } @@ -361,6 +469,7 @@ pub fn kanban_column_component(props: &KanbanColumnProps) -> Html { @@ -376,6 +485,7 @@ pub struct KanbanCardProps { pub card: KanbanCard, pub on_edit: Callback, pub on_drag_start: Callback, + pub on_comments_click: Callback, pub index: usize, } @@ -405,11 +515,21 @@ pub fn kanban_card_component(props: &KanbanCardProps) -> Html { let onclick = { let on_edit = props.on_edit.clone(); let card = card.clone(); - Callback::from(move |_| { + Callback::from(move |e: MouseEvent| { + e.stop_propagation(); on_edit.emit(card.clone()); }) }; + let on_comments_click = { + let on_comments_click = props.on_comments_click.clone(); + let card = card.clone(); + Callback::from(move |e: MouseEvent| { + e.stop_propagation(); + on_comments_click.emit(card.clone()); + }) + }; + let ondragstart = { let on_drag_start = props.on_drag_start.clone(); let card_id = card.id.clone(); @@ -471,7 +591,7 @@ pub fn kanban_card_component(props: &KanbanCardProps) -> Html { {if card.comments > 0 { html! { -
+
{card.comments}
@@ -496,6 +616,41 @@ pub fn kanban_card_component(props: &KanbanCardProps) -> Html { html! {} }}
+ + // Dense comments display + {if !card.comments_list.is_empty() { + html! { +
+
+ {for card.comments_list.iter().take(2).map(|comment| { + html! { +
+ {format!("{}:", &comment.author)} + { + if comment.content.len() > 50 { + format!("{}...", &comment.content[..50]) + } else { + comment.content.clone() + } + } +
+ } + })} + {if card.comments_list.len() > 2 { + html! { +
+ {format!("View all {} comments", card.comments_list.len())} +
+ } + } else { + html! {} + }} +
+
+ } + } else { + html! {} + }} } } @@ -554,10 +709,35 @@ pub fn card_edit_modal(props: &CardEditModalProps) -> Html { let on_due_date_change = { let card = card.clone(); - Callback::from(move |e: Event| { - let input: HtmlInputElement = e.target_unchecked_into(); + Callback::from(move |date: String| { let mut updated_card = (*card).clone(); - updated_card.due_date = input.value(); + updated_card.due_date = date; + card.set(updated_card); + }) + }; + + let on_add_comment = { + let card = card.clone(); + Callback::from(move |content: String| { + let mut updated_card = (*card).clone(); + let new_comment = Comment { + id: format!("comment-{}", Utc::now().timestamp_millis()), + author: "Current User".to_string(), // In a real app, this would be the logged-in user + content, + timestamp: Utc::now().to_rfc3339(), + }; + updated_card.comments_list.push(new_comment); + updated_card.comments = updated_card.comments_list.len() as u32; + card.set(updated_card); + }) + }; + + let on_delete_comment = { + let card = card.clone(); + Callback::from(move |comment_id: String| { + let mut updated_card = (*card).clone(); + updated_card.comments_list.retain(|c| c.id != comment_id); + updated_card.comments = updated_card.comments_list.len() as u32; card.set(updated_card); }) }; @@ -648,16 +828,12 @@ pub fn card_edit_modal(props: &CardEditModalProps) -> Html { -
- - -
+
@@ -716,6 +892,12 @@ pub fn card_edit_modal(props: &CardEditModalProps) -> Html {
+ + } +} +// Reusable DateSelector Component +#[derive(Properties, PartialEq)] +pub struct DateSelectorProps { + pub value: String, + pub on_change: Callback, + pub label: String, + pub id: String, +} + +#[function_component(DateSelector)] +pub fn date_selector(props: &DateSelectorProps) -> Html { + // Convert date format for HTML5 date input (expects YYYY-MM-DD) + let formatted_value = format_date_for_input(&props.value); + + let on_change = { + let on_change = props.on_change.clone(); + Callback::from(move |e: Event| { + let input: HtmlInputElement = e.target_unchecked_into(); + let date_value = input.value(); + // HTML5 date input returns YYYY-MM-DD format, which is what we want + on_change.emit(date_value); + }) + }; + + html! { +
+ + +
+ } +} + +// Helper function to ensure date is in YYYY-MM-DD format for HTML5 date input +fn format_date_for_input(date_str: &str) -> String { + // If already in YYYY-MM-DD format, return as is + if date_str.len() == 10 && date_str.chars().nth(4) == Some('-') && date_str.chars().nth(7) == Some('-') { + return date_str.to_string(); + } + + // Try to parse different date formats and convert to YYYY-MM-DD + // Handle DD/MM/YYYY format + if date_str.len() == 10 && date_str.chars().nth(2) == Some('/') && date_str.chars().nth(5) == Some('/') { + let parts: Vec<&str> = date_str.split('/').collect(); + if parts.len() == 3 { + return format!("{}-{:02}-{:02}", + parts[2], + parts[1].parse::().unwrap_or(1), + parts[0].parse::().unwrap_or(1) + ); + } + } + + // Handle MM/DD/YYYY format + if date_str.len() == 10 && date_str.chars().nth(2) == Some('/') && date_str.chars().nth(5) == Some('/') { + let parts: Vec<&str> = date_str.split('/').collect(); + if parts.len() == 3 { + // Assume MM/DD/YYYY if day > 12 + let first_num = parts[0].parse::().unwrap_or(1); + let second_num = parts[1].parse::().unwrap_or(1); + + if first_num > 12 { + // First number is day, so it's DD/MM/YYYY + return format!("{}-{:02}-{:02}", parts[2], second_num, first_num); + } else { + // Assume MM/DD/YYYY + return format!("{}-{:02}-{:02}", parts[2], first_num, second_num); + } + } + } + + // If we can't parse it, return the original string + date_str.to_string() +} + +// Reusable CommentsCard Component +#[derive(Properties, PartialEq)] +pub struct CommentsCardProps { + pub comments: Vec, + pub on_add_comment: Callback, + pub on_delete_comment: Callback, +} + +#[function_component(CommentsCard)] +pub fn comments_card(props: &CommentsCardProps) -> Html { + let new_comment = use_state(|| String::new()); + + let on_comment_input = { + let new_comment = new_comment.clone(); + Callback::from(move |e: Event| { + let textarea: HtmlTextAreaElement = e.target_unchecked_into(); + new_comment.set(textarea.value()); + }) + }; + + let on_add_click = { + let on_add_comment = props.on_add_comment.clone(); + let new_comment = new_comment.clone(); + Callback::from(move |_| { + if !new_comment.trim().is_empty() { + on_add_comment.emit((*new_comment).clone()); + new_comment.set(String::new()); + } + }) + }; + + let format_timestamp = |timestamp: &str| -> String { + // Simple timestamp formatting - in a real app you'd use a proper date library + if let Some(date_part) = timestamp.split('T').next() { + date_part.to_string() + } else { + timestamp.to_string() + } + }; + + html! { +
+
+

{"Comments "}{format!("({})", props.comments.len())}

+
+ +
+ {for props.comments.iter().map(|comment| { + let on_delete = { + let on_delete_comment = props.on_delete_comment.clone(); + let comment_id = comment.id.clone(); + Callback::from(move |_| { + on_delete_comment.emit(comment_id.clone()); + }) + }; + + html! { +
+
+
+
+ {comment.author.chars().take(2).collect::().to_uppercase()} +
+ {&comment.author} +
+
+ {format_timestamp(&comment.timestamp)} + +
+
+
+ {&comment.content} +
+
+ } + })} +
+ +
+ + +
+
+ } +} + +// Comments Modal Component +#[derive(Properties, PartialEq)] +pub struct CommentsModalProps { + pub card: KanbanCard, + pub on_close: Callback<()>, +} + +#[function_component(CommentsModal)] +pub fn comments_modal(props: &CommentsModalProps) -> Html { + let card = &props.card; + + let on_close_click = { + let on_close = props.on_close.clone(); + Callback::from(move |_| { + on_close.emit(()); + }) + }; + + let on_close_click_modal = { + let on_close = props.on_close.clone(); + Callback::from(move |_| { + on_close.emit(()); + }) + }; + + let format_timestamp = |timestamp: &str| -> String { + // Simple timestamp formatting - in a real app you'd use a proper date library + if let Some(date_part) = timestamp.split('T').next() { + date_part.to_string() + } else { + timestamp.to_string() + } + }; + + html! { + + } } \ No newline at end of file diff --git a/styles/comments.scss b/styles/comments.scss new file mode 100644 index 0000000..8519393 --- /dev/null +++ b/styles/comments.scss @@ -0,0 +1,250 @@ +// Comments Card styles +.comments-card { + background-color: var(--bs-card-bg); + border: 1px solid var(--bs-card-border); + border-radius: 0.5rem; + padding: 1rem; + margin-bottom: 1rem; +} + +.comments-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--bs-card-border); + + h4 { + margin: 0; + color: var(--bs-body-color); + font-weight: 600; + font-size: 1.1rem; + } + + .comment-count { + color: var(--bs-navbar-color); + font-size: 0.9rem; + font-weight: normal; + } +} + +.comments-list { + max-height: 300px; + overflow-y: auto; + margin-bottom: 1rem; +} + +.comment-item { + padding: 0.75rem; + border-radius: 0.375rem; + background-color: var(--bs-feature-bg); + margin-bottom: 0.75rem; + border: 1px solid var(--bs-card-border); + + &:last-child { + margin-bottom: 0; + } +} + +.comment-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.comment-author { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.author-avatar { + width: 1.75rem; + height: 1.75rem; + border-radius: 50%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 0.7rem; + font-weight: 600; +} + +.author-name { + font-weight: 500; + color: var(--bs-body-color); + font-size: 0.875rem; +} + +.comment-meta { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.comment-date { + color: var(--bs-navbar-color); + font-size: 0.75rem; +} + +.btn-delete-comment { + background: none; + border: none; + color: var(--bs-navbar-color); + cursor: pointer; + padding: 0.25rem; + border-radius: 0.25rem; + transition: all 0.2s ease; + + &:hover { + background-color: rgba(var(--bs-danger-rgb), 0.1); + color: var(--bs-danger); + } + + i { + font-size: 0.75rem; + } +} + +.comment-content { + color: var(--bs-body-color); + font-size: 0.875rem; + line-height: 1.4; + margin: 0; +} + +.add-comment { + border-top: 1px solid var(--bs-card-border); + padding-top: 1rem; + + textarea { + width: 100%; + padding: 0.75rem; + border: 1px solid var(--bs-card-border); + border-radius: 0.375rem; + background-color: var(--bs-body-bg); + color: var(--bs-body-color); + font-size: 0.875rem; + resize: vertical; + min-height: 80px; + margin-bottom: 0.5rem; + + &:focus { + outline: none; + border-color: var(--bs-primary); + box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25); + } + + &::placeholder { + color: var(--bs-navbar-color); + } + } + + .btn { + padding: 0.5rem 1rem; + border-radius: 0.375rem; + font-weight: 500; + font-size: 0.875rem; + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } + } +} + +// Dense comments display for cards +.card-comments-dense { + margin-top: 0.5rem; + padding-top: 0.5rem; + border-top: 1px solid var(--bs-card-border); +} + +.comments-preview { + display: flex; + align-items: center; + gap: 0.5rem; + color: var(--bs-navbar-color); + font-size: 0.75rem; + cursor: pointer; + transition: color 0.2s ease; + + &:hover { + color: var(--bs-primary); + } + + i { + font-size: 0.875rem; + } +} + +.recent-comments { + margin-top: 0.25rem; + + .recent-comment { + background-color: var(--bs-feature-bg); + border-radius: 0.25rem; + padding: 0.25rem 0.5rem; + margin-bottom: 0.25rem; + font-size: 0.7rem; + line-height: 1.3; + + &:last-child { + margin-bottom: 0; + } + + .comment-author-small { + font-weight: 500; + color: var(--bs-body-color); + } + + .comment-text-small { + color: var(--bs-navbar-color); + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + } +} + +// Comments Modal specific styles +.comments-modal { + .modal-content { + max-width: 600px; + } + + .comments-list { + max-height: 400px; + } + + .comment-item { + margin-bottom: 1rem; + padding: 1rem; + } + + .comment-content { + font-size: 0.9rem; + line-height: 1.5; + } +} + +// Responsive adjustments +@media (max-width: 576px) { + .comment-header { + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + } + + .comment-meta { + align-self: flex-end; + } + + .add-comment textarea { + min-height: 60px; + } +} \ No newline at end of file diff --git a/styles/forms.scss b/styles/forms.scss new file mode 100644 index 0000000..77ac288 --- /dev/null +++ b/styles/forms.scss @@ -0,0 +1,210 @@ +// Form styles +.form-group { + margin-bottom: 1.25rem; + + label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + color: var(--bs-body-color); + } + + .form-control { + width: 100%; + padding: 0.75rem; + border: 1px solid var(--bs-card-border); + border-radius: 0.375rem; + background-color: var(--bs-body-bg); + color: var(--bs-body-color); + font-size: 0.875rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + + &:focus { + outline: none; + border-color: var(--bs-primary); + box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25); + } + } + + textarea.form-control { + resize: vertical; + min-height: 100px; + } + + select.form-control { + cursor: pointer; + } + + input[type="date"].form-control, + .date-picker { + cursor: pointer; + position: relative; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%236b7280'%3e%3cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z'/%3e%3c/svg%3e"); + background-position: right 0.75rem center; + background-repeat: no-repeat; + background-size: 1.25rem 1.25rem; + padding-right: 2.5rem; + + &::-webkit-calendar-picker-indicator { + opacity: 0; + position: absolute; + right: 0; + width: 2.5rem; + height: 100%; + cursor: pointer; + } + + &:hover { + border-color: var(--bs-primary); + } + } +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + + @media (max-width: 576px) { + grid-template-columns: 1fr; + } +} + +.stat-display { + display: flex; + align-items: center; + padding: 0.75rem; + background-color: var(--bs-feature-bg); + border-radius: 0.375rem; + color: var(--bs-navbar-color); + font-weight: 500; +} + +.checklist-display { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.75rem; + background-color: var(--bs-feature-bg); + border-radius: 0.375rem; + + .progress-bar-large { + flex: 1; + height: 8px; + background-color: var(--bs-card-border); + border-radius: 4px; + overflow: hidden; + + .progress-fill { + height: 100%; + background-color: #198754; + transition: width 0.3s ease; + } + } + + .progress-text-large { + font-size: 0.875rem; + color: var(--bs-navbar-color); + font-weight: 500; + white-space: nowrap; + } +} + +// Button styles +.btn { + padding: 0.5rem 1rem; + border-radius: 0.375rem; + font-weight: 500; + text-decoration: none; + display: inline-flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + cursor: pointer; + border: 1px solid transparent; + font-size: 0.875rem; + + &.btn-primary { + background-color: var(--bs-primary); + color: white; + + &:hover { + background-color: var(--bs-primary); + filter: brightness(0.9); + transform: translateY(-1px); + } + } + + &.btn-secondary { + background-color: var(--bs-secondary); + color: white; + + &:hover { + background-color: var(--bs-secondary); + filter: brightness(0.9); + transform: translateY(-1px); + } + } + + &.btn-sm { + padding: 0.375rem 0.75rem; + font-size: 0.8rem; + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none !important; + } +} + +// Date selector specific styles +.date-selector { + .form-control { + position: relative; + + &::-webkit-calendar-picker-indicator { + background: transparent; + bottom: 0; + color: transparent; + cursor: pointer; + height: auto; + left: 0; + position: absolute; + right: 0; + top: 0; + width: auto; + } + + &::-webkit-inner-spin-button { + display: none; + } + + &::-webkit-clear-button { + display: none; + } + } +} + +// Focus styles for accessibility +.btn:focus, +.form-control:focus { + box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25); +} + +// Responsive form adjustments +@media (max-width: 576px) { + .form-group { + margin-bottom: 1rem; + } + + .form-control { + padding: 0.625rem; + font-size: 0.875rem; + } + + .btn { + padding: 0.625rem 1rem; + font-size: 0.875rem; + } +} \ No newline at end of file diff --git a/styles/modal.scss b/styles/modal.scss new file mode 100644 index 0000000..404306d --- /dev/null +++ b/styles/modal.scss @@ -0,0 +1,215 @@ +// Modal styles +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + backdrop-filter: blur(4px); +} + +.modal-content { + background-color: var(--bs-card-bg); + border-radius: 0.75rem; + box-shadow: var(--bs-shadow-lg); + max-width: 800px; // Increased width + width: 95%; + max-height: 90vh; + overflow-y: auto; + border: 1px solid var(--bs-card-border); + + &.comments-modal { + max-width: 600px; + } +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5rem; + border-bottom: 1px solid var(--bs-card-border); + + h2 { + margin: 0; + color: var(--bs-body-color); + font-weight: 600; + } + + .btn-close { + background: none; + border: none; + font-size: 1.25rem; + color: var(--bs-navbar-color); + cursor: pointer; + padding: 0.5rem; + border-radius: 0.25rem; + transition: all 0.2s ease; + + &:hover { + background-color: rgba(var(--bs-danger-rgb), 0.1); + color: var(--bs-danger); + } + } +} + +.modal-body { + padding: 1.5rem; +} + +.modal-footer { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + padding: 1.5rem; + border-top: 1px solid var(--bs-card-border); +} + +// Dense comments styles for kanban cards +.card-comments-dense { + margin-top: 0.75rem; + padding-top: 0.75rem; + border-top: 1px solid var(--bs-card-border); +} + +.comments-preview { + .comment-preview { + display: flex; + gap: 0.25rem; + margin-bottom: 0.25rem; + font-size: 0.75rem; + line-height: 1.2; + + .comment-author { + font-weight: 600; + color: var(--bs-body-color); + flex-shrink: 0; + } + + .comment-text { + color: var(--bs-secondary-color); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + .more-comments { + font-size: 0.75rem; + color: var(--bs-primary); + cursor: pointer; + text-decoration: underline; + margin-top: 0.25rem; + + &:hover { + color: var(--bs-primary-dark); + } + } +} + +// Comments modal specific styles +.comments-modal { + .card-info { + margin-bottom: 1.5rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--bs-card-border); + + h4 { + margin-bottom: 0.5rem; + color: var(--bs-body-color); + } + } + + .comments-section { + h5 { + margin-bottom: 1rem; + color: var(--bs-body-color); + + .comment-count { + color: var(--bs-secondary-color); + font-weight: normal; + } + } + } + + .comments-list-modal { + .comment-item-modal { + padding: 1rem; + border: 1px solid var(--bs-card-border); + border-radius: 6px; + margin-bottom: 1rem; + background-color: var(--bs-card-bg); + + .comment-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; + + .comment-author { + display: flex; + align-items: center; + gap: 0.5rem; + + .author-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + background-color: var(--bs-primary); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: 600; + } + + .author-name { + font-weight: 600; + color: var(--bs-body-color); + } + } + + .comment-date { + font-size: 0.875rem; + color: var(--bs-secondary-color); + } + } + + .comment-content { + color: var(--bs-body-color); + line-height: 1.5; + } + } + } + + .no-comments { + text-align: center; + padding: 2rem; + + p { + margin: 0; + font-style: italic; + } + } +} + +// Responsive modal adjustments +@media (max-width: 768px) { + .modal-content { + width: 98%; + margin: 0.5rem; + max-width: none; + } + + .modal-header, + .modal-body, + .modal-footer { + padding: 1rem; + } +} \ No newline at end of file