Upload files to "static"

This commit is contained in:
mhn
2025-12-05 11:20:37 +00:00
parent ea0b9ee927
commit c04fb2b5e4
3 changed files with 1223 additions and 0 deletions

70
static/index.html Normal file
View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>life-manager</title>
<link rel="stylesheet" href="style.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
</head>
<body>
<div class="container">
<header>
<div style="flex: 1; margin-right: 30px;">
<h1>Life Manager</h1>
<!-- NEW: Search Input similar to OS Site -->
<input type="search" id="search-input" placeholder="Search entries..." autocomplete="off"
style="width: 100%; max-width: 400px; padding: 10px; margin-bottom: 15px; border-radius: 8px; border: 1px solid #333; background: #0f1014; color: white;">
<div id="tag-filter-bar" class="filter-bar">
<!-- Buttons injected via JS -->
</div>
</div>
<div style="display:flex; gap:10px;">
<div class="view-toggle">
<button id="view-list" class="toggle-btn active" onclick="switchView('list')">List</button>
<button id="view-cal" class="toggle-btn" onclick="switchView('calendar')">Calendar</button>
</div>
<button onclick="openNew()" class="btn-primary" style="font-size:1.1rem;">+ New Entry</button>
</div>
</header>
<div id="grid" class="grid"></div>
<div id="calendar-view" class="calendar-container hidden">
<div class="cal-header">
<button onclick="changeMonth(-1)">&#8592;</button>
<h2 id="cal-month-name">Month Year</h2>
<button onclick="changeMonth(1)">&#8594;</button>
</div>
<div class="cal-grid-header">
<div>Sun</div><div>Mon</div><div>Tue</div><div>Wed</div><div>Thu</div><div>Fri</div><div>Sat</div>
</div>
<div id="cal-days" class="cal-grid-days">
<!-- Javascript will inject days here -->
</div>
<div id="mobile-day-details" class="mobile-day-details hidden">
<h3 id="mobile-selected-date-title">Select a day...</h3>
<div id="mobile-event-list"></div>
</div>
</div>
</div>
<!-- Wizard Modal -->
<div id="modal" class="modal">
<div class="wizard-box">
<button onclick="closeModal()" style="float:right; background:none; border:none; color:#666; cursor:pointer; font-size:1.2rem;">&times;</button>
<div id="wizard-body">
<!-- Injected via JS -->
</div>
<div style="display:flex; justify-content:space-between; margin-top:30px;">
<button id="prev-btn" class="hidden" onclick="moveWizard(-1)" style="background:none; border:none; color:#888; cursor:pointer;">Back</button>
<button id="next-btn" class="hidden btn-primary" onclick="moveWizard(1)">Next</button>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>

794
static/script.js Normal file
View File

@@ -0,0 +1,794 @@
// --- 1. The "Life Brain" Definitions ---
const CATEGORIES = {
"brain-dump": {
name: "🧠 Quick Brain Dump",
description: "Get it out of your head. Sort it later.",
emoji: "📥",
steps: [
{
title: "What's on your mind?",
fields: [
{ id: "title", label: "Short Title", type: "text", required: true },
// FIX: Added exact event_date ID
{
id: "event_date",
label: "Date/Time (Optional)",
type: "datetime-local",
},
{
id: "description",
label: "Details (#tags)",
type: "textarea",
required: true,
},
// FIX: Standardized Options
{
id: "frequency",
label: "Is this recurring?",
type: "select",
options: ["One-Time", "Daily", "Weekly", "Monthly", "Yearly"],
},
],
},
],
},
chore: {
name: "🧹 Routine / Chore",
description: "Cleaning, maintenance, bills.",
emoji: "🔄",
steps: [
{
title: "The Routine",
fields: [
{
id: "title",
label: "What needs doing?",
type: "text",
placeholder: "e.g. Take out Trash",
required: true,
},
// FIX: Added exact event_date ID
{
id: "event_date",
label: "Start Date (for recurring calc)",
type: "datetime-local",
},
{
id: "frequency",
label: "How often?",
type: "select",
options: ["One-Time", "Daily", "Weekly", "Monthly", "Yearly"],
},
{
id: "assignee",
label: "Who does this? (Optional)",
type: "text",
placeholder: "e.g. Dad, Kids",
},
],
},
{
title: "Details",
fields: [
{
id: "description",
label: "Instructions / Notes (Use #tags)",
type: "textarea",
},
{ id: "tools", label: "Tools/Supplies needed?", type: "text" },
],
},
],
},
project: {
name: "🛠️ Project / Fix",
description: "Something broken or a goal.",
emoji: "🚧",
steps: [
{
title: "The Project",
fields: [
{ id: "title", label: "Project Name", type: "text", required: true },
// FIX: Added exact event_date ID
{
id: "event_date",
label: "Deadline / Date",
type: "datetime-local",
},
// FIX: Changed "Ongoing" to standard list
{
id: "frequency",
label: "Timeline",
type: "select",
options: ["One-Time", "Daily", "Weekly", "Monthly", "Yearly"],
},
],
},
{
title: "The Plan",
fields: [
{
id: "description",
label: "Desired Outcome (#tags)",
type: "textarea",
},
{ id: "budget", label: "Estimated Cost ($)", type: "number" },
{ id: "blockers", label: "What is stopping us?", type: "text" },
],
},
],
},
inventory: {
name: "📦 Life / Appointments",
description: "Car, Health, House, etc..",
emoji: "📋",
steps: [
{
title: "System Info",
fields: [
{
id: "title",
label: "Item Name",
type: "text",
placeholder: "e.g. Honda Civic",
required: true,
},
// FIX: Added exact event_date ID
{
id: "event_date",
label: "Next Maintenance/Appt Date",
type: "datetime-local",
},
// FIX: Changed "None" to standard list
{
id: "frequency",
label: "Maintenance/Appt Cycle",
type: "select",
options: ["One-Time", "Daily", "Weekly", "Monthly", "Yearly"],
},
],
},
{
title: "Reference Data",
fields: [
{
id: "description",
label: "Key Data (VIN, Names, ID #) - Use #tags",
type: "textarea",
},
{
id: "location",
label: "Where is it happening/stored?",
type: "text",
},
],
},
],
},
};
// --- State ---
let allEntries = [];
let editingId = null;
let currentStep = 0;
let currentCatKey = null;
let wizardData = {};
// NEW: OS-Site Style State
let activeTags = new Set();
let searchQuery = "";
// --- Elements ---
const grid = document.getElementById("grid");
const modal = document.getElementById("modal");
const modalBody = document.getElementById("wizard-body");
const prevBtn = document.getElementById("prev-btn");
const nextBtn = document.getElementById("next-btn");
const searchInput = document.getElementById("search-input"); // NEW
document.addEventListener("DOMContentLoaded", () => {
fetchEntries();
// NEW: Search Listener
searchInput.addEventListener("input", (e) => {
searchQuery = e.target.value.trim().toLowerCase();
refreshViews();
});
});
// --- Core Functions ---
async function fetchEntries() {
const res = await fetch("/api/entries");
allEntries = await res.json();
renderTagBar();
refreshViews();
}
function refreshViews() {
if (currentView === "list") renderGrid();
else renderCalendar();
}
// --- Tag System (Ported from OS Site) ---
function renderTagBar() {
const container = document.getElementById("tag-filter-bar");
const tags = new Set();
// 1. Collect all tags
allEntries.forEach((entry) => {
if (entry.tags) entry.tags.forEach((t) => tags.add(t));
const matches = entry.description ? entry.description.match(/#\w+/g) : [];
if (matches) matches.forEach((m) => tags.add(m.substring(1)));
});
// 2. Build HTML
container.innerHTML = "";
// "All" Button (Clears filters)
const allBtn = document.createElement("button");
allBtn.className = `chip ${activeTags.size === 0 ? "active" : ""}`;
allBtn.textContent = "All";
allBtn.onclick = () => {
activeTags.clear();
renderTagBar();
refreshViews();
};
container.appendChild(allBtn);
// Tag Buttons
Array.from(tags).sort().forEach((tag) => {
const btn = document.createElement("button");
btn.className = `chip ${activeTags.has(tag) ? "active" : ""}`;
btn.textContent = `#${tag}`;
btn.onclick = () => toggleTag(tag);
container.appendChild(btn);
});
}
function toggleTag(tag) {
// Multi-select logic from OS Site
if (activeTags.has(tag)) {
activeTags.delete(tag);
} else {
activeTags.add(tag);
}
renderTagBar(); // Re-render to update active classes
refreshViews();
}
// --- Filter Logic (The Brains) ---
function entryMatchesFilter(entry) {
// 1. Text Search Check (Name, Desc, Details)
const textMatch = !searchQuery ||
entry.title.toLowerCase().includes(searchQuery) ||
(entry.description && entry.description.toLowerCase().includes(searchQuery));
if (!textMatch) return false;
// 2. Tag Check (Multi-select OR logic)
// If no tags selected, show everything. If tags selected, entry must have at least one.
if (activeTags.size === 0) return true;
// Collect entry tags
const entryTags = new Set(entry.tags || []);
const descTags = entry.description ? entry.description.match(/#\w+/g) : [];
if (descTags) descTags.forEach(t => entryTags.add(t.substring(1)));
// Check intersection
for (let tag of activeTags) {
if (entryTags.has(tag)) return true;
}
return false;
}
// Update the renderGrid to use this new logic
function renderGrid() {
grid.innerHTML = "";
// Use the shared filter function
const visibleEntries = allEntries.filter(entry => entryMatchesFilter(entry));
if (visibleEntries.length === 0) {
grid.innerHTML = `<div style="text-align:center; grid-column:1/-1; color:#555; margin-top:50px;">
<h2>No entries found.</h2>
</div>`;
return;
}
visibleEntries.forEach((entry) => {
const descWithTags = parseTags(entry.description || "");
const card = document.createElement("div");
card.className = "card";
// ... (rest of card generation remains the same) ...
card.innerHTML = `
<div class="card-top">
<span style="font-size:1.5rem">${CATEGORIES[entry.template_type]?.emoji || "📄"}</span>
<span class="freq-badge freq-${entry.frequency || "One-Time"}">${entry.frequency || "One-Time"}</span>
</div>
<h3>${entry.title}</h3>
<p>${descWithTags}</p>
<div class="card-actions">
<button class="btn-icon" onclick="openEdit('${entry.id}')">✏️ Edit</button>
<button class="btn-icon" style="color:var(--danger)" onclick="deleteEntry('${entry.id}')">🗑️</button>
</div>
`;
grid.appendChild(card);
});
}
// Convert "#tag" text into clickable spans
function parseTags(text) {
if (!text) return "";
return text.replace(
/#(\w+)/g,
'<span class="hashtag" onclick="filterByTag(\'$1\')">#$1</span>',
);
}
function filterByTag(tag) {
const filtered = allEntries.filter((e) => {
const inDesc = e.description && e.description.includes("#" + tag);
const inTags = e.tags && e.tags.includes(tag);
return inDesc || inTags;
});
renderGrid(filtered);
// Visual feedback
document.getElementById("filter-label").innerText = `Filtering by: #${tag}`;
document.getElementById("clear-filter").classList.remove("hidden");
}
function clearFilter() {
renderGrid(allEntries);
document.getElementById("filter-label").innerText = "All Items";
document.getElementById("clear-filter").classList.add("hidden");
}
// --- Wizard / Editor Logic ---
function openNew() {
editingId = null;
wizardData = {};
showCategorySelect();
modal.classList.add("open");
}
async function openEdit(id) {
const entry = allEntries.find((e) => e.id === id);
if (!entry) return;
editingId = id;
currentCatKey = entry.template_type;
// Helper to format date for input field (YYYY-MM-DDTHH:mm)
let formattedDate = "";
if (entry.event_date) {
const d = new Date(entry.event_date);
// Adjust to local timezone string manually to fit input
const pad = (n) => (n < 10 ? "0" + n : n);
formattedDate = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
// Populate wizardData
wizardData = {
title: entry.title,
description: entry.description,
frequency: entry.frequency,
event_date: formattedDate, // <--- LOAD THE DATE HERE
...entry.details,
};
startWizard(currentCatKey);
modal.classList.add("open");
}
function showCategorySelect() {
modalBody.innerHTML = `
<h2 style="margin-bottom:20px; text-align:center;">What are we organizing?</h2>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:15px;">
${Object.entries(CATEGORIES)
.map(
([key, cat]) => `
<div onclick="startWizard('${key}')"
style="background:#2c303a; padding:20px; border-radius:12px; cursor:pointer; text-align:center; border:1px solid #333; transition:0.2s;">
<div style="font-size:2rem; margin-bottom:10px;">${cat.emoji}</div>
<strong>${cat.name}</strong><br>
<small style="color:#888">${cat.description}</small>
</div>
`,
)
.join("")}
</div>
`;
prevBtn.classList.add("hidden");
nextBtn.classList.add("hidden");
}
function startWizard(key) {
currentCatKey = key;
currentStep = 0;
renderStep();
}
function renderStep() {
const template = CATEGORIES[currentCatKey];
const step = template.steps[currentStep];
// Build Form
let html = `<h2>${step.title}</h2>`;
step.fields.forEach((field) => {
const val = wizardData[field.id] || "";
html += `<div class="form-group"><label>${field.label}</label>`;
if (field.type === "textarea") {
html += `<textarea id="inp_${field.id}" rows="4">${val}</textarea>`;
} else if (field.type === "select") {
html += `<select id="inp_${field.id}">
${field.options.map((o) => `<option value="${o}" ${val === o ? "selected" : ""}>${o}</option>`).join("")}
</select>`;
} else {
html += `<input type="${field.type}" id="inp_${field.id}" value="${val}" placeholder="${field.placeholder || ""}">`;
}
html += `</div>`;
});
modalBody.innerHTML = html;
// Buttons
prevBtn.classList.remove("hidden");
nextBtn.classList.remove("hidden");
nextBtn.innerText =
currentStep === template.steps.length - 1
? editingId
? "Update"
: "Save"
: "Next";
}
// Logic to move between steps
window.moveWizard = async (dir) => {
// 1. Save current slide data to wizardData object
const inputs = modalBody.querySelectorAll("input, select, textarea");
inputs.forEach((i) => {
const key = i.id.replace("inp_", "");
wizardData[key] = i.value;
});
// 2. Calculate next step
const template = CATEGORIES[currentCatKey];
const nextIdx = currentStep + dir;
if (dir === -1 && currentStep === 0) {
if (editingId)
closeModal(); // If editing and go back, just close
else showCategorySelect(); // If new, go back to chooser
return;
}
if (nextIdx >= template.steps.length) {
await saveData();
return;
}
currentStep = nextIdx;
renderStep();
};
async function saveData() {
const payload = {
template_type: currentCatKey,
title: wizardData.title || "Untitled",
description: wizardData.description || "",
frequency: wizardData.frequency || "One-Time",
// NEW: Send the date if it exists
event_date: wizardData.event_date
? new Date(wizardData.event_date).toISOString()
: null,
tags: extractTags(wizardData.description || ""),
details: wizardData,
};
const url = editingId ? `/api/entries/${editingId}` : "/api/entries";
const method = editingId ? "PUT" : "POST";
try {
const res = await fetch(url, {
method: method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
// FIX: Check if the server rejected the data
if (!res.ok) {
const errorText = await res.text();
console.error("Server Error:", errorText);
alert(
"Error saving entry! Check console for details.\n\nServer said: " +
res.status,
);
return;
}
// If we get here, it worked
closeModal();
fetchEntries();
} catch (e) {
console.error(e);
alert("Network Error: Could not reach the server.");
}
}
function extractTags(text) {
const match = text.match(/#\w+/g);
return match ? match.map((t) => t.substring(1)) : []; // Remove '#'
}
async function deleteEntry(id) {
if (confirm("Are you sure you want to remove this?")) {
await fetch(`/api/entries/${id}`, { method: "DELETE" });
fetchEntries();
}
}
function closeModal() {
modal.classList.remove("open");
}
// --- Calendar Logic ---
let currentDate = new Date();
let currentView = "list";
function switchView(view) {
currentView = view;
document
.getElementById("view-list")
.classList.toggle("active", view === "list");
document
.getElementById("view-cal")
.classList.toggle("active", view === "calendar");
if (view === "calendar") {
document.getElementById("grid").classList.add("hidden");
document.getElementById("calendar-view").classList.remove("hidden");
renderCalendar();
} else {
document.getElementById("grid").classList.remove("hidden");
document.getElementById("calendar-view").classList.add("hidden");
}
}
function changeMonth(dir) {
currentDate.setMonth(currentDate.getMonth() + dir);
renderCalendar();
}
function renderCalendar() {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
// Update Header
const monthNames = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
document.getElementById("cal-month-name").innerText =
`${monthNames[month]} ${year}`;
const daysContainer = document.getElementById("cal-days");
daysContainer.innerHTML = "";
const firstDayIndex = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
// 1. Add Empty Slots for previous month
for (let i = 0; i < firstDayIndex; i++) {
const div = document.createElement("div");
div.className = "cal-day";
div.style.background = "#111"; // dim
daysContainer.appendChild(div);
}
// 2. Add Days
const today = new Date();
for (let i = 1; i <= daysInMonth; i++) {
const dayDiv = document.createElement("div");
dayDiv.className = "cal-day";
// Highlight Today
if (i === today.getDate() && month === today.getMonth() && year === today.getFullYear()) {
dayDiv.classList.add("today");
}
dayDiv.innerHTML = `<span class="day-number">${i}</span>`;
// Find Events
const dayEvents = getEventsForDay(year, month, i);
// Render Event Indicators (Bars on desktop, Dots on mobile via CSS)
dayEvents.forEach((evt) => {
const el = document.createElement("div");
el.className = `cal-event type-${evt.type}`;
el.innerText = evt.title; // CSS hides this on mobile
// Desktop Click: Open Edit directly
el.onclick = (e) => {
if(window.innerWidth > 768) {
e.stopPropagation();
openEdit(evt.id);
}
};
dayDiv.appendChild(el);
});
// Mobile Click: Select the day and show details below
dayDiv.onclick = () => {
selectMobileDay(dayDiv, dayEvents, i, year, month);
};
daysContainer.appendChild(dayDiv);
}
}
function getEventsForDay(year, month, day) {
const targetDate = new Date(year, month, day);
const dayOfWeek = targetDate.getDay();
const dayOfMonth = day;
const targetMonth = month;
let events = [];
allEntries.forEach((entry) => {
if (!entryMatchesFilter(entry)) return;
const freq = entry.frequency || "One-Time";
const title = entry.title;
const id = entry.id;
// 1. Determine the "Base Date"
// Use the manually picked date. If none exists, use the date it was created.
const baseDateStr = entry.event_date || entry.created_at;
const baseDate = new Date(baseDateStr);
// --- FIXED SECTION ---
// 1. One-Time Events
// Now uses baseDate, so items appear even if you didn't pick a deadline.
if (freq === "One-Time") {
if (
baseDate.getFullYear() === year &&
baseDate.getMonth() === month &&
baseDate.getDate() === day
) {
events.push({
id,
title,
type: "one-time",
category: entry.template_type,
});
}
}
// ---------------------
// 2. Daily
if (freq === "Daily") {
// Check if target day is AFTER the start date
// Note: We clone baseDate with new Date() to avoid modifying the original in the loop
if (targetDate >= new Date(baseDate).setHours(0, 0, 0, 0)) {
events.push({
id,
title,
type: "daily",
category: entry.template_type,
});
}
}
// 3. Weekly
if (freq === "Weekly") {
if (
baseDate.getDay() === dayOfWeek &&
targetDate >= new Date(baseDate).setHours(0, 0, 0, 0)
) {
events.push({
id,
title,
type: "weekly",
category: entry.template_type,
});
}
}
// 4. Monthly
if (freq === "Monthly") {
if (
baseDate.getDate() === dayOfMonth &&
targetDate >= new Date(baseDate).setHours(0, 0, 0, 0)
) {
events.push({
id,
title,
type: "monthly",
category: entry.template_type,
});
}
}
// 5. Yearly
if (freq === "Yearly") {
if (
baseDate.getMonth() === targetMonth &&
baseDate.getDate() === dayOfMonth &&
targetDate >= new Date(baseDate).setHours(0, 0, 0, 0)
) {
events.push({
id,
title,
type: "yearly",
category: entry.template_type,
});
}
}
});
return events;
}
function selectMobileDay(el, events, day, year, month) {
// 1. UI Selection Highlighting
document.querySelectorAll('.cal-day').forEach(d => d.classList.remove('selected'));
el.classList.add('selected');
// 2. Show the Container
const detailBox = document.getElementById('mobile-day-details');
const titleEl = document.getElementById('mobile-selected-date-title');
const listEl = document.getElementById('mobile-event-list');
detailBox.classList.remove('hidden');
// 3. Format Date Title
const dateObj = new Date(year, month, day);
titleEl.innerText = dateObj.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' });
// 4. Render List
if (events.length === 0) {
listEl.innerHTML = `<div style="color:#666; font-style:italic;">No events planned.</div>`;
return;
}
listEl.innerHTML = events.map(evt => `
<div class="mobile-event-card"
style="border-left-color: ${getCategoryColor(evt.type)}"
onclick="openEdit('${evt.id}')">
<span>${evt.title}</span>
<span style="font-size:0.8rem; color:#888;">${evt.category}</span>
</div>
`).join('');
// Scroll to details on very small screens
if(window.innerWidth < 600) {
detailBox.scrollIntoView({ behavior: "smooth" });
}
}
// Helper for colors
function getCategoryColor(type) {
if(type === 'daily') return 'var(--success)';
if(type === 'weekly') return 'var(--warning)';
if(type === 'monthly') return '#2979ff';
if(type === 'yearly') return '#ff4081';
return 'var(--accent)';
}

359
static/style.css Normal file
View File

@@ -0,0 +1,359 @@
:root {
--bg-main: #0f1014;
--bg-card: #1b1d24;
--accent: #0064ff;
--accent-glow: rgb(81, 117, 255);
--text-primary: #c9d4e3;
--text-secondary: #8f9bb3;
--success: #00e676;
--warning: #ffab00;
--danger: #ff1744;
--radius: 12px;
}
body {
background-color: var(--bg-main);
color: var(--text-primary);
font-family: 'Inter', sans-serif;
margin: 0;
padding: 20px;
}
.container { max-width: 1200px; margin: 0 auto; }
/* =========================================
HEADER & FILTER STYLES (Responsive & Wrapping)
========================================= */
header {
display: flex;
flex-direction: column; /* Default: Stack vertically for mobile */
gap: 20px;
margin-bottom: 30px;
}
/* Desktop: Side-by-side layout */
@media (min-width: 768px) {
header {
flex-direction: row;
justify-content: space-between;
align-items: flex-start; /* Aligns to top */
}
/* Title & Tags Container */
header > div:first-child {
flex: 1; /* Takes up available space */
margin-right: 30px;
}
/* Button Container */
header > div:last-child {
flex-shrink: 0; /* Prevents buttons from squishing */
margin-top: 5px; /* Aligns nicely with the H1 */
}
}
h1 {
font-weight: 700;
background: linear-gradient(to right, #fff, #aaa);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin: 0 0 15px 0; /* Add space below title for the chips */
font-size: 2rem;
line-height: 1.2;
}
/* Filter Bar - Wrapping List */
.filter-bar {
display: flex;
flex-wrap: wrap; /* KEY CHANGE: Wraps tags to next line */
gap: 8px; /* Consistent spacing */
width: 100%;
}
.chip {
padding: 6px 14px;
background: var(--bg-card);
border: 1px solid #333;
border-radius: 50px; /* Fully rounded pill shape */
color: var(--text-secondary);
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s ease;
user-select: none;
}
.chip:hover {
border-color: var(--accent);
color: #fff;
transform: translateY(-1px);
}
.chip.active {
background: var(--accent);
color: white;
border-color: var(--accent);
box-shadow: 0 0 12px var(--accent-glow);
}
/* Card Grid */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
}
.card {
background: var(--bg-card);
border-radius: var(--radius);
padding: 20px;
border: 1px solid #2c303a;
transition: transform 0.2s, box-shadow 0.2s;
position: relative;
display: flex;
flex-direction: column;
}
.card:hover { transform: translateY(-3px); border-color: #444; box-shadow: 0 10px 30px rgba(0,0,0,0.5); }
/* Card Internals */
.card-top { display: flex; justify-content: space-between; margin-bottom: 10px; }
.freq-badge {
font-size: 0.7rem; text-transform: uppercase; font-weight: bold; padding: 4px 8px; border-radius: 4px; letter-spacing: 0.5px;
}
.freq-Daily { background: rgba(0, 230, 118, 0.15); color: var(--success); }
.freq-Weekly { background: rgba(255, 171, 0, 0.15); color: var(--warning); }
.freq-Monthly { background: rgba(41, 121, 255, 0.15); color: #2979ff; }
.freq-One-Time { background: #333; color: #aaa; }
.card h3 { margin: 0 0 10px 0; font-size: 1.2rem; }
.card p { color: var(--text-secondary); line-height: 1.5; font-size: 0.95rem; flex-grow: 1; white-space: pre-wrap; }
/* The Hash Tag Style */
.hashtag {
color: #c9d4e3;
background: rgba(124, 77, 255, 0.1);
padding: 2px 6px;
border-radius: 4px;
font-weight: 600;
cursor: pointer;
}
.hashtag:hover { text-decoration: underline; }
.card-actions { margin-top: 15px; border-top: 1px solid #2c303a; padding-top: 15px; display: flex; gap: 10px; justify-content: flex-end; }
.btn-icon { background: none; border: none; cursor: pointer; color: #666; font-size: 0.9rem; }
.btn-icon:hover { color: #fff; }
/* Wizard Modal */
.modal {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.8); backdrop-filter: blur(8px);
display: flex; justify-content: center; align-items: center;
z-index: 1000; opacity: 0; pointer-events: none; transition: opacity 0.3s;
}
.modal.open { opacity: 1; pointer-events: all; }
.wizard-box {
background: #1b1d24; width: 500px; max-width: 90%; border-radius: 20px;
padding: 30px; border: 1px solid #333; box-shadow: 0 20px 60px rgba(0,0,0,0.6);
}
.form-group { margin-bottom: 20px; }
.form-group label { display: block; color: var(--text-secondary); margin-bottom: 8px; font-size: 0.9rem; }
.form-group input, .form-group textarea, .form-group select {
width: 100%; padding: 12px; background: #0f1014; border: 1px solid #333;
color: white; border-radius: 8px; font-family: inherit; font-size: 1rem;
box-sizing: border-box;
}
.form-group input:focus { border-color: var(--accent); outline: none; }
.btn-primary { background: var(--accent); color: white; border: none; padding: 10px 24px; border-radius: 8px; font-weight: 600; cursor: pointer; }
.btn-primary:hover { background: var(--bg-card); }
.hidden { display: none; }
.view-toggle {
background: #1b1d24;
border: 1px solid #333;
border-radius: 8px;
padding: 4px;
display: flex;
}
.toggle-btn {
background: transparent;
border: none;
color: #888;
padding: 8px 16px;
cursor: pointer;
border-radius: 6px;
font-weight: 600;
}
.toggle-btn.active { background: #333; color: white; }
/* Calendar Container */
.calendar-container {
background: var(--bg-card);
border-radius: var(--radius);
padding: 20px;
border: 1px solid #333;
}
.cal-header {
display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;
}
.cal-header h2 { margin: 0; color: var(--accent); }
.cal-header button {
background: #333; border: none; color: white; width: 40px; height: 40px; border-radius: 50%; cursor: pointer;
}
.cal-header button:hover { background: var(--accent); }
/* Calendar Grid */
.cal-grid-header, .cal-grid-days {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
background: #333; /* Border color */
border: 1px solid #333;
}
.cal-grid-header div {
background: #1b1d24;
padding: 10px;
text-align: center;
color: var(--text-secondary);
font-weight: bold;
}
.cal-day {
background: #15171c;
min-height: 120px;
padding: 5px;
position: relative;
cursor: pointer;
}
.cal-day:hover { background: #1f2229; }
.cal-day.today { background: rgba(124, 77, 255, 0.05); }
.day-number {
position: absolute; top: 5px; right: 8px; color: #666; font-size: 0.9rem;
}
/* Calendar Events */
.cal-event {
font-size: 0.75rem;
margin-top: 2px;
padding: 3px 6px;
border-radius: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.cal-event.type-one-time { background: var(--accent); color: white; }
.cal-event.type-daily { background: rgba(0, 230, 118, 0.2); color: var(--success); border-left: 2px solid var(--success); }
.cal-event.type-weekly { background: rgba(255, 171, 0, 0.2); color: var(--warning); border-left: 2px solid var(--warning); }
.cal-event.type-monthly { background: rgba(41, 121, 255, 0.2); color: #2979ff; border-left: 2px solid #2979ff; }
.cal-event.type-yearly {
background: rgba(255, 64, 129, 0.2);
color: #ff4081;
border-left: 2px solid #ff4081;
}
.cal-event::before {
content: '•';
margin-right: 4px;
font-weight: bold;
color: rgba(255,255,255,0.7);
}
/* Specific colors for categories if you want them */
.cal-event[onclick*="brain-dump"] { border-right: 3px solid #e040fb; }
.cal-event[onclick*="project"] { border-right: 3px solid #00bcd4; }
.cal-event[onclick*="chore"] { border-right: 3px solid #ffab00; }
#search-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent-glow);
}
@media (max-width: 768px) {
/* 1. Header controls: Make them smaller */
.cal-header h2 { font-size: 1.1rem; }
.cal-header button { width: 32px; height: 32px; }
/* 2. The Grid: Reduce height and font size */
.cal-day {
min-height: 50px; /* Much shorter than desktop */
font-size: 0.8rem;
padding: 2px;
display: flex;
flex-direction: column;
align-items: center; /* Center the dots */
}
/* 3. The Events: Transform text bars into DOTS */
.cal-event {
font-size: 0; /* Hide text */
width: 6px; /* Dot width */
height: 6px; /* Dot height */
padding: 0;
border-radius: 50%; /* Circle */
border: none !important; /* Remove the left-border styling */
margin: 1px;
display: inline-block;
}
/* Keep colors but apply to background */
.cal-event.type-one-time { background: var(--accent); }
.cal-event.type-daily { background: var(--success); }
.cal-event.type-weekly { background: var(--warning); }
.cal-event.type-monthly { background: #2979ff; }
.cal-event.type-yearly { background: #ff4081; }
/* 4. Container for the Dots */
/* This forces dots to wrap if there are many */
.cal-day {
display: flex;
flex-flow: row wrap;
justify-content: center;
align-content: flex-start;
gap: 2px;
padding-top: 20px; /* Make room for the number */
}
.day-number {
position: absolute;
top: 2px;
left: 50%;
transform: translateX(-50%);
font-size: 0.75rem;
}
/* 5. Selected State for Mobile */
.cal-day.selected {
background: rgba(255, 255, 255, 0.1);
border: 1px solid var(--accent);
}
}
/* Styles for the Details Box below calendar (Visible on mobile) */
.mobile-day-details {
margin-top: 20px;
border-top: 1px solid #333;
padding-top: 15px;
animation: fadeIn 0.3s ease;
}
.mobile-day-details h3 {
font-size: 1rem;
color: var(--text-secondary);
margin-bottom: 10px;
}
/* Reuse card styles for the mobile list items */
.mobile-event-card {
background: #15171c;
padding: 12px;
border-radius: 8px;
margin-bottom: 8px;
border-left: 4px solid transparent;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}