Compare commits
8 Commits
feature/sq
...
bugfix/wee
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
970b0a07da | ||
|
|
e2e5813b54 | ||
|
|
73567c185c | ||
| 0587762bbb | |||
|
|
cd6e9c3619 | ||
|
|
d8c3997f24 | ||
|
|
e44d49e190 | ||
| 4d2aad404b |
@@ -38,4 +38,4 @@ calendar.db
|
|||||||
**/tests/
|
**/tests/
|
||||||
|
|
||||||
# Migrations (not needed for builds)
|
# Migrations (not needed for builds)
|
||||||
migrations/
|
migrations/
|
||||||
|
|||||||
21
Dockerfile
21
Dockerfile
@@ -47,6 +47,9 @@ FROM rust:alpine AS backend-builder
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN apk add --no-cache musl-dev pkgconfig openssl-dev openssl-libs-static
|
RUN apk add --no-cache musl-dev pkgconfig openssl-dev openssl-libs-static
|
||||||
|
|
||||||
|
# Install sqlx-cli for migrations
|
||||||
|
RUN cargo install sqlx-cli --no-default-features --features sqlite
|
||||||
|
|
||||||
# Copy shared models
|
# Copy shared models
|
||||||
COPY calendar-models ./calendar-models
|
COPY calendar-models ./calendar-models
|
||||||
|
|
||||||
@@ -76,19 +79,29 @@ RUN cargo build --release --bin backend
|
|||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
|
|
||||||
# Install runtime dependencies
|
# Install runtime dependencies
|
||||||
RUN apk add --no-cache ca-certificates tzdata
|
RUN apk add --no-cache ca-certificates tzdata sqlite
|
||||||
|
|
||||||
# Copy frontend files to temporary location
|
# Copy frontend files to temporary location
|
||||||
COPY --from=builder /app/frontend/dist /app/frontend-dist
|
COPY --from=builder /app/frontend/dist /app/frontend-dist
|
||||||
|
|
||||||
# Copy backend binary (built in workspace root)
|
# Copy backend binary and sqlx-cli
|
||||||
COPY --from=backend-builder /app/target/release/backend /usr/local/bin/backend
|
COPY --from=backend-builder /app/target/release/backend /usr/local/bin/backend
|
||||||
|
COPY --from=backend-builder /usr/local/cargo/bin/sqlx /usr/local/bin/sqlx
|
||||||
|
|
||||||
# Create startup script to copy frontend files to shared volume
|
# Copy migrations for database setup
|
||||||
RUN mkdir -p /srv/www
|
COPY --from=backend-builder /app/backend/migrations /migrations
|
||||||
|
|
||||||
|
# Create startup script to copy frontend files, run migrations, and start backend
|
||||||
|
RUN mkdir -p /srv/www /db
|
||||||
RUN echo '#!/bin/sh' > /usr/local/bin/start.sh && \
|
RUN echo '#!/bin/sh' > /usr/local/bin/start.sh && \
|
||||||
|
echo 'echo "Copying frontend files..."' >> /usr/local/bin/start.sh && \
|
||||||
echo 'cp -r /app/frontend-dist/* /srv/www/' >> /usr/local/bin/start.sh && \
|
echo 'cp -r /app/frontend-dist/* /srv/www/' >> /usr/local/bin/start.sh && \
|
||||||
|
echo 'echo "Ensuring database directory exists..."' >> /usr/local/bin/start.sh && \
|
||||||
|
echo 'mkdir -p /db && chmod 755 /db' >> /usr/local/bin/start.sh && \
|
||||||
|
echo 'echo "Running database migrations..."' >> /usr/local/bin/start.sh && \
|
||||||
|
echo 'sqlx migrate run --database-url "sqlite:///db/calendar.db" --source /migrations || echo "Migration failed but continuing..."' >> /usr/local/bin/start.sh && \
|
||||||
echo 'echo "Starting backend server..."' >> /usr/local/bin/start.sh && \
|
echo 'echo "Starting backend server..."' >> /usr/local/bin/start.sh && \
|
||||||
|
echo 'export DATABASE_URL="sqlite:///db/calendar.db"' >> /usr/local/bin/start.sh && \
|
||||||
echo '/usr/local/bin/backend' >> /usr/local/bin/start.sh && \
|
echo '/usr/local/bin/backend' >> /usr/local/bin/start.sh && \
|
||||||
chmod +x /usr/local/bin/start.sh
|
chmod +x /usr/local/bin/start.sh
|
||||||
|
|
||||||
|
|||||||
48
README.md
48
README.md
@@ -29,6 +29,12 @@ While there are many excellent self-hosted CalDAV server implementations (Nextcl
|
|||||||
- **Real-time Updates**: Seamless synchronization with CalDAV servers
|
- **Real-time Updates**: Seamless synchronization with CalDAV servers
|
||||||
- **Timezone Aware**: Proper local time display with UTC storage
|
- **Timezone Aware**: Proper local time display with UTC storage
|
||||||
|
|
||||||
|
### User Experience
|
||||||
|
- **Persistent Preferences**: Settings sync across devices and sessions
|
||||||
|
- **Remember Me**: Optional server/username remembering for convenience
|
||||||
|
- **Session Management**: Secure session tokens with automatic expiry
|
||||||
|
- **Cross-Device Sync**: User preferences stored in database, not just browser
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Frontend (Yew WebAssembly)
|
### Frontend (Yew WebAssembly)
|
||||||
@@ -40,7 +46,8 @@ While there are many excellent self-hosted CalDAV server implementations (Nextcl
|
|||||||
|
|
||||||
### Backend (Axum)
|
### Backend (Axum)
|
||||||
- **Framework**: Axum async web framework with CORS support
|
- **Framework**: Axum async web framework with CORS support
|
||||||
- **Authentication**: JWT token management and validation
|
- **Authentication**: SQLite-backed session management with JWT tokens
|
||||||
|
- **Database**: SQLite for user preferences and session storage
|
||||||
- **CalDAV Client**: Full CalDAV protocol implementation for server sync
|
- **CalDAV Client**: Full CalDAV protocol implementation for server sync
|
||||||
- **API Design**: RESTful endpoints following calendar operation patterns
|
- **API Design**: RESTful endpoints following calendar operation patterns
|
||||||
- **Data Flow**: Proxy between frontend and CalDAV servers with proper authentication
|
- **Data Flow**: Proxy between frontend and CalDAV servers with proper authentication
|
||||||
@@ -54,12 +61,36 @@ While there are many excellent self-hosted CalDAV server implementations (Nextcl
|
|||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### Prerequisites
|
### Docker Deployment (Recommended)
|
||||||
|
|
||||||
|
The easiest way to run the calendar is using Docker Compose:
|
||||||
|
|
||||||
|
1. **Clone the repository**:
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd calendar
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Start the application**:
|
||||||
|
```bash
|
||||||
|
docker compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Access the application** at `http://localhost`
|
||||||
|
|
||||||
|
The Docker setup includes:
|
||||||
|
- **Automatic database migrations** on startup
|
||||||
|
- **Persistent data storage** in `./data/db/` volume
|
||||||
|
- **Frontend served via Caddy** on port 80
|
||||||
|
- **Backend API** accessible on port 3000
|
||||||
|
|
||||||
|
### Development Setup
|
||||||
|
|
||||||
|
#### Prerequisites
|
||||||
- Rust (latest stable version)
|
- Rust (latest stable version)
|
||||||
- Trunk (`cargo install trunk`)
|
- Trunk (`cargo install trunk`)
|
||||||
|
|
||||||
### Development Setup
|
#### Local Development
|
||||||
|
|
||||||
1. **Start the backend server** (serves API at http://localhost:3000):
|
1. **Start the backend server** (serves API at http://localhost:3000):
|
||||||
```bash
|
```bash
|
||||||
@@ -73,6 +104,17 @@ While there are many excellent self-hosted CalDAV server implementations (Nextcl
|
|||||||
|
|
||||||
3. **Access the application** at `http://localhost:8080`
|
3. **Access the application** at `http://localhost:8080`
|
||||||
|
|
||||||
|
#### Database Setup
|
||||||
|
|
||||||
|
For local development, run the database migrations:
|
||||||
|
```bash
|
||||||
|
# Install sqlx-cli if not already installed
|
||||||
|
cargo install sqlx-cli --features sqlite
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
sqlx migrate run --database-url "sqlite:calendar.db" --source backend/migrations
|
||||||
|
```
|
||||||
|
|
||||||
### Building for Production
|
### Building for Production
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
2
backend/migrations/004_add_style_preference.sql
Normal file
2
backend/migrations/004_add_style_preference.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- Add calendar style preference to user preferences
|
||||||
|
ALTER TABLE user_preferences ADD COLUMN calendar_style TEXT DEFAULT 'default';
|
||||||
@@ -91,6 +91,7 @@ impl AuthService {
|
|||||||
calendar_time_increment: preferences.calendar_time_increment,
|
calendar_time_increment: preferences.calendar_time_increment,
|
||||||
calendar_view_mode: preferences.calendar_view_mode,
|
calendar_view_mode: preferences.calendar_view_mode,
|
||||||
calendar_theme: preferences.calendar_theme,
|
calendar_theme: preferences.calendar_theme,
|
||||||
|
calendar_style: preferences.calendar_style,
|
||||||
calendar_colors: preferences.calendar_colors,
|
calendar_colors: preferences.calendar_colors,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ pub struct UserPreferences {
|
|||||||
pub calendar_time_increment: Option<i32>,
|
pub calendar_time_increment: Option<i32>,
|
||||||
pub calendar_view_mode: Option<String>,
|
pub calendar_view_mode: Option<String>,
|
||||||
pub calendar_theme: Option<String>,
|
pub calendar_theme: Option<String>,
|
||||||
|
pub calendar_style: Option<String>,
|
||||||
pub calendar_colors: Option<String>, // JSON string
|
pub calendar_colors: Option<String>, // JSON string
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
@@ -106,6 +107,7 @@ impl UserPreferences {
|
|||||||
calendar_time_increment: Some(15),
|
calendar_time_increment: Some(15),
|
||||||
calendar_view_mode: Some("month".to_string()),
|
calendar_view_mode: Some("month".to_string()),
|
||||||
calendar_theme: Some("light".to_string()),
|
calendar_theme: Some("light".to_string()),
|
||||||
|
calendar_style: Some("default".to_string()),
|
||||||
calendar_colors: None,
|
calendar_colors: None,
|
||||||
updated_at: Utc::now(),
|
updated_at: Utc::now(),
|
||||||
}
|
}
|
||||||
@@ -264,14 +266,15 @@ impl<'a> PreferencesRepository<'a> {
|
|||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO user_preferences
|
"INSERT INTO user_preferences
|
||||||
(user_id, calendar_selected_date, calendar_time_increment,
|
(user_id, calendar_selected_date, calendar_time_increment,
|
||||||
calendar_view_mode, calendar_theme, calendar_colors, updated_at)
|
calendar_view_mode, calendar_theme, calendar_style, calendar_colors, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)",
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
)
|
)
|
||||||
.bind(&prefs.user_id)
|
.bind(&prefs.user_id)
|
||||||
.bind(&prefs.calendar_selected_date)
|
.bind(&prefs.calendar_selected_date)
|
||||||
.bind(&prefs.calendar_time_increment)
|
.bind(&prefs.calendar_time_increment)
|
||||||
.bind(&prefs.calendar_view_mode)
|
.bind(&prefs.calendar_view_mode)
|
||||||
.bind(&prefs.calendar_theme)
|
.bind(&prefs.calendar_theme)
|
||||||
|
.bind(&prefs.calendar_style)
|
||||||
.bind(&prefs.calendar_colors)
|
.bind(&prefs.calendar_colors)
|
||||||
.bind(&prefs.updated_at)
|
.bind(&prefs.updated_at)
|
||||||
.execute(self.db.pool())
|
.execute(self.db.pool())
|
||||||
@@ -286,7 +289,7 @@ impl<'a> PreferencesRepository<'a> {
|
|||||||
sqlx::query(
|
sqlx::query(
|
||||||
"UPDATE user_preferences
|
"UPDATE user_preferences
|
||||||
SET calendar_selected_date = ?, calendar_time_increment = ?,
|
SET calendar_selected_date = ?, calendar_time_increment = ?,
|
||||||
calendar_view_mode = ?, calendar_theme = ?,
|
calendar_view_mode = ?, calendar_theme = ?, calendar_style = ?,
|
||||||
calendar_colors = ?, updated_at = ?
|
calendar_colors = ?, updated_at = ?
|
||||||
WHERE user_id = ?",
|
WHERE user_id = ?",
|
||||||
)
|
)
|
||||||
@@ -294,6 +297,7 @@ impl<'a> PreferencesRepository<'a> {
|
|||||||
.bind(&prefs.calendar_time_increment)
|
.bind(&prefs.calendar_time_increment)
|
||||||
.bind(&prefs.calendar_view_mode)
|
.bind(&prefs.calendar_view_mode)
|
||||||
.bind(&prefs.calendar_theme)
|
.bind(&prefs.calendar_theme)
|
||||||
|
.bind(&prefs.calendar_style)
|
||||||
.bind(&prefs.calendar_colors)
|
.bind(&prefs.calendar_colors)
|
||||||
.bind(Utc::now())
|
.bind(Utc::now())
|
||||||
.bind(&prefs.user_id)
|
.bind(&prefs.user_id)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use axum::{extract::State, http::HeaderMap, response::Json};
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::calendar::CalDAVClient;
|
use crate::calendar::CalDAVClient;
|
||||||
use crate::config::CalDAVConfig;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
models::{ApiError, AuthResponse, CalDAVLoginRequest, CalendarInfo, UserInfo},
|
models::{ApiError, AuthResponse, CalDAVLoginRequest, CalendarInfo, UserInfo},
|
||||||
AppState,
|
AppState,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ pub async fn get_preferences(
|
|||||||
calendar_time_increment: preferences.calendar_time_increment,
|
calendar_time_increment: preferences.calendar_time_increment,
|
||||||
calendar_view_mode: preferences.calendar_view_mode,
|
calendar_view_mode: preferences.calendar_view_mode,
|
||||||
calendar_theme: preferences.calendar_theme,
|
calendar_theme: preferences.calendar_theme,
|
||||||
|
calendar_style: preferences.calendar_style,
|
||||||
calendar_colors: preferences.calendar_colors,
|
calendar_colors: preferences.calendar_colors,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -78,6 +79,9 @@ pub async fn update_preferences(
|
|||||||
if request.calendar_theme.is_some() {
|
if request.calendar_theme.is_some() {
|
||||||
preferences.calendar_theme = request.calendar_theme;
|
preferences.calendar_theme = request.calendar_theme;
|
||||||
}
|
}
|
||||||
|
if request.calendar_style.is_some() {
|
||||||
|
preferences.calendar_style = request.calendar_style;
|
||||||
|
}
|
||||||
if request.calendar_colors.is_some() {
|
if request.calendar_colors.is_some() {
|
||||||
preferences.calendar_colors = request.calendar_colors;
|
preferences.calendar_colors = request.calendar_colors;
|
||||||
}
|
}
|
||||||
@@ -94,6 +98,7 @@ pub async fn update_preferences(
|
|||||||
calendar_time_increment: preferences.calendar_time_increment,
|
calendar_time_increment: preferences.calendar_time_increment,
|
||||||
calendar_view_mode: preferences.calendar_view_mode,
|
calendar_view_mode: preferences.calendar_view_mode,
|
||||||
calendar_theme: preferences.calendar_theme,
|
calendar_theme: preferences.calendar_theme,
|
||||||
|
calendar_style: preferences.calendar_style,
|
||||||
calendar_colors: preferences.calendar_colors,
|
calendar_colors: preferences.calendar_colors,
|
||||||
}),
|
}),
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ pub struct UserPreferencesResponse {
|
|||||||
pub calendar_time_increment: Option<i32>,
|
pub calendar_time_increment: Option<i32>,
|
||||||
pub calendar_view_mode: Option<String>,
|
pub calendar_view_mode: Option<String>,
|
||||||
pub calendar_theme: Option<String>,
|
pub calendar_theme: Option<String>,
|
||||||
|
pub calendar_style: Option<String>,
|
||||||
pub calendar_colors: Option<String>,
|
pub calendar_colors: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ pub struct UpdatePreferencesRequest {
|
|||||||
pub calendar_time_increment: Option<i32>,
|
pub calendar_time_increment: Option<i32>,
|
||||||
pub calendar_view_mode: Option<String>,
|
pub calendar_view_mode: Option<String>,
|
||||||
pub calendar_theme: Option<String>,
|
pub calendar_theme: Option<String>,
|
||||||
|
pub calendar_style: Option<String>,
|
||||||
pub calendar_colors: Option<String>,
|
pub calendar_colors: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
calendar.db
BIN
calendar.db
Binary file not shown.
@@ -1,17 +1,16 @@
|
|||||||
services:
|
services:
|
||||||
calendar-backend:
|
calendar-backend:
|
||||||
build: .
|
build: .
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/site_dist:/srv/www
|
- ./data/site_dist:/srv/www
|
||||||
|
- ./data/db:/db
|
||||||
|
|
||||||
calendar-frontend:
|
calendar-frontend:
|
||||||
image: caddy
|
image: caddy
|
||||||
env_file:
|
environment:
|
||||||
- .env
|
- BACKEND_API_URL=http://localhost:3000/api
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ web-sys = { version = "0.3", features = [
|
|||||||
"HtmlSelectElement",
|
"HtmlSelectElement",
|
||||||
"HtmlInputElement",
|
"HtmlInputElement",
|
||||||
"HtmlTextAreaElement",
|
"HtmlTextAreaElement",
|
||||||
|
"HtmlLinkElement",
|
||||||
|
"HtmlHeadElement",
|
||||||
"Event",
|
"Event",
|
||||||
"MouseEvent",
|
"MouseEvent",
|
||||||
"InputEvent",
|
"InputEvent",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<base data-trunk-public-url />
|
<base data-trunk-public-url />
|
||||||
<link data-trunk rel="css" href="styles.css">
|
<link data-trunk rel="css" href="styles.css">
|
||||||
|
<link data-trunk rel="copy-file" href="styles/google.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ use crate::components::{
|
|||||||
EditAction, EventClass, EventContextMenu, EventCreationData, EventStatus, RecurrenceType,
|
EditAction, EventClass, EventContextMenu, EventCreationData, EventStatus, RecurrenceType,
|
||||||
ReminderType, RouteHandler, Sidebar, Theme, ViewMode,
|
ReminderType, RouteHandler, Sidebar, Theme, ViewMode,
|
||||||
};
|
};
|
||||||
|
use crate::components::sidebar::{Style};
|
||||||
use crate::models::ical::VEvent;
|
use crate::models::ical::VEvent;
|
||||||
use crate::services::{calendar_service::UserInfo, CalendarService};
|
use crate::services::{calendar_service::UserInfo, CalendarService};
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use gloo_storage::{LocalStorage, Storage};
|
use gloo_storage::{LocalStorage, Storage};
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
use web_sys::MouseEvent;
|
use web_sys::MouseEvent;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew_router::prelude::*;
|
use yew_router::prelude::*;
|
||||||
@@ -96,6 +98,16 @@ pub fn App() -> Html {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Style state - load from localStorage if available
|
||||||
|
let current_style = use_state(|| {
|
||||||
|
// Try to load saved style from localStorage
|
||||||
|
if let Ok(saved_style) = LocalStorage::get::<String>("calendar_style") {
|
||||||
|
Style::from_value(&saved_style)
|
||||||
|
} else {
|
||||||
|
Style::Default // Default style
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let available_colors = use_state(|| get_theme_event_colors());
|
let available_colors = use_state(|| get_theme_event_colors());
|
||||||
|
|
||||||
let on_login = {
|
let on_login = {
|
||||||
@@ -152,6 +164,42 @@ pub fn App() -> Html {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let on_style_change = {
|
||||||
|
let current_style = current_style.clone();
|
||||||
|
Callback::from(move |new_style: Style| {
|
||||||
|
// Save style to localStorage
|
||||||
|
let _ = LocalStorage::set("calendar_style", new_style.value());
|
||||||
|
|
||||||
|
// Hot-swap stylesheet
|
||||||
|
if let Some(window) = web_sys::window() {
|
||||||
|
if let Some(document) = window.document() {
|
||||||
|
// Remove existing style link if it exists
|
||||||
|
if let Some(existing_link) = document.get_element_by_id("dynamic-style") {
|
||||||
|
existing_link.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and append new stylesheet link only if style has a path
|
||||||
|
if let Some(stylesheet_path) = new_style.stylesheet_path() {
|
||||||
|
if let Ok(link) = document.create_element("link") {
|
||||||
|
let link = link.dyn_into::<web_sys::HtmlLinkElement>().unwrap();
|
||||||
|
link.set_id("dynamic-style");
|
||||||
|
link.set_rel("stylesheet");
|
||||||
|
link.set_href(stylesheet_path);
|
||||||
|
|
||||||
|
if let Some(head) = document.head() {
|
||||||
|
let _ = head.append_child(&link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If stylesheet_path is None (Default style), just removing the dynamic link is enough
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
current_style.set(new_style);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
// Apply initial theme on mount
|
// Apply initial theme on mount
|
||||||
{
|
{
|
||||||
let current_theme = current_theme.clone();
|
let current_theme = current_theme.clone();
|
||||||
@@ -165,6 +213,32 @@ pub fn App() -> Html {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply initial style on mount
|
||||||
|
{
|
||||||
|
let current_style = current_style.clone();
|
||||||
|
use_effect_with((), move |_| {
|
||||||
|
let style = (*current_style).clone();
|
||||||
|
if let Some(window) = web_sys::window() {
|
||||||
|
if let Some(document) = window.document() {
|
||||||
|
// Create and append stylesheet link for initial style only if it has a path
|
||||||
|
if let Some(stylesheet_path) = style.stylesheet_path() {
|
||||||
|
if let Ok(link) = document.create_element("link") {
|
||||||
|
let link = link.dyn_into::<web_sys::HtmlLinkElement>().unwrap();
|
||||||
|
link.set_id("dynamic-style");
|
||||||
|
link.set_rel("stylesheet");
|
||||||
|
link.set_href(stylesheet_path);
|
||||||
|
|
||||||
|
if let Some(head) = document.head() {
|
||||||
|
let _ = head.append_child(&link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If initial style is Default (None), no additional stylesheet needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch user info when token is available
|
// Fetch user info when token is available
|
||||||
{
|
{
|
||||||
let user_info = user_info.clone();
|
let user_info = user_info.clone();
|
||||||
@@ -718,6 +792,8 @@ pub fn App() -> Html {
|
|||||||
on_view_change={on_view_change}
|
on_view_change={on_view_change}
|
||||||
current_theme={(*current_theme).clone()}
|
current_theme={(*current_theme).clone()}
|
||||||
on_theme_change={on_theme_change}
|
on_theme_change={on_theme_change}
|
||||||
|
current_style={(*current_style).clone()}
|
||||||
|
on_style_change={on_style_change}
|
||||||
/>
|
/>
|
||||||
<main class="app-main">
|
<main class="app-main">
|
||||||
<RouteHandler
|
<RouteHandler
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ pub enum Theme {
|
|||||||
Mint,
|
Mint,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub enum Style {
|
||||||
|
Default,
|
||||||
|
Google,
|
||||||
|
}
|
||||||
|
|
||||||
impl Theme {
|
impl Theme {
|
||||||
pub fn value(&self) -> &'static str {
|
pub fn value(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
@@ -60,6 +66,30 @@ impl Theme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Style {
|
||||||
|
pub fn value(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Style::Default => "default",
|
||||||
|
Style::Google => "google",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_value(value: &str) -> Self {
|
||||||
|
match value {
|
||||||
|
"google" => Style::Google,
|
||||||
|
_ => Style::Default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn stylesheet_path(&self) -> Option<&'static str> {
|
||||||
|
match self {
|
||||||
|
Style::Default => None, // No additional stylesheet needed - uses base styles.css
|
||||||
|
Style::Google => Some("google.css"), // Trunk copies to root level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for ViewMode {
|
impl Default for ViewMode {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
ViewMode::Month
|
ViewMode::Month
|
||||||
@@ -80,6 +110,8 @@ pub struct SidebarProps {
|
|||||||
pub on_view_change: Callback<ViewMode>,
|
pub on_view_change: Callback<ViewMode>,
|
||||||
pub current_theme: Theme,
|
pub current_theme: Theme,
|
||||||
pub on_theme_change: Callback<Theme>,
|
pub on_theme_change: Callback<Theme>,
|
||||||
|
pub current_style: Style,
|
||||||
|
pub on_style_change: Callback<Style>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component(Sidebar)]
|
#[function_component(Sidebar)]
|
||||||
@@ -111,6 +143,18 @@ pub fn sidebar(props: &SidebarProps) -> Html {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let on_style_change = {
|
||||||
|
let on_style_change = props.on_style_change.clone();
|
||||||
|
Callback::from(move |e: Event| {
|
||||||
|
let target = e.target_dyn_into::<HtmlSelectElement>();
|
||||||
|
if let Some(select) = target {
|
||||||
|
let value = select.value();
|
||||||
|
let new_style = Style::from_value(&value);
|
||||||
|
on_style_change.emit(new_style);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<aside class="app-sidebar">
|
<aside class="app-sidebar">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
@@ -175,6 +219,7 @@ pub fn sidebar(props: &SidebarProps) -> Html {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="theme-selector">
|
<div class="theme-selector">
|
||||||
|
<label>{"Theme:"}</label>
|
||||||
<select class="theme-selector-dropdown" onchange={on_theme_change}>
|
<select class="theme-selector-dropdown" onchange={on_theme_change}>
|
||||||
<option value="default" selected={matches!(props.current_theme, Theme::Default)}>{"Default"}</option>
|
<option value="default" selected={matches!(props.current_theme, Theme::Default)}>{"Default"}</option>
|
||||||
<option value="ocean" selected={matches!(props.current_theme, Theme::Ocean)}>{"Ocean"}</option>
|
<option value="ocean" selected={matches!(props.current_theme, Theme::Ocean)}>{"Ocean"}</option>
|
||||||
@@ -187,6 +232,14 @@ pub fn sidebar(props: &SidebarProps) -> Html {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="style-selector">
|
||||||
|
<label>{"Style:"}</label>
|
||||||
|
<select class="style-selector-dropdown" onchange={on_style_change}>
|
||||||
|
<option value="default" selected={matches!(props.current_style, Style::Default)}>{"Default"}</option>
|
||||||
|
<option value="google" selected={matches!(props.current_style, Style::Google)}>{"Google Calendar"}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button onclick={props.on_logout.reform(|_| ())} class="logout-button">{"Logout"}</button>
|
<button onclick={props.on_logout.reform(|_| ())} class="logout-button">{"Logout"}</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -585,11 +585,6 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
|||||||
}
|
}
|
||||||
}).collect::<Html>()
|
}).collect::<Html>()
|
||||||
}
|
}
|
||||||
// Final boundary slot to complete the 24-hour visual grid - make it interactive like other slots
|
|
||||||
<div class="time-slot boundary-slot">
|
|
||||||
<div class="time-slot-half"></div>
|
|
||||||
<div class="time-slot-half"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
// Events positioned absolutely based on their actual times
|
// Events positioned absolutely based on their actual times
|
||||||
<div class="events-container">
|
<div class="events-container">
|
||||||
@@ -1029,8 +1024,13 @@ fn calculate_event_position(event: &VEvent, date: NaiveDate) -> (f32, f32, bool)
|
|||||||
let local_start = event.dtstart.with_timezone(&Local);
|
let local_start = event.dtstart.with_timezone(&Local);
|
||||||
let event_date = local_start.date_naive();
|
let event_date = local_start.date_naive();
|
||||||
|
|
||||||
// Only position events that are on this specific date
|
// Position events based on when they appear in local time, not their original date
|
||||||
if event_date != date {
|
// For timezone issues: an event created at 10 PM Sunday might be stored as Monday UTC
|
||||||
|
// but should still display on Sunday's column since that's when the user sees it
|
||||||
|
let should_display_here = event_date == date ||
|
||||||
|
(event_date == date - chrono::Duration::days(1) && local_start.hour() >= 20);
|
||||||
|
|
||||||
|
if !should_display_here {
|
||||||
return (0.0, 0.0, false); // Event not on this date
|
return (0.0, 0.0, false); // Event not on this date
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,4 +2,3 @@ pub mod calendar_service;
|
|||||||
pub mod preferences;
|
pub mod preferences;
|
||||||
|
|
||||||
pub use calendar_service::CalendarService;
|
pub use calendar_service::CalendarService;
|
||||||
pub use preferences::PreferencesService;
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ pub struct UserPreferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub struct UpdatePreferencesRequest {
|
pub struct UpdatePreferencesRequest {
|
||||||
pub calendar_selected_date: Option<String>,
|
pub calendar_selected_date: Option<String>,
|
||||||
pub calendar_time_increment: Option<i32>,
|
pub calendar_time_increment: Option<i32>,
|
||||||
@@ -23,10 +24,12 @@ pub struct UpdatePreferencesRequest {
|
|||||||
pub calendar_colors: Option<String>,
|
pub calendar_colors: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub struct PreferencesService {
|
pub struct PreferencesService {
|
||||||
base_url: String,
|
base_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
impl PreferencesService {
|
impl PreferencesService {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let base_url = option_env!("BACKEND_API_URL")
|
let base_url = option_env!("BACKEND_API_URL")
|
||||||
@@ -147,7 +150,7 @@ impl PreferencesService {
|
|||||||
let session_token = LocalStorage::get::<String>("session_token")
|
let session_token = LocalStorage::get::<String>("session_token")
|
||||||
.map_err(|_| "No session token found".to_string())?;
|
.map_err(|_| "No session token found".to_string())?;
|
||||||
|
|
||||||
let mut request = UpdatePreferencesRequest {
|
let request = UpdatePreferencesRequest {
|
||||||
calendar_selected_date: LocalStorage::get::<String>("calendar_selected_date").ok(),
|
calendar_selected_date: LocalStorage::get::<String>("calendar_selected_date").ok(),
|
||||||
calendar_time_increment: LocalStorage::get::<u32>("calendar_time_increment").ok().map(|i| i as i32),
|
calendar_time_increment: LocalStorage::get::<u32>("calendar_time_increment").ok().map(|i| i as i32),
|
||||||
calendar_view_mode: LocalStorage::get::<String>("calendar_view_mode").ok(),
|
calendar_view_mode: LocalStorage::get::<String>("calendar_view_mode").ok(),
|
||||||
|
|||||||
@@ -1,9 +1,120 @@
|
|||||||
|
/* Base Styles - Always Loaded */
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* CSS Variables for Style System */
|
||||||
|
--border-radius-small: 4px;
|
||||||
|
--border-radius-medium: 8px;
|
||||||
|
--border-radius-large: 12px;
|
||||||
|
--spacing-xs: 4px;
|
||||||
|
--spacing-sm: 8px;
|
||||||
|
--spacing-md: 16px;
|
||||||
|
--spacing-lg: 24px;
|
||||||
|
--spacing-xl: 32px;
|
||||||
|
--shadow-sm: 0 1px 3px rgba(0,0,0,0.1);
|
||||||
|
--shadow-md: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
|
--shadow-lg: 0 8px 25px rgba(0,0,0,0.15);
|
||||||
|
--border-light: 1px solid #e9ecef;
|
||||||
|
--border-medium: 1px solid #dee2e6;
|
||||||
|
--transition-fast: 0.15s ease;
|
||||||
|
--transition-normal: 0.2s ease;
|
||||||
|
--transition-slow: 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-layout {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base Layout */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 280px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Basic Form Elements */
|
||||||
|
input, select, textarea, button {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility Classes */
|
||||||
|
.loading {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme Data Attributes for Color Schemes */
|
||||||
|
[data-theme="default"] {
|
||||||
|
--primary-color: #667eea;
|
||||||
|
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="ocean"] {
|
||||||
|
--primary-color: #006994;
|
||||||
|
--primary-gradient: linear-gradient(135deg, #006994 0%, #0891b2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="forest"] {
|
||||||
|
--primary-color: #065f46;
|
||||||
|
--primary-gradient: linear-gradient(135deg, #065f46 0%, #047857 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="sunset"] {
|
||||||
|
--primary-color: #ea580c;
|
||||||
|
--primary-gradient: linear-gradient(135deg, #ea580c 0%, #dc2626 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="purple"] {
|
||||||
|
--primary-color: #7c3aed;
|
||||||
|
--primary-gradient: linear-gradient(135deg, #7c3aed 0%, #a855f7 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--primary-color: #374151;
|
||||||
|
--primary-gradient: linear-gradient(135deg, #374151 0%, #1f2937 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="rose"] {
|
||||||
|
--primary-color: #e11d48;
|
||||||
|
--primary-gradient: linear-gradient(135deg, #e11d48 0%, #f43f5e 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="mint"] {
|
||||||
|
--primary-color: #10b981;
|
||||||
|
--primary-gradient: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||||
|
}* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
@@ -574,12 +685,13 @@ body {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
min-height: 0; /* Allow flex item to shrink below content size */
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-grid {
|
.time-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 80px 1fr;
|
grid-template-columns: 80px 1fr;
|
||||||
min-height: 100%;
|
min-height: 1530px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Time Labels */
|
/* Time Labels */
|
||||||
@@ -589,6 +701,7 @@ body {
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
|
min-height: 1440px; /* Match the time slots height */
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-label {
|
.time-label {
|
||||||
@@ -614,12 +727,13 @@ body {
|
|||||||
.week-days-grid {
|
.week-days-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(7, 1fr);
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
min-height: 1440px; /* Ensure grid is tall enough for 24 time slots */
|
||||||
}
|
}
|
||||||
|
|
||||||
.week-day-column {
|
.week-day-column {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-right: 1px solid var(--time-label-border, #e9ecef);
|
border-right: 1px solid var(--time-label-border, #e9ecef);
|
||||||
min-height: 1500px; /* 25 time labels × 60px = 1500px total */
|
min-height: 1440px; /* 24 time slots × 60px = 1440px total */
|
||||||
}
|
}
|
||||||
|
|
||||||
.week-day-column:last-child {
|
.week-day-column:last-child {
|
||||||
@@ -3014,6 +3128,50 @@ body {
|
|||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Style Selector Styles */
|
||||||
|
.style-selector {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-selector label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-selector-dropdown {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: white;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-selector-dropdown:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-selector-dropdown:focus {
|
||||||
|
outline: none;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-color: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-selector-dropdown option {
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Theme Definitions */
|
/* Theme Definitions */
|
||||||
:root {
|
:root {
|
||||||
/* Default Theme */
|
/* Default Theme */
|
||||||
|
|||||||
3501
frontend/styles.css.backup
Normal file
3501
frontend/styles.css.backup
Normal file
File diff suppressed because it is too large
Load Diff
51
frontend/styles/base.css
Normal file
51
frontend/styles/base.css
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/* Base Styles - Always Loaded */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-layout {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base Layout */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 280px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Basic Form Elements */
|
||||||
|
input, select, textarea, button {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility Classes */
|
||||||
|
.loading {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
3501
frontend/styles/default.css
Normal file
3501
frontend/styles/default.css
Normal file
File diff suppressed because it is too large
Load Diff
645
frontend/styles/google.css
Normal file
645
frontend/styles/google.css
Normal file
@@ -0,0 +1,645 @@
|
|||||||
|
/* Google Calendar-inspired styles */
|
||||||
|
|
||||||
|
/* Override CSS Variables for Google Calendar Style */
|
||||||
|
:root {
|
||||||
|
/* Google-style spacing */
|
||||||
|
--spacing-xs: 2px;
|
||||||
|
--spacing-sm: 4px;
|
||||||
|
--spacing-md: 8px;
|
||||||
|
--spacing-lg: 12px;
|
||||||
|
--spacing-xl: 16px;
|
||||||
|
|
||||||
|
/* Google-style borders and radius */
|
||||||
|
--border-radius-small: 2px;
|
||||||
|
--border-radius-medium: 4px;
|
||||||
|
--border-radius-large: 8px;
|
||||||
|
--border-light: 1px solid #dadce0;
|
||||||
|
--border-medium: 1px solid #dadce0;
|
||||||
|
|
||||||
|
/* Google-style shadows */
|
||||||
|
--shadow-sm: 0 1px 2px 0 rgba(60,64,67,.3), 0 1px 3px 1px rgba(60,64,67,.15);
|
||||||
|
--shadow-md: 0 1px 3px 0 rgba(60,64,67,.3), 0 4px 8px 3px rgba(60,64,67,.15);
|
||||||
|
--shadow-lg: 0 4px 6px 0 rgba(60,64,67,.3), 0 8px 25px 5px rgba(60,64,67,.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Google-style sidebar - override all theme variants */
|
||||||
|
body .app-sidebar,
|
||||||
|
[data-theme] .app-sidebar,
|
||||||
|
[data-theme="default"] .app-sidebar,
|
||||||
|
[data-theme="ocean"] .app-sidebar,
|
||||||
|
[data-theme="forest"] .app-sidebar,
|
||||||
|
[data-theme="sunset"] .app-sidebar,
|
||||||
|
[data-theme="purple"] .app-sidebar,
|
||||||
|
[data-theme="dark"] .app-sidebar,
|
||||||
|
[data-theme="rose"] .app-sidebar,
|
||||||
|
[data-theme="mint"] .app-sidebar {
|
||||||
|
background: #ffffff !important;
|
||||||
|
border-right: 1px solid #dadce0 !important;
|
||||||
|
font-family: 'Google Sans', 'Roboto', sans-serif !important;
|
||||||
|
box-shadow: 2px 0 8px rgba(60,64,67,.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body .sidebar-header,
|
||||||
|
[data-theme] .sidebar-header {
|
||||||
|
background: transparent !important;
|
||||||
|
border-bottom: 1px solid #dadce0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body .sidebar-header h1,
|
||||||
|
[data-theme] .sidebar-header h1 {
|
||||||
|
font-size: 20px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
color: #3c4043 !important;
|
||||||
|
font-family: 'Google Sans', 'Roboto', sans-serif !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body .user-info,
|
||||||
|
[data-theme] .user-info {
|
||||||
|
color: #3c4043 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body .user-info .username,
|
||||||
|
[data-theme] .user-info .username {
|
||||||
|
font-weight: 500 !important;
|
||||||
|
color: #3c4043 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body .user-info .server-url,
|
||||||
|
[data-theme] .user-info .server-url {
|
||||||
|
color: #5f6368 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Google-style buttons */
|
||||||
|
.create-calendar-button {
|
||||||
|
background: #1a73e8 !important;
|
||||||
|
color: white !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
padding: 8px 16px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
box-shadow: 0 1px 2px 0 rgba(60,64,67,.3), 0 1px 3px 1px rgba(60,64,67,.15) !important;
|
||||||
|
transition: box-shadow 0.2s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-calendar-button:hover {
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(60,64,67,.3), 0 4px 8px 3px rgba(60,64,67,.15) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-button {
|
||||||
|
background: transparent !important;
|
||||||
|
color: #1a73e8 !important;
|
||||||
|
border: 1px solid #dadce0 !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
padding: 8px 16px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
transition: background-color 0.2s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-button:hover {
|
||||||
|
background: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Google-style navigation and sidebar text */
|
||||||
|
body .sidebar-nav .nav-link,
|
||||||
|
[data-theme] .sidebar-nav .nav-link {
|
||||||
|
color: #3c4043 !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body .sidebar-nav .nav-link:hover,
|
||||||
|
[data-theme] .sidebar-nav .nav-link:hover {
|
||||||
|
color: #1a73e8 !important;
|
||||||
|
background: #f1f3f4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar list styling */
|
||||||
|
body .calendar-list h3,
|
||||||
|
[data-theme] .calendar-list h3 {
|
||||||
|
color: #3c4043 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body .calendar-list .calendar-name,
|
||||||
|
[data-theme] .calendar-list .calendar-name {
|
||||||
|
color: #3c4043 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body .no-calendars,
|
||||||
|
[data-theme] .no-calendars {
|
||||||
|
color: #5f6368 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form labels and text */
|
||||||
|
body .sidebar-footer label,
|
||||||
|
[data-theme] .sidebar-footer label,
|
||||||
|
body .view-selector label,
|
||||||
|
[data-theme] .view-selector label,
|
||||||
|
body .theme-selector label,
|
||||||
|
[data-theme] .theme-selector label,
|
||||||
|
body .style-selector label,
|
||||||
|
[data-theme] .style-selector label {
|
||||||
|
color: #3c4043 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Google-style selectors */
|
||||||
|
body .view-selector-dropdown,
|
||||||
|
body .theme-selector-dropdown,
|
||||||
|
body .style-selector-dropdown,
|
||||||
|
[data-theme] .view-selector-dropdown,
|
||||||
|
[data-theme] .theme-selector-dropdown,
|
||||||
|
[data-theme] .style-selector-dropdown {
|
||||||
|
border: 1px solid #dadce0 !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
padding: 8px !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
color: #3c4043 !important;
|
||||||
|
background: white !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-selector-dropdown:focus,
|
||||||
|
.theme-selector-dropdown:focus,
|
||||||
|
.style-selector-dropdown:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #1a73e8;
|
||||||
|
box-shadow: 0 0 0 2px rgba(26,115,232,.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Google-style calendar list items */
|
||||||
|
.calendar-list h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #3c4043;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-list ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-list .calendar-item {
|
||||||
|
padding: 4px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-list .calendar-item:hover {
|
||||||
|
background-color: #f1f3f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-list .calendar-name {
|
||||||
|
color: #3c4043;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Google-style main content area */
|
||||||
|
body .app-main,
|
||||||
|
[data-theme] .app-main {
|
||||||
|
background: #ffffff !important;
|
||||||
|
font-family: 'Google Sans', 'Roboto', sans-serif !important;
|
||||||
|
color: #3c4043 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar header elements */
|
||||||
|
body .calendar-header,
|
||||||
|
[data-theme] .calendar-header {
|
||||||
|
background: #f8f9fa !important;
|
||||||
|
color: #3c4043 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body .calendar-header h2,
|
||||||
|
body .calendar-header h3,
|
||||||
|
body .month-header,
|
||||||
|
body .week-header,
|
||||||
|
[data-theme] .calendar-header h2,
|
||||||
|
[data-theme] .calendar-header h3,
|
||||||
|
[data-theme] .month-header,
|
||||||
|
[data-theme] .week-header {
|
||||||
|
color: #3c4043 !important;
|
||||||
|
font-family: 'Google Sans', 'Roboto', sans-serif !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Month name and title - aggressive override */
|
||||||
|
body h1,
|
||||||
|
body h2,
|
||||||
|
body h3,
|
||||||
|
body .month-title,
|
||||||
|
body .calendar-title,
|
||||||
|
body .current-month,
|
||||||
|
body .month-year,
|
||||||
|
body .header-title,
|
||||||
|
[data-theme] h1,
|
||||||
|
[data-theme] h2,
|
||||||
|
[data-theme] h3,
|
||||||
|
[data-theme] .month-title,
|
||||||
|
[data-theme] .calendar-title,
|
||||||
|
[data-theme] .current-month,
|
||||||
|
[data-theme] .month-year,
|
||||||
|
[data-theme] .header-title {
|
||||||
|
color: #3c4043 !important;
|
||||||
|
font-family: 'Google Sans', 'Roboto', sans-serif !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation arrows and buttons - aggressive override */
|
||||||
|
body button,
|
||||||
|
body .nav-button,
|
||||||
|
body .calendar-nav-button,
|
||||||
|
body .prev-button,
|
||||||
|
body .next-button,
|
||||||
|
body .arrow-button,
|
||||||
|
body .navigation-arrow,
|
||||||
|
body [class*="arrow"],
|
||||||
|
body [class*="nav"],
|
||||||
|
body [class*="button"],
|
||||||
|
[data-theme] button,
|
||||||
|
[data-theme] .nav-button,
|
||||||
|
[data-theme] .calendar-nav-button,
|
||||||
|
[data-theme] .prev-button,
|
||||||
|
[data-theme] .next-button,
|
||||||
|
[data-theme] .arrow-button,
|
||||||
|
[data-theme] .navigation-arrow,
|
||||||
|
[data-theme] [class*="arrow"],
|
||||||
|
[data-theme] [class*="nav"],
|
||||||
|
[data-theme] [class*="button"] {
|
||||||
|
color: #3c4043 !important;
|
||||||
|
background: #f8f9fa !important;
|
||||||
|
border: 1px solid #dadce0 !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body button:hover,
|
||||||
|
body .nav-button:hover,
|
||||||
|
body .calendar-nav-button:hover,
|
||||||
|
body .prev-button:hover,
|
||||||
|
body .next-button:hover,
|
||||||
|
body .arrow-button:hover,
|
||||||
|
[data-theme] button:hover,
|
||||||
|
[data-theme] .nav-button:hover,
|
||||||
|
[data-theme] .calendar-nav-button:hover,
|
||||||
|
[data-theme] .prev-button:hover,
|
||||||
|
[data-theme] .next-button:hover,
|
||||||
|
[data-theme] .arrow-button:hover {
|
||||||
|
background: #e8f0fe !important;
|
||||||
|
color: #1a73e8 !important;
|
||||||
|
border-color: #1a73e8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar controls and date display */
|
||||||
|
body .calendar-controls,
|
||||||
|
body .current-date,
|
||||||
|
body .date-display,
|
||||||
|
[data-theme] .calendar-controls,
|
||||||
|
[data-theme] .current-date,
|
||||||
|
[data-theme] .date-display {
|
||||||
|
color: #3c4043 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ultimate nuclear approach - override EVERYTHING */
|
||||||
|
html body .app-main,
|
||||||
|
html body .app-main *,
|
||||||
|
html body .main-content,
|
||||||
|
html body .main-content *,
|
||||||
|
html body .calendar-container,
|
||||||
|
html body .calendar-container *,
|
||||||
|
html [data-theme] .app-main,
|
||||||
|
html [data-theme] .app-main *,
|
||||||
|
html [data-theme] .main-content,
|
||||||
|
html [data-theme] .main-content *,
|
||||||
|
html [data-theme] .calendar-container,
|
||||||
|
html [data-theme] .calendar-container *,
|
||||||
|
html [data-theme="default"] .app-main *,
|
||||||
|
html [data-theme="ocean"] .app-main *,
|
||||||
|
html [data-theme="forest"] .app-main *,
|
||||||
|
html [data-theme="sunset"] .app-main *,
|
||||||
|
html [data-theme="purple"] .app-main *,
|
||||||
|
html [data-theme="dark"] .app-main *,
|
||||||
|
html [data-theme="rose"] .app-main *,
|
||||||
|
html [data-theme="mint"] .app-main * {
|
||||||
|
color: #3c4043 !important;
|
||||||
|
text-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force all text elements */
|
||||||
|
html body .app-main h1,
|
||||||
|
html body .app-main h2,
|
||||||
|
html body .app-main h3,
|
||||||
|
html body .app-main h4,
|
||||||
|
html body .app-main h5,
|
||||||
|
html body .app-main h6,
|
||||||
|
html body .app-main p,
|
||||||
|
html body .app-main span,
|
||||||
|
html body .app-main div,
|
||||||
|
html body .app-main button,
|
||||||
|
html [data-theme] .app-main h1,
|
||||||
|
html [data-theme] .app-main h2,
|
||||||
|
html [data-theme] .app-main h3,
|
||||||
|
html [data-theme] .app-main h4,
|
||||||
|
html [data-theme] .app-main h5,
|
||||||
|
html [data-theme] .app-main h6,
|
||||||
|
html [data-theme] .app-main p,
|
||||||
|
html [data-theme] .app-main span,
|
||||||
|
html [data-theme] .app-main div,
|
||||||
|
html [data-theme] .app-main button {
|
||||||
|
color: #3c4043 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Exception for buttons - make them stand out */
|
||||||
|
body .app-main button,
|
||||||
|
body .main-content button,
|
||||||
|
[data-theme] .app-main button,
|
||||||
|
[data-theme] .main-content button {
|
||||||
|
color: #3c4043 !important;
|
||||||
|
background: #f8f9fa !important;
|
||||||
|
border: 1px solid #dadce0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Google-style calendar grid - more aggressive styling */
|
||||||
|
html body .calendar-grid,
|
||||||
|
html [data-theme] .calendar-grid,
|
||||||
|
body .calendar-container,
|
||||||
|
[data-theme] .calendar-container {
|
||||||
|
border: 1px solid #dadce0 !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
background: white !important;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(60,64,67,.3), 0 4px 8px 3px rgba(60,64,67,.15) !important;
|
||||||
|
font-family: 'Google Sans', 'Roboto', sans-serif !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html body .calendar-header,
|
||||||
|
html [data-theme] .calendar-header {
|
||||||
|
background: #f8f9fa !important;
|
||||||
|
border-bottom: 1px solid #dadce0 !important;
|
||||||
|
padding: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html body .month-header,
|
||||||
|
html body .week-header,
|
||||||
|
html [data-theme] .month-header,
|
||||||
|
html [data-theme] .week-header {
|
||||||
|
font-size: 22px !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
color: #3c4043 !important;
|
||||||
|
font-family: 'Google Sans', 'Roboto', sans-serif !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Google-style calendar cells - complete overhaul */
|
||||||
|
html body .calendar-day,
|
||||||
|
html [data-theme] .calendar-day,
|
||||||
|
body .day-cell,
|
||||||
|
[data-theme] .day-cell {
|
||||||
|
border: 1px solid #e8eaed !important;
|
||||||
|
background: white !important;
|
||||||
|
transition: background-color 0.15s ease !important;
|
||||||
|
padding: 8px !important;
|
||||||
|
min-height: 120px !important;
|
||||||
|
position: relative !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html body .calendar-day:hover,
|
||||||
|
html [data-theme] .calendar-day:hover,
|
||||||
|
body .day-cell:hover,
|
||||||
|
[data-theme] .day-cell:hover {
|
||||||
|
background: #f8f9fa !important;
|
||||||
|
box-shadow: inset 0 0 0 1px #dadce0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html body .calendar-day.today,
|
||||||
|
html [data-theme] .calendar-day.today,
|
||||||
|
body .day-cell.today,
|
||||||
|
[data-theme] .day-cell.today {
|
||||||
|
background: #e8f0fe !important;
|
||||||
|
border-color: #1a73e8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html body .calendar-day.other-month,
|
||||||
|
html [data-theme] .calendar-day.other-month,
|
||||||
|
body .day-cell.other-month,
|
||||||
|
[data-theme] .day-cell.other-month {
|
||||||
|
background: #fafafa !important;
|
||||||
|
color: #9aa0a6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html body .day-number,
|
||||||
|
html [data-theme] .day-number,
|
||||||
|
body .date-number,
|
||||||
|
[data-theme] .date-number {
|
||||||
|
font-size: 14px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
color: #3c4043 !important;
|
||||||
|
margin-bottom: 4px !important;
|
||||||
|
font-family: 'Google Sans', 'Roboto', sans-serif !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Day headers (Mon, Tue, Wed, etc.) */
|
||||||
|
html body .day-header,
|
||||||
|
html [data-theme] .day-header,
|
||||||
|
body .weekday-header,
|
||||||
|
[data-theme] .weekday-header {
|
||||||
|
background: #f8f9fa !important;
|
||||||
|
color: #5f6368 !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
text-transform: uppercase !important;
|
||||||
|
letter-spacing: 0.8px !important;
|
||||||
|
padding: 8px !important;
|
||||||
|
border-bottom: 1px solid #dadce0 !important;
|
||||||
|
font-family: 'Google Sans', 'Roboto', sans-serif !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Google Calendar-style events - complete redesign */
|
||||||
|
html body .app-main .event,
|
||||||
|
html [data-theme] .app-main .event,
|
||||||
|
html body .calendar-container .event,
|
||||||
|
html [data-theme] .calendar-container .event,
|
||||||
|
body .event,
|
||||||
|
[data-theme] .event {
|
||||||
|
border-radius: 4px !important;
|
||||||
|
padding: 2px 8px !important;
|
||||||
|
font-size: 11px !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
margin: 1px 0 2px 0 !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
border: none !important;
|
||||||
|
color: white !important;
|
||||||
|
font-family: 'Google Sans', 'Roboto', sans-serif !important;
|
||||||
|
box-shadow: 0 1px 3px rgba(60,64,67,.3) !important;
|
||||||
|
transition: transform 0.1s ease, box-shadow 0.1s ease !important;
|
||||||
|
display: block !important;
|
||||||
|
text-overflow: ellipsis !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
line-height: 1.3 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html body .app-main .event *,
|
||||||
|
html [data-theme] .app-main .event *,
|
||||||
|
html body .calendar-container .event *,
|
||||||
|
html [data-theme] .calendar-container .event *,
|
||||||
|
body .event *,
|
||||||
|
[data-theme] .event * {
|
||||||
|
color: white !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html body .app-main .event:hover,
|
||||||
|
html [data-theme] .app-main .event:hover,
|
||||||
|
body .event:hover,
|
||||||
|
[data-theme] .event:hover {
|
||||||
|
transform: translateY(-1px) !important;
|
||||||
|
box-shadow: 0 2px 8px rgba(60,64,67,.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* All-day events styling */
|
||||||
|
html body .event.all-day,
|
||||||
|
html [data-theme] .event.all-day {
|
||||||
|
border-radius: 12px !important;
|
||||||
|
padding: 4px 12px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
margin: 2px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event time display */
|
||||||
|
html body .event-time,
|
||||||
|
html [data-theme] .event-time {
|
||||||
|
opacity: 0.9 !important;
|
||||||
|
font-size: 10px !important;
|
||||||
|
margin-right: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Week view events */
|
||||||
|
html body .week-view .event,
|
||||||
|
html [data-theme] .week-view .event {
|
||||||
|
border-left: 3px solid rgba(255,255,255,0.8) !important;
|
||||||
|
border-radius: 0 4px 4px 0 !important;
|
||||||
|
padding-left: 6px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar table structure */
|
||||||
|
html body .calendar-table,
|
||||||
|
html [data-theme] .calendar-table,
|
||||||
|
body table,
|
||||||
|
[data-theme] table {
|
||||||
|
border-collapse: separate !important;
|
||||||
|
border-spacing: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
background: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html body .calendar-table td,
|
||||||
|
html [data-theme] .calendar-table td,
|
||||||
|
body table td,
|
||||||
|
[data-theme] table td {
|
||||||
|
vertical-align: top !important;
|
||||||
|
border: 1px solid #e8eaed !important;
|
||||||
|
background: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Month/Week view toggle */
|
||||||
|
html body .view-toggle,
|
||||||
|
html [data-theme] .view-toggle {
|
||||||
|
display: flex !important;
|
||||||
|
gap: 4px !important;
|
||||||
|
background: #f1f3f4 !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
padding: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html body .view-toggle button,
|
||||||
|
html [data-theme] .view-toggle button {
|
||||||
|
padding: 6px 12px !important;
|
||||||
|
border: none !important;
|
||||||
|
background: transparent !important;
|
||||||
|
color: #5f6368 !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
transition: all 0.15s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html body .view-toggle button.active,
|
||||||
|
html [data-theme] .view-toggle button.active {
|
||||||
|
background: white !important;
|
||||||
|
color: #1a73e8 !important;
|
||||||
|
box-shadow: 0 1px 3px rgba(60,64,67,.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Today button */
|
||||||
|
html body .today-button,
|
||||||
|
html [data-theme] .today-button {
|
||||||
|
background: white !important;
|
||||||
|
border: 1px solid #dadce0 !important;
|
||||||
|
color: #1a73e8 !important;
|
||||||
|
padding: 8px 16px !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
transition: all 0.15s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html body .today-button:hover,
|
||||||
|
html [data-theme] .today-button:hover {
|
||||||
|
background: #f8f9fa !important;
|
||||||
|
border-color: #1a73e8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Google-style modals */
|
||||||
|
.modal-overlay {
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #3c4043;
|
||||||
|
font-family: 'Google Sans', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Google-style form inputs */
|
||||||
|
input[type="text"],
|
||||||
|
input[type="email"],
|
||||||
|
input[type="password"],
|
||||||
|
input[type="url"],
|
||||||
|
input[type="date"],
|
||||||
|
input[type="time"],
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
border: 1px solid #dadce0;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #3c4043;
|
||||||
|
background: white;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus,
|
||||||
|
textarea:focus,
|
||||||
|
select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #1a73e8;
|
||||||
|
box-shadow: 0 0 0 2px rgba(26,115,232,.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Google-style labels */
|
||||||
|
label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #3c4043;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user