v1
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Spark Slop — Your ideas</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<nav class="nav">
|
||||
<div class="container nav-inner">
|
||||
<a href="/" class="logo">
|
||||
<span class="spark">✨</span>
|
||||
<span>Spark<span class="gradient-text">Slop</span></span>
|
||||
</a>
|
||||
<a href="/" class="btn btn-ghost btn-sm">← Home</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="app-main">
|
||||
<div class="container">
|
||||
<div class="app-header">
|
||||
<div>
|
||||
<h1>Your ideas</h1>
|
||||
<p>Capture a spark, then grow it with timestamped notes.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout">
|
||||
<!-- Left: new idea + list -->
|
||||
<div>
|
||||
<div class="panel" style="margin-bottom: 20px">
|
||||
<h2>New idea</h2>
|
||||
<form id="idea-form">
|
||||
<div class="field">
|
||||
<label for="idea-title">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
id="idea-title"
|
||||
placeholder="e.g. AI recipe planner"
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="idea-desc">Description (optional)</label>
|
||||
<textarea
|
||||
id="idea-desc"
|
||||
rows="3"
|
||||
placeholder="What's the idea?"
|
||||
></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%">
|
||||
✨ Add idea
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>All ideas (<span id="idea-count">0</span>)</h2>
|
||||
<div class="ideas-list" id="ideas-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: detail + notes -->
|
||||
<div class="panel" id="detail-panel">
|
||||
<div class="empty" id="detail-empty">
|
||||
<span class="big">👈</span>
|
||||
Select an idea to see its notes, or create one to get started.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
// --- Tiny API client ---
|
||||
const api = {
|
||||
async req(method, path, body) {
|
||||
const res = await fetch(path, {
|
||||
method,
|
||||
headers: body ? { "Content-Type": "application/json" } : undefined,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
const data = res.status === 204 ? null : await res.json().catch(() => null);
|
||||
if (!res.ok) throw new Error((data && data.error) || "Request failed");
|
||||
return data;
|
||||
},
|
||||
ideas: () => api.req("GET", "/api/ideas"),
|
||||
createIdea: (title, description) =>
|
||||
api.req("POST", "/api/ideas", { title, description }),
|
||||
deleteIdea: (id) => api.req("DELETE", `/api/ideas/${id}`),
|
||||
notes: (id) => api.req("GET", `/api/ideas/${id}/notes`),
|
||||
addNote: (id, body) => api.req("POST", `/api/ideas/${id}/notes`, { body }),
|
||||
deleteNote: (id) => api.req("DELETE", `/api/notes/${id}`),
|
||||
};
|
||||
|
||||
// --- State ---
|
||||
let ideas = [];
|
||||
let selectedId = null;
|
||||
|
||||
// --- Helpers ---
|
||||
const $ = (sel) => document.querySelector(sel);
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
// SQLite stores UTC datetimes as "YYYY-MM-DD HH:MM:SS"; parse as UTC.
|
||||
function parseUTC(s) {
|
||||
return new Date(s.replace(" ", "T") + "Z");
|
||||
}
|
||||
|
||||
function relativeTime(s) {
|
||||
const then = parseUTC(s);
|
||||
const diff = (Date.now() - then.getTime()) / 1000;
|
||||
if (diff < 45) return "just now";
|
||||
if (diff < 90) return "a minute ago";
|
||||
if (diff < 3600) return `${Math.round(diff / 60)} min ago`;
|
||||
if (diff < 7200) return "an hour ago";
|
||||
if (diff < 86400) return `${Math.round(diff / 3600)} hours ago`;
|
||||
if (diff < 172800) return "yesterday";
|
||||
if (diff < 604800) return `${Math.round(diff / 86400)} days ago`;
|
||||
return then.toLocaleDateString(undefined, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function fullTime(s) {
|
||||
return parseUTC(s).toLocaleString(undefined, {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
});
|
||||
}
|
||||
|
||||
let toastTimer;
|
||||
function toast(msg, isError = false) {
|
||||
const el = $("#toast");
|
||||
el.textContent = msg;
|
||||
el.classList.toggle("error", isError);
|
||||
el.classList.add("show");
|
||||
clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => el.classList.remove("show"), 2600);
|
||||
}
|
||||
|
||||
// --- Rendering ---
|
||||
function renderList() {
|
||||
const list = $("#ideas-list");
|
||||
$("#idea-count").textContent = ideas.length;
|
||||
|
||||
if (ideas.length === 0) {
|
||||
list.innerHTML =
|
||||
'<div class="empty" style="padding:30px 10px">No ideas yet. Add your first one!</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = ideas
|
||||
.map(
|
||||
(idea) => `
|
||||
<div class="idea-item ${idea.id === selectedId ? "active" : ""}" data-id="${idea.id}">
|
||||
<h3>${escapeHtml(idea.title)}</h3>
|
||||
<div class="meta">
|
||||
<span class="pill">${idea.note_count} note${idea.note_count === 1 ? "" : "s"}</span>
|
||||
<span>updated ${relativeTime(idea.updated_at)}</span>
|
||||
</div>
|
||||
</div>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
list.querySelectorAll(".idea-item").forEach((el) => {
|
||||
el.addEventListener("click", () => selectIdea(Number(el.dataset.id)));
|
||||
});
|
||||
}
|
||||
|
||||
async function renderDetail() {
|
||||
const panel = $("#detail-panel");
|
||||
const idea = ideas.find((i) => i.id === selectedId);
|
||||
|
||||
if (!idea) {
|
||||
panel.innerHTML =
|
||||
'<div class="empty"><span class="big">👈</span>Select an idea to see its notes, or create one to get started.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
panel.innerHTML = `
|
||||
<div class="detail-head">
|
||||
<h2>${escapeHtml(idea.title)}</h2>
|
||||
<button class="btn btn-danger btn-sm" id="del-idea">Delete idea</button>
|
||||
</div>
|
||||
${idea.description ? `<p class="detail-desc">${escapeHtml(idea.description)}</p>` : ""}
|
||||
<div class="detail-meta">Created ${fullTime(idea.created_at)}</div>
|
||||
<div class="divider"></div>
|
||||
<h2 style="font-size:1.05rem;margin-bottom:4px">Notes</h2>
|
||||
<div id="notes-container"><div class="empty" style="padding:20px">Loading…</div></div>
|
||||
<form class="note-composer" id="note-form">
|
||||
<textarea id="note-body" rows="2" placeholder="Add a note… (Cmd/Ctrl+Enter to post)" required></textarea>
|
||||
<div class="row"><button type="submit" class="btn btn-primary btn-sm">💬 Post note</button></div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
$("#del-idea").addEventListener("click", () => onDeleteIdea(idea.id));
|
||||
|
||||
const form = $("#note-form");
|
||||
form.addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
onAddNote(idea.id);
|
||||
});
|
||||
$("#note-body").addEventListener("keydown", (e) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
onAddNote(idea.id);
|
||||
}
|
||||
});
|
||||
|
||||
loadNotes(idea.id);
|
||||
}
|
||||
|
||||
function renderNotes(notes) {
|
||||
const container = $("#notes-container");
|
||||
if (!container) return;
|
||||
if (notes.length === 0) {
|
||||
container.innerHTML =
|
||||
'<div class="empty" style="padding:24px">No notes yet — add the first one below.</div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = notes
|
||||
.map(
|
||||
(n) => `
|
||||
<div class="note">
|
||||
<div class="avatar">📝</div>
|
||||
<div class="note-body">
|
||||
<div class="text">${escapeHtml(n.body)}</div>
|
||||
<div class="note-time">
|
||||
<span title="${escapeHtml(fullTime(n.created_at))}">${relativeTime(n.created_at)}</span>
|
||||
<span class="del" data-note="${n.id}">delete</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
container.querySelectorAll(".del").forEach((el) => {
|
||||
el.addEventListener("click", () => onDeleteNote(Number(el.dataset.note)));
|
||||
});
|
||||
}
|
||||
|
||||
// --- Actions ---
|
||||
async function refresh() {
|
||||
try {
|
||||
ideas = await api.ideas();
|
||||
renderList();
|
||||
renderDetail();
|
||||
} catch (e) {
|
||||
toast(e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function selectIdea(id) {
|
||||
selectedId = id;
|
||||
renderList();
|
||||
renderDetail();
|
||||
}
|
||||
|
||||
async function loadNotes(id) {
|
||||
try {
|
||||
const notes = await api.notes(id);
|
||||
if (selectedId === id) renderNotes(notes);
|
||||
} catch (e) {
|
||||
toast(e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function onCreateIdea(e) {
|
||||
e.preventDefault();
|
||||
const title = $("#idea-title").value.trim();
|
||||
const description = $("#idea-desc").value.trim();
|
||||
if (!title) return;
|
||||
try {
|
||||
const idea = await api.createIdea(title, description);
|
||||
$("#idea-form").reset();
|
||||
await refresh();
|
||||
selectIdea(idea.id);
|
||||
toast("Idea added ✨");
|
||||
} catch (err) {
|
||||
toast(err.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function onDeleteIdea(id) {
|
||||
if (!confirm("Delete this idea and all its notes?")) return;
|
||||
try {
|
||||
await api.deleteIdea(id);
|
||||
if (selectedId === id) selectedId = null;
|
||||
await refresh();
|
||||
toast("Idea deleted");
|
||||
} catch (err) {
|
||||
toast(err.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function onAddNote(id) {
|
||||
const input = $("#note-body");
|
||||
const body = input.value.trim();
|
||||
if (!body) return;
|
||||
try {
|
||||
await api.addNote(id, body);
|
||||
input.value = "";
|
||||
// Refresh notes immediately, plus list for the updated count/order.
|
||||
await Promise.all([loadNotes(id), refreshListOnly()]);
|
||||
} catch (err) {
|
||||
toast(err.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshListOnly() {
|
||||
ideas = await api.ideas();
|
||||
renderList();
|
||||
}
|
||||
|
||||
async function onDeleteNote(noteId) {
|
||||
try {
|
||||
await api.deleteNote(noteId);
|
||||
if (selectedId) await Promise.all([loadNotes(selectedId), refreshListOnly()]);
|
||||
toast("Note deleted");
|
||||
} catch (err) {
|
||||
toast(err.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Init ---
|
||||
$("#idea-form").addEventListener("submit", onCreateIdea);
|
||||
refresh();
|
||||
@@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Spark Slop — Capture every project idea</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="A clean home for your project ideas and the running notes that grow them."
|
||||
/>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<nav class="nav">
|
||||
<div class="container nav-inner">
|
||||
<a href="/" class="logo">
|
||||
<span class="spark">✨</span>
|
||||
<span>Spark<span class="gradient-text">Slop</span></span>
|
||||
</a>
|
||||
<a href="/app" class="btn btn-primary btn-sm">Open the app →</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<header class="hero">
|
||||
<div class="container">
|
||||
<span class="badge">SQLite · Bun · TypeScript</span>
|
||||
<h1>
|
||||
Every idea deserves a<br />
|
||||
<span class="gradient-text">place to grow.</span>
|
||||
</h1>
|
||||
<p class="sub">
|
||||
Spark Slop is a tiny, fast workspace for your project ideas. Jot the
|
||||
spark, then leave timestamped notes as the thought evolves — like a
|
||||
comment thread for your own brain.
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a href="/app" class="btn btn-primary">Start capturing ideas</a>
|
||||
<a href="#features" class="btn btn-ghost">See how it works</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section id="features" class="container">
|
||||
<div class="features">
|
||||
<div class="feature-card">
|
||||
<span class="icon">💡</span>
|
||||
<h3>Capture the spark</h3>
|
||||
<p>
|
||||
Drop a title and a quick description the moment inspiration hits.
|
||||
No friction, no fields you don't need.
|
||||
</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<span class="icon">💬</span>
|
||||
<h3>Notes with timestamps</h3>
|
||||
<p>
|
||||
Add running notes like comments. Each one is stamped with the
|
||||
moment you wrote it, so you can watch an idea mature over time.
|
||||
</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<span class="icon">⚡</span>
|
||||
<h3>Fast & local</h3>
|
||||
<p>
|
||||
Powered by Bun and SQLite. Your data lives in a single file on your
|
||||
machine — snappy, private, and yours.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
Built with Bun + SQLite + TypeScript · ✨ Spark Slop
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,445 @@
|
||||
:root {
|
||||
--bg: #0b0d12;
|
||||
--bg-soft: #12151d;
|
||||
--surface: #161a24;
|
||||
--surface-2: #1d2230;
|
||||
--border: #262c3a;
|
||||
--text: #e8ebf2;
|
||||
--muted: #8b93a7;
|
||||
--accent: #7c5cff;
|
||||
--accent-2: #00d4ff;
|
||||
--danger: #ff5470;
|
||||
--radius: 14px;
|
||||
--shadow: 0 10px 40px -12px rgba(0, 0, 0, 0.6);
|
||||
--grad: linear-gradient(135deg, var(--accent), var(--accent-2));
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
/* ---------- Buttons ---------- */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface-2);
|
||||
color: var(--text);
|
||||
padding: 10px 18px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.12s ease, border-color 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
.btn:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.btn-primary {
|
||||
background: var(--grad);
|
||||
border: none;
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 24px -8px rgba(124, 92, 255, 0.6);
|
||||
}
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
}
|
||||
.btn-danger {
|
||||
color: var(--danger);
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
.btn-danger:hover {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
.btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* ---------- Nav ---------- */
|
||||
.nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
backdrop-filter: blur(12px);
|
||||
background: rgba(11, 13, 18, 0.7);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.nav-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 68px;
|
||||
}
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-weight: 800;
|
||||
font-size: 1.2rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.logo .spark {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
.gradient-text {
|
||||
background: var(--grad);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* ---------- Landing ---------- */
|
||||
.hero {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
padding: 120px 0 100px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.hero::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -200px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 700px;
|
||||
height: 700px;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(124, 92, 255, 0.25),
|
||||
transparent 60%
|
||||
);
|
||||
filter: blur(40px);
|
||||
z-index: -1;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 6px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
font-size: 0.82rem;
|
||||
color: var(--muted);
|
||||
margin-bottom: 28px;
|
||||
background: var(--surface);
|
||||
}
|
||||
.hero h1 {
|
||||
font-size: clamp(2.4rem, 6vw, 4.2rem);
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.03em;
|
||||
font-weight: 800;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
.hero p.sub {
|
||||
font-size: 1.2rem;
|
||||
color: var(--muted);
|
||||
max-width: 620px;
|
||||
margin: 0 auto 38px;
|
||||
}
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 22px;
|
||||
padding: 40px 0 120px;
|
||||
}
|
||||
.feature-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 28px;
|
||||
transition: transform 0.18s ease, border-color 0.2s ease;
|
||||
}
|
||||
.feature-card:hover {
|
||||
transform: translateY(-4px);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.feature-card .icon {
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 14px;
|
||||
display: block;
|
||||
}
|
||||
.feature-card h3 {
|
||||
font-size: 1.15rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.feature-card p {
|
||||
color: var(--muted);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 30px 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ---------- App ---------- */
|
||||
.app-main {
|
||||
padding: 40px 0 80px;
|
||||
}
|
||||
.app-header {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
margin-bottom: 32px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.app-header h1 {
|
||||
font-size: 2rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.app-header p {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 380px 1fr;
|
||||
gap: 28px;
|
||||
align-items: start;
|
||||
}
|
||||
@media (max-width: 860px) {
|
||||
.layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 22px;
|
||||
}
|
||||
.panel h2 {
|
||||
font-size: 1.05rem;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.field {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.field label {
|
||||
display: block;
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
input[type="text"],
|
||||
textarea {
|
||||
width: 100%;
|
||||
background: var(--bg-soft);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
color: var(--text);
|
||||
padding: 11px 13px;
|
||||
font-size: 0.95rem;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
input[type="text"]:focus,
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Idea list */
|
||||
.ideas-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.idea-item {
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-soft);
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.18s ease, transform 0.12s ease;
|
||||
}
|
||||
.idea-item:hover {
|
||||
border-color: var(--accent);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
.idea-item.active {
|
||||
border-color: var(--accent);
|
||||
background: var(--surface-2);
|
||||
}
|
||||
.idea-item h3 {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.idea-item .meta {
|
||||
font-size: 0.78rem;
|
||||
color: var(--muted);
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.pill {
|
||||
background: rgba(124, 92, 255, 0.15);
|
||||
color: #b9a8ff;
|
||||
padding: 1px 8px;
|
||||
border-radius: 999px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Detail */
|
||||
.detail-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.detail-head h2 {
|
||||
font-size: 1.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
.detail-desc {
|
||||
color: var(--muted);
|
||||
margin-bottom: 4px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.detail-meta {
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
margin: 18px 0;
|
||||
}
|
||||
|
||||
/* Notes / comments */
|
||||
.note {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.note:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.note .avatar {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
background: var(--grad);
|
||||
flex-shrink: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.note-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.note-body .text {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.note-time {
|
||||
font-size: 0.75rem;
|
||||
color: var(--muted);
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.note-time .del {
|
||||
color: var(--danger);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
.note:hover .note-time .del {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.note-composer {
|
||||
margin-top: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.note-composer .row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Empty / states */
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
padding: 60px 20px;
|
||||
}
|
||||
.empty .big {
|
||||
font-size: 2.5rem;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(100px);
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
padding: 12px 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow);
|
||||
transition: transform 0.25s ease;
|
||||
z-index: 100;
|
||||
}
|
||||
.toast.show {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
.toast.error {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
Reference in New Issue
Block a user