Kethan Dosapati commited on
Commit
363bda3
·
1 Parent(s): 8dee7a2

Enhance static UI and authentication: improve sidebar auth visibility, add mobile responsiveness, and refine error messages; update `/report` endpoint to use `ArticleOut` model; redesign styles for a Reddit-like layout with theming and responsive tweaks.

Browse files
api/database.py CHANGED
@@ -5,8 +5,10 @@ from pathlib import Path
5
  from dotenv import load_dotenv
6
  from supabase import create_client, Client
7
 
8
- # Load .env from project root (parent of api/) so vars are available
9
- load_dotenv(Path(__file__).resolve().parent.parent / ".env")
 
 
10
 
11
  SUPABASE_URL = os.environ["SUPABASE_URL"]
12
  SUPABASE_SERVICE_KEY = os.environ["SUPABASE_SERVICE_KEY"]
 
5
  from dotenv import load_dotenv
6
  from supabase import create_client, Client
7
 
8
+ # Load .env from api/ first, then project root, so vars are available
9
+ _root = Path(__file__).resolve().parent
10
+ load_dotenv(_root / ".env")
11
+ load_dotenv(_root.parent / ".env")
12
 
13
  SUPABASE_URL = os.environ["SUPABASE_URL"]
14
  SUPABASE_SERVICE_KEY = os.environ["SUPABASE_SERVICE_KEY"]
api/endpoints/articles.py CHANGED
@@ -1,12 +1,29 @@
1
  """GET /articles — list recent articles (no search required)."""
2
  from typing import Optional
3
 
4
- from fastapi import APIRouter
5
 
6
  from database import supabase
7
 
8
  router = APIRouter(tags=["articles"])
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  @router.get("/articles")
12
  def list_articles(
@@ -18,7 +35,7 @@ def list_articles(
18
  """List recent articles, newest first. Optional filters: since (ISO date), language, type."""
19
  query = (
20
  supabase.table("articles")
21
- .select("id, title, body, language, tags, type, contributing_agent, confidence, created_at")
22
  .order("created_at", desc=True)
23
  .limit(min(limit, 100))
24
  )
 
1
  """GET /articles — list recent articles (no search required)."""
2
  from typing import Optional
3
 
4
+ from fastapi import APIRouter, HTTPException
5
 
6
  from database import supabase
7
 
8
  router = APIRouter(tags=["articles"])
9
 
10
+ ARTICLE_FIELDS = "id, title, body, language, tags, type, contributing_agent, confidence, created_at"
11
+
12
+
13
+ @router.get("/articles/{article_id}")
14
+ def get_article(article_id: str):
15
+ """Get a single article by id for the post detail view."""
16
+ result = (
17
+ supabase.table("articles")
18
+ .select(ARTICLE_FIELDS)
19
+ .eq("id", article_id)
20
+ .limit(1)
21
+ .execute()
22
+ )
23
+ if not result.data:
24
+ raise HTTPException(status_code=404, detail="Article not found")
25
+ return result.data[0]
26
+
27
 
28
  @router.get("/articles")
29
  def list_articles(
 
35
  """List recent articles, newest first. Optional filters: since (ISO date), language, type."""
36
  query = (
37
  supabase.table("articles")
38
+ .select(ARTICLE_FIELDS)
39
  .order("created_at", desc=True)
40
  .limit(min(limit, 100))
41
  )
api/endpoints/comments.py CHANGED
@@ -5,11 +5,12 @@ from fastapi import APIRouter, Header, HTTPException
5
 
6
  from auth import get_author_from_bearer
7
  from database import supabase
 
8
 
9
  router = APIRouter(tags=["comments"])
10
 
11
 
12
- @router.get("/articles/{article_id}/comments")
13
  def list_comments(article_id: str):
14
  """List comments for an article, oldest first."""
15
  result = (
@@ -22,7 +23,7 @@ def list_comments(article_id: str):
22
  return result.data
23
 
24
 
25
- @router.post("/articles/{article_id}/comments", status_code=201)
26
  def create_comment(
27
  article_id: str,
28
  payload: dict,
 
5
 
6
  from auth import get_author_from_bearer
7
  from database import supabase
8
+ from models import Comment
9
 
10
  router = APIRouter(tags=["comments"])
11
 
12
 
13
+ @router.get("/articles/{article_id}/comments", response_model=list[Comment])
14
  def list_comments(article_id: str):
15
  """List comments for an article, oldest first."""
16
  result = (
 
23
  return result.data
24
 
25
 
26
+ @router.post("/articles/{article_id}/comments", response_model=Comment, status_code=201)
27
  def create_comment(
28
  article_id: str,
29
  payload: dict,
api/endpoints/match.py CHANGED
@@ -4,11 +4,12 @@ from typing import Optional
4
  from fastapi import APIRouter, Query
5
 
6
  from database import supabase
 
7
 
8
  router = APIRouter(tags=["match"])
9
 
10
 
11
- @router.get("")
12
  def match_articles(
13
  q: str = Query(..., min_length=1, description="Search query"),
14
  language: Optional[str] = Query(None, description="Filter by language"),
 
4
  from fastapi import APIRouter, Query
5
 
6
  from database import supabase
7
+ from models import Article
8
 
9
  router = APIRouter(tags=["match"])
10
 
11
 
12
+ @router.get("", response_model=list[Article])
13
  def match_articles(
14
  q: str = Query(..., min_length=1, description="Search query"),
15
  language: Optional[str] = Query(None, description="Filter by language"),
api/endpoints/report.py CHANGED
@@ -4,6 +4,7 @@ from typing import Any, Optional
4
  from fastapi import APIRouter, HTTPException
5
 
6
  from database import supabase
 
7
 
8
  router = APIRouter(tags=["report"])
9
 
@@ -36,7 +37,7 @@ def _body_from_report(payload: dict) -> str:
36
  return "\n".join(parts).strip()
37
 
38
 
39
- @router.post("/report", status_code=201)
40
  def report_article(payload: dict[str, Any]):
41
  """
42
  Report a new article (MCP yantrabodha_report).
@@ -77,6 +78,6 @@ def report_article(payload: dict[str, Any]):
77
  if not result.data:
78
  raise HTTPException(status_code=500, detail="Failed to insert article")
79
  out = result.data[0]
80
- return {"id": str(out["id"]), "title": out["title"], "created_at": out["created_at"]}
81
  except Exception as e:
82
  raise HTTPException(status_code=500, detail=str(e))
 
4
  from fastapi import APIRouter, HTTPException
5
 
6
  from database import supabase
7
+ from models import ArticleOut
8
 
9
  router = APIRouter(tags=["report"])
10
 
 
37
  return "\n".join(parts).strip()
38
 
39
 
40
+ @router.post("/report", response_model=ArticleOut, status_code=201)
41
  def report_article(payload: dict[str, Any]):
42
  """
43
  Report a new article (MCP yantrabodha_report).
 
78
  if not result.data:
79
  raise HTTPException(status_code=500, detail="Failed to insert article")
80
  out = result.data[0]
81
+ return ArticleOut(id=str(out["id"]), title=out["title"], created_at=str(out["created_at"]))
82
  except Exception as e:
83
  raise HTTPException(status_code=500, detail=str(e))
api/models.py CHANGED
@@ -1,7 +1,7 @@
1
  """Shared Pydantic models for the API."""
2
  from typing import Optional
3
 
4
- from pydantic import BaseModel
5
 
6
 
7
  class ArticleIn(BaseModel):
@@ -21,3 +21,51 @@ class ArticleOut(BaseModel):
21
  id: str
22
  title: str
23
  created_at: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """Shared Pydantic models for the API."""
2
  from typing import Optional
3
 
4
+ from pydantic import BaseModel, ConfigDict
5
 
6
 
7
  class ArticleIn(BaseModel):
 
21
  id: str
22
  title: str
23
  created_at: str
24
+
25
+
26
+ class Article(BaseModel):
27
+ """Article as returned by GET /api/articles and GET /api/articles/{id}."""
28
+
29
+ model_config = ConfigDict(extra="ignore")
30
+
31
+ id: str
32
+ title: str
33
+ body: Optional[str] = None
34
+ language: Optional[str] = None
35
+ tags: Optional[list[str]] = None
36
+ type: Optional[str] = None
37
+ contributing_agent: Optional[str] = None
38
+ confidence: Optional[str] = None
39
+ created_at: str
40
+
41
+
42
+ class ArticlesListResponse(BaseModel):
43
+ """Response for GET /api/articles — list recent articles."""
44
+
45
+ articles: list[Article]
46
+ all_projects: list[Article]
47
+
48
+
49
+ class Comment(BaseModel):
50
+ """Comment as returned by GET/POST /api/articles/{id}/comments."""
51
+
52
+ model_config = ConfigDict(extra="ignore")
53
+
54
+ id: str
55
+ article_id: str
56
+ body: str
57
+ author: Optional[str] = None
58
+ created_at: str
59
+
60
+
61
+ class ConfigResponse(BaseModel):
62
+ """Response for GET /api/config."""
63
+
64
+ supabaseUrl: str
65
+ supabaseAnonKey: str
66
+
67
+
68
+ class HealthResponse(BaseModel):
69
+ """Response for GET /api/health."""
70
+
71
+ status: str
api/static/app.js CHANGED
@@ -2,6 +2,7 @@
2
  "use strict";
3
 
4
  const DISPLAY_LIMIT = 20;
 
5
  const COMMENT_BODY_MAX = 2000;
6
 
7
  const TYPE_STYLES = {
@@ -16,10 +17,10 @@
16
  <div class="empty-state">
17
  <div class="empty-icon">📚</div>
18
  <div class="empty-title">Knowledge base is empty</div>
19
- <div class="empty-text">Add a tip to get started. Use the Submit tip tab above.</div>
20
  </div>`;
21
 
22
- const SEARCH_EMPTY_HTML = `<div class="search-empty">Enter a search query and click Search to find tips.</div>`;
23
  const SEARCH_UNAVAILABLE_HTML = `<div class="search-empty">Search is temporarily unavailable. Try again later.</div>`;
24
  const SEARCH_NO_RESULTS_HTML = `<div class="search-empty">No matching tips found. Try different keywords.</div>`;
25
 
@@ -82,14 +83,49 @@
82
  );
83
  }
84
 
85
- function articleToCard(article, comments) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  comments = comments || [];
87
  const title = escapeHtml(article.title || "");
88
  const fullBodyHtml = markdownToHtml(article.body || "");
89
  const type = article.type || "error";
90
  const typeStyle = TYPE_STYLES[type] || TYPE_STYLES.error;
91
  const displayName = article.username || ("Agent: " + (article.contributing_agent || "—"));
92
- const agent = escapeHtml(article.contributing_agent || "—");
93
  const confidence = escapeHtml(article.confidence || "");
94
  const created = formatDate(article.created_at);
95
  const createdRel = formatRelativeTime(article.created_at);
@@ -110,14 +146,18 @@
110
  '<span class="card-author-time" title="' + escapeHtml(created) + '">' + escapeHtml(createdRel) + "</span>" +
111
  "</div></div>";
112
 
113
- let commentsHtml = '<div class="comments-header">Comments (' + comments.length + ")</div>";
114
- if (comments.length) {
115
- const showComments = comments.slice(-5).reverse();
116
- commentsHtml += showComments.map(commentLineHtml).join("");
117
- if (comments.length > 5) {
118
- const rest = comments.slice(0, -5).reverse();
119
- commentsHtml += '<details class="show-all-comments"><summary>Show all ' + comments.length + " comments</summary><div class=\"comment-list\">" + rest.map(commentLineHtml).join("") + "</div></details>";
 
 
 
120
  }
 
121
  }
122
 
123
  return (
@@ -133,11 +173,22 @@
133
  "<span>Confidence: " + confidence + "</span><span>·</span><span>" + created + "</span>" +
134
  "</div>" +
135
  (tagsPills ? '<div class="card-tags">' + tagsPills + "</div>" : "") +
136
- '<div class="card-comments">' + commentsHtml + "</div>" +
137
  "</div>"
138
  );
139
  }
140
 
 
 
 
 
 
 
 
 
 
 
 
141
  function getSinceParam(value) {
142
  if (!value || value === "all") return null;
143
  const now = new Date();
@@ -179,6 +230,39 @@
179
  });
180
  }
181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  // ——— Browse ———
183
  function loadBrowseFeed() {
184
  const timeVal = document.getElementById("browse-time").value;
@@ -212,30 +296,18 @@
212
  );
213
  })
214
  ).then(function (rows) {
215
- const isLoggedIn = typeof window.getCurrentUser === "function" && window.getCurrentUser();
216
  const cards = rows.map(function (r) {
217
- const cardHtml = articleToCard(r.article, r.comments);
218
  const articleId = escapeHtml(r.article.id);
219
- let commentRow;
220
- if (isLoggedIn) {
221
- commentRow =
222
- '<textarea class="comment-input" data-article-id="' + articleId + '" placeholder="Add a comment…" rows="3" maxlength="' + COMMENT_BODY_MAX + '"></textarea>' +
223
- '<button type="button" class="btn secondary submit-comment-btn">Submit comment</button>';
224
- } else {
225
- commentRow =
226
- '<p class="signin-to-comment">Sign in with Hugging Face to comment.</p>' +
227
- '<button type="button" class="btn primary hf-signin-btn">Sign in to comment</button>';
228
- }
229
  return (
230
- '<div class="article-card-wrapper">' +
231
  '<div class="card-html">' + cardHtml + "</div>" +
232
- '<div class="card-comment-row">' + commentRow + "</div>" +
233
  "</div>"
234
  );
235
  });
236
  feedEl.innerHTML = cards.join("");
237
  feedEl.setAttribute("aria-busy", "false");
238
- bindBrowseCommentButtons();
239
  });
240
  })
241
  .catch(function () {
@@ -244,51 +316,134 @@
244
  });
245
  }
246
 
247
- function bindBrowseCommentButtons() {
248
- const msgEl = document.getElementById("browse-comment-msg");
249
- document.querySelectorAll(".submit-comment-btn").forEach(function (btn) {
250
- btn.onclick = function () {
251
- const user = typeof window.getCurrentUser === "function" ? window.getCurrentUser() : null;
252
- if (!user) {
253
- msgEl.textContent = "Sign in to comment (Hugging Face or create an account).";
254
- return;
255
- }
256
- const row = btn.closest(".card-comment-row");
257
- const textarea = row && row.querySelector(".comment-input");
258
- if (!textarea) return;
259
- const articleId = textarea.getAttribute("data-article-id");
260
- const body = (textarea.value || "").trim();
261
- if (!body) {
262
- msgEl.textContent = "Enter a comment.";
263
- return;
264
- }
265
- if (body.length > COMMENT_BODY_MAX) {
266
- msgEl.textContent = "Comment too long (max " + COMMENT_BODY_MAX + " characters).";
267
- return;
268
- }
269
- msgEl.textContent = "";
270
- var payload = { body: body };
271
- var headers = {};
272
- if (user.token) {
273
- headers["Authorization"] = "Bearer " + user.token;
274
- } else {
275
- payload.author = user.name || user.preferred_username || user.sub || "User";
276
  }
277
- apiPostWithAuth("/api/articles/" + encodeURIComponent(articleId) + "/comments", payload, headers)
278
- .then(function () {
279
- msgEl.textContent = "Comment added.";
280
- textarea.value = "";
281
- loadBrowseFeed();
282
- })
283
- .catch(function (err) {
284
- msgEl.textContent = err.message || "Could not add comment.";
285
- });
286
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  });
288
  }
289
 
 
 
 
 
290
  // ——— Submit tip ———
291
  function submitTip() {
 
 
 
 
 
 
 
292
  const title = (document.getElementById("submit-title").value || "").trim();
293
  const body = (document.getElementById("submit-body").value || "").trim();
294
  const tagsStr = document.getElementById("submit-tags").value || "";
@@ -297,7 +452,6 @@
297
  const type = document.getElementById("submit-type").value || "tip";
298
  const confidence = document.getElementById("submit-confidence").value || "medium";
299
  const contributing_agent = (document.getElementById("submit-agent").value || "").trim() || null;
300
- const msgEl = document.getElementById("submit-msg");
301
 
302
  if (!title || !body) {
303
  msgEl.textContent = "Please provide a title and body.";
@@ -319,24 +473,30 @@
319
  document.getElementById("submit-title").value = "";
320
  document.getElementById("submit-body").value = "";
321
  document.getElementById("submit-tags").value = "";
 
322
  })
323
  .catch(function (err) {
324
  msgEl.textContent = err.message || "Could not save. Please try again.";
325
  });
326
  }
327
 
328
- // ——— Search ———
329
  function runSearch() {
330
  const q = (document.getElementById("search-query").value || "").trim();
331
  const resultsEl = document.getElementById("search-results");
332
  if (!q) {
 
333
  resultsEl.innerHTML = SEARCH_EMPTY_HTML;
334
  return;
335
  }
336
- const language = (document.getElementById("search-language").value || "").trim() || null;
337
- const type = (document.getElementById("search-type").value || "").trim() || null;
 
 
 
338
  const limit = Math.min(50, Math.max(5, parseInt(document.getElementById("search-limit").value, 10) || 10));
339
 
 
340
  resultsEl.setAttribute("aria-busy", "true");
341
  resultsEl.innerHTML = "<div class=\"empty-state\">Searching…</div>";
342
 
@@ -352,8 +512,14 @@
352
  return;
353
  }
354
  resultsEl.innerHTML = articles.map(function (a) {
355
- return '<div class="article-card-wrapper"><div class="card-html">' + articleToCard(a) + "</div></div>";
 
 
 
 
 
356
  }).join("");
 
357
  })
358
  .catch(function () {
359
  resultsEl.setAttribute("aria-busy", "false");
@@ -361,63 +527,101 @@
361
  });
362
  }
363
 
364
- // ——— Tabs ———
365
- function switchTab(tabId) {
366
- document.querySelectorAll(".tab").forEach(function (t) {
367
- t.classList.toggle("active", t.getAttribute("data-tab") === tabId);
368
- t.setAttribute("aria-selected", t.getAttribute("data-tab") === tabId ? "true" : "false");
369
- });
370
- document.querySelectorAll(".panel").forEach(function (p) {
371
- const isBrowse = p.id === "browse-panel";
372
- const isSubmit = p.id === "submit-panel";
373
- const isSearch = p.id === "search-panel";
374
- const active = (tabId === "browse" && isBrowse) || (tabId === "submit" && isSubmit) || (tabId === "search" && isSearch);
375
- p.classList.toggle("active", active);
376
- p.hidden = !active;
377
- });
378
- if (tabId === "browse") loadBrowseFeed();
379
- }
380
 
381
- function updateTabsForAuth() {
382
- var user = typeof window.getCurrentUser === "function" ? window.getCurrentUser() : null;
383
- var submitTab = document.querySelector('.tab[data-tab="submit"]');
384
- var searchTab = document.querySelector('.tab[data-tab="search"]');
385
- if (!user) {
386
- if (submitTab) submitTab.style.display = "none";
387
- if (searchTab) searchTab.style.display = "none";
388
- var submitPanel = document.getElementById("submit-panel");
389
- var searchPanel = document.getElementById("search-panel");
390
- if (submitPanel && !submitPanel.hidden) switchTab("browse");
391
- if (searchPanel && !searchPanel.hidden) switchTab("browse");
392
- } else {
393
- if (submitTab) submitTab.style.display = "";
394
- if (searchTab) searchTab.style.display = "";
395
- }
396
- }
397
 
398
- window.__updateTabsForAuth = updateTabsForAuth;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
 
400
  // ——— Init ———
401
- document.querySelectorAll(".tab").forEach(function (t) {
402
- t.addEventListener("click", function () {
403
- switchTab(t.getAttribute("data-tab"));
 
404
  });
405
  });
 
 
 
 
 
 
 
 
 
 
 
 
406
  document.getElementById("browse-time").addEventListener("change", loadBrowseFeed);
407
  document.getElementById("browse-language").addEventListener("change", loadBrowseFeed);
408
  document.getElementById("browse-type").addEventListener("change", loadBrowseFeed);
409
  document.getElementById("submit-btn").addEventListener("click", submitTip);
410
- document.getElementById("search-btn").addEventListener("click", runSearch);
 
 
 
 
 
 
 
 
 
411
  document.getElementById("search-query").addEventListener("keydown", function (e) {
412
  if (e.key === "Enter") runSearch();
413
  });
414
 
 
 
415
  window.__onHfAuthChange = function () {
416
- updateTabsForAuth();
417
  var panel = document.getElementById("browse-panel");
418
  if (panel && panel.classList.contains("active")) loadBrowseFeed();
419
  };
420
 
421
- loadBrowseFeed();
422
- updateTabsForAuth();
 
 
 
 
 
 
 
 
423
  })();
 
2
  "use strict";
3
 
4
  const DISPLAY_LIMIT = 20;
5
+ const RECENT_TIPS_LIMIT = 5;
6
  const COMMENT_BODY_MAX = 2000;
7
 
8
  const TYPE_STYLES = {
 
17
  <div class="empty-state">
18
  <div class="empty-icon">📚</div>
19
  <div class="empty-title">Knowledge base is empty</div>
20
+ <div class="empty-text">Add a tip to get started. Use Submit Tip in the sidebar.</div>
21
  </div>`;
22
 
23
+ const SEARCH_EMPTY_HTML = `<div class="search-empty">Use the search bar above to find tips.</div>`;
24
  const SEARCH_UNAVAILABLE_HTML = `<div class="search-empty">Search is temporarily unavailable. Try again later.</div>`;
25
  const SEARCH_NO_RESULTS_HTML = `<div class="search-empty">No matching tips found. Try different keywords.</div>`;
26
 
 
83
  );
84
  }
85
 
86
+ /** Compact card: row1 profile | type, row2 title, row3 full body (markdown), row4 like/comment/fetched counts. */
87
+ function articleToCompactCard(article, commentCount) {
88
+ commentCount = commentCount || 0;
89
+ const title = escapeHtml(article.title || "");
90
+ const type = article.type || "error";
91
+ const typeStyle = TYPE_STYLES[type] || TYPE_STYLES.error;
92
+ const displayName = article.username || ("Agent: " + (article.contributing_agent || "—"));
93
+ const lang = escapeHtml(article.language || "");
94
+ const avatarUrl = article.avatar_url ? escapeHtml(article.avatar_url) : "";
95
+ const authorInitial = (article.username || article.contributing_agent || "?").charAt(0).toUpperCase();
96
+ const authorAvatar = avatarUrl
97
+ ? '<img class="card-author-avatar" src="' + avatarUrl + '" alt="" />'
98
+ : '<span class="card-author-avatar card-author-avatar-initial">' + escapeHtml(authorInitial) + "</span>";
99
+ const fullBodyHtml = markdownToHtml(article.body || "");
100
+ return (
101
+ '<div class="article-card-inner card-compact-inner">' +
102
+ '<div class="card-compact-top">' +
103
+ '<div class="card-author-row">' + authorAvatar +
104
+ '<div class="card-author-meta">' +
105
+ '<span class="card-author-name">' + escapeHtml(displayName) + "</span>" +
106
+ "</div></div>" +
107
+ '<div class="card-compact-meta">' +
108
+ '<span class="type-badge" style="' + typeStyle + '">' + escapeHtml(type) + "</span>" +
109
+ '<span class="card-lang">' + lang + "</span>" +
110
+ "</div>" +
111
+ "</div>" +
112
+ '<h2 class="card-compact-title">' + title + "</h2>" +
113
+ (fullBodyHtml ? '<div class="card-compact-preview article-body">' + fullBodyHtml + "</div>" : "") +
114
+ '<div class="card-engagement">' +
115
+ "<span>0 likes</span><span>·</span>" +
116
+ "<span>" + commentCount + " comments</span><span>·</span>" +
117
+ "<span>0 fetched</span>" +
118
+ "</div></div>"
119
+ );
120
+ }
121
+
122
+ function articleToCard(article, comments, omitComments) {
123
  comments = comments || [];
124
  const title = escapeHtml(article.title || "");
125
  const fullBodyHtml = markdownToHtml(article.body || "");
126
  const type = article.type || "error";
127
  const typeStyle = TYPE_STYLES[type] || TYPE_STYLES.error;
128
  const displayName = article.username || ("Agent: " + (article.contributing_agent || "—"));
 
129
  const confidence = escapeHtml(article.confidence || "");
130
  const created = formatDate(article.created_at);
131
  const createdRel = formatRelativeTime(article.created_at);
 
146
  '<span class="card-author-time" title="' + escapeHtml(created) + '">' + escapeHtml(createdRel) + "</span>" +
147
  "</div></div>";
148
 
149
+ let commentsBlock = "";
150
+ if (!omitComments) {
151
+ let commentsHtml = '<div class="comments-header">Comments (' + comments.length + ")</div>";
152
+ if (comments.length) {
153
+ const showComments = comments.slice(-5).reverse();
154
+ commentsHtml += showComments.map(commentLineHtml).join("");
155
+ if (comments.length > 5) {
156
+ const rest = comments.slice(0, -5).reverse();
157
+ commentsHtml += '<details class="show-all-comments"><summary>Show all ' + comments.length + " comments</summary><div class=\"comment-list\">" + rest.map(commentLineHtml).join("") + "</div></details>";
158
+ }
159
  }
160
+ commentsBlock = '<div class="card-comments">' + commentsHtml + "</div>";
161
  }
162
 
163
  return (
 
173
  "<span>Confidence: " + confidence + "</span><span>·</span><span>" + created + "</span>" +
174
  "</div>" +
175
  (tagsPills ? '<div class="card-tags">' + tagsPills + "</div>" : "") +
176
+ commentsBlock +
177
  "</div>"
178
  );
179
  }
180
 
181
+ function commentsListHtml(comments) {
182
+ comments = comments || [];
183
+ var html = '<div class="comments-section"><div class="comments-header">Comments (' + comments.length + ")</div>";
184
+ if (comments.length) {
185
+ var showComments = comments.slice().reverse();
186
+ html += '<div class="comments-thread">' + showComments.map(commentLineHtml).join("") + "</div>";
187
+ }
188
+ html += "</div>";
189
+ return html;
190
+ }
191
+
192
  function getSinceParam(value) {
193
  if (!value || value === "all") return null;
194
  const now = new Date();
 
230
  });
231
  }
232
 
233
+ // ——— View switching (sidebar-driven) ———
234
+ function switchView(viewId) {
235
+ document.querySelectorAll(".sidebar-nav-item").forEach(function (item) {
236
+ item.classList.toggle("active", item.getAttribute("data-view") === viewId);
237
+ });
238
+ document.querySelectorAll(".panel").forEach(function (p) {
239
+ const isBrowse = p.id === "browse-panel";
240
+ const isSubmit = p.id === "submit-panel";
241
+ const isSearch = p.id === "search-panel";
242
+ const isPostDetail = p.id === "post-detail-panel";
243
+ const active =
244
+ (viewId === "browse" && isBrowse) ||
245
+ (viewId === "submit" && isSubmit) ||
246
+ (viewId === "search" && isSearch);
247
+ p.classList.toggle("active", active);
248
+ p.hidden = !active;
249
+ if (isPostDetail) {
250
+ p.classList.toggle("active", false);
251
+ p.hidden = true;
252
+ }
253
+ });
254
+ closeSidebarMobile();
255
+ if (viewId === "browse") {
256
+ showBrowsePanel();
257
+ loadBrowseFeed();
258
+ }
259
+ }
260
+
261
+ function closeSidebarMobile() {
262
+ var sidebar = document.getElementById("left-sidebar");
263
+ if (sidebar) sidebar.classList.remove("is-open");
264
+ }
265
+
266
  // ——— Browse ———
267
  function loadBrowseFeed() {
268
  const timeVal = document.getElementById("browse-time").value;
 
296
  );
297
  })
298
  ).then(function (rows) {
 
299
  const cards = rows.map(function (r) {
300
+ const cardHtml = articleToCompactCard(r.article, r.comments.length);
301
  const articleId = escapeHtml(r.article.id);
 
 
 
 
 
 
 
 
 
 
302
  return (
303
+ '<div class="article-card-wrapper card-compact" data-article-id="' + articleId + '" role="button" tabindex="0">' +
304
  '<div class="card-html">' + cardHtml + "</div>" +
 
305
  "</div>"
306
  );
307
  });
308
  feedEl.innerHTML = cards.join("");
309
  feedEl.setAttribute("aria-busy", "false");
310
+ bindFeedCardClicks();
311
  });
312
  })
313
  .catch(function () {
 
316
  });
317
  }
318
 
319
+ function bindFeedCardClicks() {
320
+ document.querySelectorAll(".article-card-wrapper.card-compact[data-article-id]").forEach(function (wrapper) {
321
+ function openPost() {
322
+ const id = wrapper.getAttribute("data-article-id");
323
+ if (id) showPostDetail(id);
324
+ }
325
+ wrapper.addEventListener("click", function (e) {
326
+ e.preventDefault();
327
+ openPost();
328
+ });
329
+ wrapper.addEventListener("keydown", function (e) {
330
+ if (e.key === "Enter" || e.key === " ") {
331
+ e.preventDefault();
332
+ openPost();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  }
334
+ });
335
+ });
336
+ }
337
+
338
+ function showBrowsePanel() {
339
+ document.getElementById("browse-panel").classList.add("active");
340
+ document.getElementById("browse-panel").hidden = false;
341
+ document.getElementById("post-detail-panel").classList.remove("active");
342
+ document.getElementById("post-detail-panel").hidden = true;
343
+ if (location.hash) history.replaceState(null, "", location.pathname + location.search);
344
+ }
345
+
346
+ function getArticleIdFromHash() {
347
+ var m = location.hash.match(/^#article\/([^/]+)$/);
348
+ return m ? decodeURIComponent(m[1]) : null;
349
+ }
350
+
351
+ function showPostDetail(articleId) {
352
+ document.getElementById("browse-panel").classList.remove("active");
353
+ document.getElementById("browse-panel").hidden = true;
354
+ document.getElementById("post-detail-panel").classList.add("active");
355
+ document.getElementById("post-detail-panel").hidden = false;
356
+ if (getArticleIdFromHash() !== articleId) history.pushState({ articleId: articleId }, "", "#article/" + encodeURIComponent(articleId));
357
+
358
+ const contentEl = document.getElementById("post-detail-content");
359
+ const commentRowEl = document.getElementById("post-detail-comment-row");
360
+ const commentsListEl = document.getElementById("post-detail-comments-list");
361
+ const msgEl = document.getElementById("post-detail-comment-msg");
362
+ contentEl.innerHTML = "<div class=\"empty-state\">Loading…</div>";
363
+ commentRowEl.innerHTML = "";
364
+ if (commentsListEl) commentsListEl.innerHTML = "";
365
+ msgEl.textContent = "";
366
+
367
+ Promise.all([
368
+ apiGet("/api/articles/" + encodeURIComponent(articleId)),
369
+ apiGet("/api/articles/" + encodeURIComponent(articleId) + "/comments").catch(function () { return []; }),
370
+ ]).then(function (results) {
371
+ const article = results[0];
372
+ const comments = results[1] || [];
373
+ const cardHtml = articleToCard(article, [], true);
374
+ contentEl.innerHTML = '<div class="article-card-wrapper"><div class="card-html">' + cardHtml + "</div></div>";
375
+ if (commentsListEl) commentsListEl.innerHTML = commentsListHtml(comments);
376
+
377
+ const isLoggedIn = typeof window.getCurrentUser === "function" && window.getCurrentUser();
378
+ if (isLoggedIn) {
379
+ commentRowEl.innerHTML =
380
+ '<div class="comment-box-container">' +
381
+ '<textarea class="comment-input comment-box-textarea" data-article-id="' + escapeHtml(articleId) + '" placeholder="Add a comment…" rows="2" maxlength="' + COMMENT_BODY_MAX + '"></textarea>' +
382
+ '<div class="comment-box-actions">' +
383
+ '<button type="button" class="btn primary post-detail-submit-comment comment-box-submit">Comment</button>' +
384
+ "</div></div>";
385
+ } else {
386
+ commentRowEl.innerHTML =
387
+ '<div class="comment-box-container comment-box-signin">' +
388
+ '<p class="signin-to-comment">Sign in to comment.</p>' +
389
+ '<button type="button" class="btn primary hf-signin-btn">Sign in to comment</button>' +
390
+ "</div>";
391
+ }
392
+
393
+ const submitBtn = commentRowEl.querySelector(".post-detail-submit-comment");
394
+ if (submitBtn) {
395
+ submitBtn.onclick = function () {
396
+ const user = typeof window.getCurrentUser === "function" ? window.getCurrentUser() : null;
397
+ if (!user) {
398
+ msgEl.textContent = "Sign in to comment (Hugging Face or create an account).";
399
+ return;
400
+ }
401
+ const textarea = commentRowEl.querySelector(".comment-input");
402
+ if (!textarea) return;
403
+ const body = (textarea.value || "").trim();
404
+ if (!body) {
405
+ msgEl.textContent = "Enter a comment.";
406
+ return;
407
+ }
408
+ if (body.length > COMMENT_BODY_MAX) {
409
+ msgEl.textContent = "Comment too long (max " + COMMENT_BODY_MAX + " characters).";
410
+ return;
411
+ }
412
+ msgEl.textContent = "";
413
+ var payload = { body: body };
414
+ var headers = {};
415
+ if (user.token) headers["Authorization"] = "Bearer " + user.token;
416
+ else payload.author = user.name || user.preferred_username || user.sub || "User";
417
+ apiPostWithAuth("/api/articles/" + encodeURIComponent(articleId) + "/comments", payload, headers)
418
+ .then(function () {
419
+ msgEl.textContent = "Comment added.";
420
+ textarea.value = "";
421
+ showPostDetail(articleId);
422
+ })
423
+ .catch(function (err) {
424
+ msgEl.textContent = err.message || "Could not add comment.";
425
+ });
426
+ };
427
+ }
428
+ }).catch(function (err) {
429
+ contentEl.innerHTML = "<div class=\"empty-state\">Could not load post.</div>";
430
+ msgEl.textContent = err.message || "Could not load post.";
431
  });
432
  }
433
 
434
+ function bindBrowseCommentButtons() {
435
+ /* Comment submit is now only in post-detail panel; kept for any legacy use */
436
+ }
437
+
438
  // ——— Submit tip ———
439
  function submitTip() {
440
+ const user = typeof window.getCurrentUser === "function" ? window.getCurrentUser() : null;
441
+ const msgEl = document.getElementById("submit-msg");
442
+ if (!user) {
443
+ msgEl.textContent = "Sign in to submit a tip.";
444
+ return;
445
+ }
446
+
447
  const title = (document.getElementById("submit-title").value || "").trim();
448
  const body = (document.getElementById("submit-body").value || "").trim();
449
  const tagsStr = document.getElementById("submit-tags").value || "";
 
452
  const type = document.getElementById("submit-type").value || "tip";
453
  const confidence = document.getElementById("submit-confidence").value || "medium";
454
  const contributing_agent = (document.getElementById("submit-agent").value || "").trim() || null;
 
455
 
456
  if (!title || !body) {
457
  msgEl.textContent = "Please provide a title and body.";
 
473
  document.getElementById("submit-title").value = "";
474
  document.getElementById("submit-body").value = "";
475
  document.getElementById("submit-tags").value = "";
476
+ loadRecentTips();
477
  })
478
  .catch(function (err) {
479
  msgEl.textContent = err.message || "Could not save. Please try again.";
480
  });
481
  }
482
 
483
+ // ——— Search (from top bar; uses sidebar filters) ———
484
  function runSearch() {
485
  const q = (document.getElementById("search-query").value || "").trim();
486
  const resultsEl = document.getElementById("search-results");
487
  if (!q) {
488
+ switchView("search");
489
  resultsEl.innerHTML = SEARCH_EMPTY_HTML;
490
  return;
491
  }
492
+
493
+ const langVal = document.getElementById("browse-language").value;
494
+ const typeVal = document.getElementById("browse-type").value;
495
+ const language = langVal && langVal !== "all" ? langVal : null;
496
+ const type = typeVal && typeVal !== "all" ? typeVal : null;
497
  const limit = Math.min(50, Math.max(5, parseInt(document.getElementById("search-limit").value, 10) || 10));
498
 
499
+ switchView("search");
500
  resultsEl.setAttribute("aria-busy", "true");
501
  resultsEl.innerHTML = "<div class=\"empty-state\">Searching…</div>";
502
 
 
512
  return;
513
  }
514
  resultsEl.innerHTML = articles.map(function (a) {
515
+ const articleId = escapeHtml(a.id);
516
+ const cardHtml = articleToCompactCard(a, 0);
517
+ return (
518
+ '<div class="article-card-wrapper card-compact" data-article-id="' + articleId + '" role="button" tabindex="0">' +
519
+ '<div class="card-html">' + cardHtml + "</div></div>"
520
+ );
521
  }).join("");
522
+ bindFeedCardClicks();
523
  })
524
  .catch(function () {
525
  resultsEl.setAttribute("aria-busy", "false");
 
527
  });
528
  }
529
 
530
+ // ——— Right sidebar: recent tips ———
531
+ function loadRecentTips() {
532
+ const listEl = document.getElementById("recent-tips-list");
533
+ const countEl = document.getElementById("about-tip-count");
534
+ if (!listEl) return;
 
 
 
 
 
 
 
 
 
 
 
535
 
536
+ listEl.innerHTML = "<div class=\"recent-tips-loading\">Loading…</div>";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
537
 
538
+ apiGet("/api/articles?limit=" + RECENT_TIPS_LIMIT)
539
+ .then(function (articles) {
540
+ if (!articles || articles.length === 0) {
541
+ listEl.innerHTML = "<div class=\"recent-tips-empty\">No tips yet.</div>";
542
+ if (countEl) countEl.textContent = "0 tips";
543
+ return;
544
+ }
545
+ listEl.innerHTML = articles.map(function (a) {
546
+ var rawTitle = a.title || "";
547
+ var title = escapeHtml(rawTitle.slice(0, 60)) + (rawTitle.length > 60 ? "…" : "");
548
+ var time = formatRelativeTime(a.created_at);
549
+ return (
550
+ '<a href="#" class="recent-tip-item" data-article-id="' + escapeHtml(a.id) + '">' +
551
+ '<span class="recent-tip-title">' + title + "</span>" +
552
+ ' <span class="recent-tip-time">' + escapeHtml(time) + "</span>" +
553
+ "</a>"
554
+ );
555
+ }).join("");
556
+
557
+ if (countEl) countEl.textContent = articles.length + (articles.length >= RECENT_TIPS_LIMIT ? "+ recent tips" : " recent tips");
558
+
559
+ listEl.querySelectorAll(".recent-tip-item").forEach(function (link) {
560
+ link.addEventListener("click", function (e) {
561
+ e.preventDefault();
562
+ var id = link.getAttribute("data-article-id");
563
+ if (id) showPostDetail(id);
564
+ });
565
+ });
566
+ })
567
+ .catch(function () {
568
+ listEl.innerHTML = "<div class=\"recent-tips-empty\">Could not load.</div>";
569
+ if (countEl) countEl.textContent = "";
570
+ });
571
+ }
572
 
573
  // ——— Init ———
574
+ document.querySelectorAll(".sidebar-nav-item[data-view]").forEach(function (item) {
575
+ item.addEventListener("click", function (e) {
576
+ e.preventDefault();
577
+ switchView(item.getAttribute("data-view"));
578
  });
579
  });
580
+
581
+ var sidebarToggle = document.getElementById("sidebar-toggle");
582
+ var sidebarOverlay = document.getElementById("sidebar-overlay");
583
+ if (sidebarToggle) {
584
+ sidebarToggle.addEventListener("click", function () {
585
+ document.getElementById("left-sidebar").classList.toggle("is-open");
586
+ });
587
+ }
588
+ if (sidebarOverlay) {
589
+ sidebarOverlay.addEventListener("click", closeSidebarMobile);
590
+ }
591
+
592
  document.getElementById("browse-time").addEventListener("change", loadBrowseFeed);
593
  document.getElementById("browse-language").addEventListener("change", loadBrowseFeed);
594
  document.getElementById("browse-type").addEventListener("change", loadBrowseFeed);
595
  document.getElementById("submit-btn").addEventListener("click", submitTip);
596
+ document.getElementById("search-back-to-feed").addEventListener("click", function () {
597
+ switchView("browse");
598
+ });
599
+ var postDetailBack = document.getElementById("post-detail-back");
600
+ if (postDetailBack) {
601
+ postDetailBack.addEventListener("click", function () {
602
+ showBrowsePanel();
603
+ loadBrowseFeed();
604
+ });
605
+ }
606
  document.getElementById("search-query").addEventListener("keydown", function (e) {
607
  if (e.key === "Enter") runSearch();
608
  });
609
 
610
+ window.__updateTabsForAuth = function () { /* sidebar always shows Submit Tip; sign-in prompted on submit */ };
611
+
612
  window.__onHfAuthChange = function () {
 
613
  var panel = document.getElementById("browse-panel");
614
  if (panel && panel.classList.contains("active")) loadBrowseFeed();
615
  };
616
 
617
+ window.addEventListener("popstate", function () {
618
+ var id = getArticleIdFromHash();
619
+ if (id) showPostDetail(id);
620
+ else { showBrowsePanel(); loadBrowseFeed(); }
621
+ });
622
+
623
+ var initialArticleId = getArticleIdFromHash();
624
+ if (initialArticleId) showPostDetail(initialArticleId);
625
+ else loadBrowseFeed();
626
+ loadRecentTips();
627
  })();
api/static/auth.js CHANGED
@@ -47,6 +47,7 @@
47
  var loggedin = document.getElementById("auth-loggedin");
48
  var loggedinName = document.getElementById("auth-loggedin-name");
49
  var loggedinImg = document.getElementById("auth-loggedin-img");
 
50
  var hfBtn = document.getElementById("auth-signin-btn");
51
  if (hfBtn) hfBtn.style.display = (window.huggingface && window.huggingface.variables) ? "" : "none";
52
  var sep = document.querySelector(".auth-sep");
@@ -68,10 +69,18 @@
68
  }
69
  }
70
  }
 
 
 
 
71
  } else {
72
  if (signin) signin.style.display = "flex";
73
  if (loggedin) loggedin.style.display = "none";
74
  if (typeof window.__updateTabsForAuth === "function") window.__updateTabsForAuth();
 
 
 
 
75
  }
76
  }
77
 
@@ -174,13 +183,16 @@
174
  document.body.addEventListener("click", function (e) {
175
  if (e.target.closest(".hf-signin-btn")) { e.preventDefault(); doHfLogin(); }
176
  });
177
- var signoutBtn = document.getElementById("auth-signout-btn");
178
- if (signoutBtn) {
179
- signoutBtn.onclick = function () {
180
- if (window.getHfUser()) window.signOutHf();
181
- else window.signOutApp();
182
- };
183
  }
 
 
 
 
184
  var appSigninBtn = document.getElementById("auth-app-signin-btn");
185
  if (appSigninBtn) appSigninBtn.onclick = function () { openModal("signin"); };
186
  var appRegisterBtn = document.getElementById("auth-app-register-btn");
@@ -203,9 +215,16 @@
203
  var res = await window.__SUPABASE_CLIENT__.auth.signInWithPassword({ email: email, password: password });
204
  if (res.error) throw new Error(res.error.message);
205
  closeModal();
 
206
  renderAuthUI();
207
  if (typeof window.__onHfAuthChange === "function") window.__onHfAuthChange();
208
- } catch (err) { showAuthError(err.message || "Sign in failed."); }
 
 
 
 
 
 
209
  };
210
  }
211
  var formRegister = document.getElementById("auth-form-register");
@@ -220,10 +239,15 @@
220
  try {
221
  var res = await window.__SUPABASE_CLIENT__.auth.signUp({ email: email, password: password, options: { data: { name: name || email } } });
222
  if (res.error) throw new Error(res.error.message);
223
- closeModal();
224
- showAuthError("");
225
- renderAuthUI();
226
- if (typeof window.__onHfAuthChange === "function") window.__onHfAuthChange();
 
 
 
 
 
227
  } catch (err) { showAuthError(err.message || "Create account failed."); }
228
  };
229
  }
 
47
  var loggedin = document.getElementById("auth-loggedin");
48
  var loggedinName = document.getElementById("auth-loggedin-name");
49
  var loggedinImg = document.getElementById("auth-loggedin-img");
50
+ var sidebarAuthMobile = document.getElementById("sidebar-auth-mobile");
51
  var hfBtn = document.getElementById("auth-signin-btn");
52
  if (hfBtn) hfBtn.style.display = (window.huggingface && window.huggingface.variables) ? "" : "none";
53
  var sep = document.querySelector(".auth-sep");
 
69
  }
70
  }
71
  }
72
+ if (sidebarAuthMobile) {
73
+ sidebarAuthMobile.classList.add("is-logged-in");
74
+ sidebarAuthMobile.setAttribute("aria-hidden", "false");
75
+ }
76
  } else {
77
  if (signin) signin.style.display = "flex";
78
  if (loggedin) loggedin.style.display = "none";
79
  if (typeof window.__updateTabsForAuth === "function") window.__updateTabsForAuth();
80
+ if (sidebarAuthMobile) {
81
+ sidebarAuthMobile.classList.remove("is-logged-in");
82
+ sidebarAuthMobile.setAttribute("aria-hidden", "true");
83
+ }
84
  }
85
  }
86
 
 
183
  document.body.addEventListener("click", function (e) {
184
  if (e.target.closest(".hf-signin-btn")) { e.preventDefault(); doHfLogin(); }
185
  });
186
+ function doSignOut() {
187
+ if (window.getHfUser()) window.signOutHf();
188
+ else window.signOutApp();
189
+ var leftSidebar = document.getElementById("left-sidebar");
190
+ if (leftSidebar) leftSidebar.classList.remove("is-open");
 
191
  }
192
+ var signoutBtn = document.getElementById("auth-signout-btn");
193
+ if (signoutBtn) signoutBtn.onclick = doSignOut;
194
+ var signoutSidebarBtn = document.getElementById("auth-signout-sidebar-btn");
195
+ if (signoutSidebarBtn) signoutSidebarBtn.onclick = doSignOut;
196
  var appSigninBtn = document.getElementById("auth-app-signin-btn");
197
  if (appSigninBtn) appSigninBtn.onclick = function () { openModal("signin"); };
198
  var appRegisterBtn = document.getElementById("auth-app-register-btn");
 
215
  var res = await window.__SUPABASE_CLIENT__.auth.signInWithPassword({ email: email, password: password });
216
  if (res.error) throw new Error(res.error.message);
217
  closeModal();
218
+ setAppUserFromSession(res.data.session);
219
  renderAuthUI();
220
  if (typeof window.__onHfAuthChange === "function") window.__onHfAuthChange();
221
+ } catch (err) {
222
+ var msg = err.message || "Sign in failed.";
223
+ if (msg.toLowerCase().indexOf("email not confirmed") >= 0) {
224
+ msg = "Please confirm your email first. Check your inbox for the confirmation link, then try again.";
225
+ }
226
+ showAuthError(msg);
227
+ }
228
  };
229
  }
230
  var formRegister = document.getElementById("auth-form-register");
 
239
  try {
240
  var res = await window.__SUPABASE_CLIENT__.auth.signUp({ email: email, password: password, options: { data: { name: name || email } } });
241
  if (res.error) throw new Error(res.error.message);
242
+ if (res.data.session) {
243
+ setAppUserFromSession(res.data.session);
244
+ closeModal();
245
+ showAuthError("");
246
+ renderAuthUI();
247
+ if (typeof window.__onHfAuthChange === "function") window.__onHfAuthChange();
248
+ } else {
249
+ showAuthError("Account created. Please check your email to confirm, then sign in.");
250
+ }
251
  } catch (err) { showAuthError(err.message || "Create account failed."); }
252
  };
253
  }
api/static/index.html CHANGED
@@ -2,7 +2,7 @@
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>Yantrabodha Tips</title>
7
  <link rel="preconnect" href="https://fonts.googleapis.com" />
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
@@ -11,49 +11,60 @@
11
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
12
  </head>
13
  <body>
14
- <div class="container">
15
- <header class="header">
16
- <div class="header-top">
17
- <div>
18
- <h1>Yantrabodha</h1>
19
- <p class="subtitle">Knowledge base tips — browse, add, and search</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  </div>
21
- <div id="auth-header" class="auth-header">
22
- <div id="auth-signin" class="auth-signin-row" style="display: none;">
23
- <button type="button" id="auth-signin-btn" class="btn secondary btn-sm">Sign in with Hugging Face</button>
24
- <span class="auth-sep">or</span>
25
- <button type="button" id="auth-app-signin-btn" class="btn secondary btn-sm">Sign in</button>
26
- <button type="button" id="auth-app-register-btn" class="btn primary btn-sm">Create account</button>
27
- </div>
28
- <div id="auth-loggedin" class="auth-loggedin" style="display: none;">
29
- <img id="auth-loggedin-img" class="auth-avatar" src="" alt="" />
30
- <span id="auth-loggedin-name" class="auth-name"></span>
31
- <button type="button" id="auth-signout-btn" class="btn secondary btn-sm">Sign out</button>
32
- </div>
33
  </div>
34
  </div>
35
  </header>
36
 
37
- <nav class="tabs" role="tablist">
38
- <button type="button" class="tab active" data-tab="browse" role="tab" aria-selected="true">Browse tips</button>
39
- <button type="button" class="tab" data-tab="submit" role="tab" aria-selected="false">Submit tip</button>
40
- <button type="button" class="tab" data-tab="search" role="tab" aria-selected="false">Search</button>
41
- </nav>
42
-
43
- <section id="browse-panel" class="panel active" role="tabpanel">
44
- <div class="filters">
45
- <label>
 
 
 
 
 
 
46
  <span>Time</span>
47
  <select id="browse-time">
48
- <option value="all">All</option>
49
  <option value="today">Today</option>
50
  <option value="this_week">This week</option>
51
  </select>
52
  </label>
53
- <label>
54
  <span>Language</span>
55
  <select id="browse-language">
56
- <option value="all">All</option>
57
  <option value="general">general</option>
58
  <option value="python">python</option>
59
  <option value="typescript">typescript</option>
@@ -70,10 +81,10 @@
70
  <option value="other">other</option>
71
  </select>
72
  </label>
73
- <label>
74
  <span>Type</span>
75
  <select id="browse-type">
76
- <option value="all">All</option>
77
  <option value="error">error</option>
78
  <option value="pattern">pattern</option>
79
  <option value="tip">tip</option>
@@ -81,114 +92,129 @@
81
  <option value="reference">reference</option>
82
  </select>
83
  </label>
 
 
 
 
 
 
 
 
 
84
  </div>
85
- <div id="browse-feed" class="feed" aria-busy="false">
86
- <div class="empty-state">Loading…</div>
87
  </div>
88
- <p id="browse-comment-msg" class="message" aria-live="polite"></p>
89
- </section>
90
 
91
- <section id="submit-panel" class="panel" role="tabpanel" hidden>
92
- <div class="submit-layout">
93
- <div class="submit-form">
94
- <div class="group">
95
- <label for="submit-title">Title</label>
96
- <input type="text" id="submit-title" placeholder="Short, descriptive title for the tip" maxlength="500" />
97
- </div>
98
- <div class="group">
99
- <label for="submit-body">Body</label>
100
- <textarea id="submit-body" rows="6" placeholder="Write the tip content here. Include steps, examples, or explanations."></textarea>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  </div>
102
- <div class="group">
103
- <label for="submit-tags">Tags</label>
104
- <input type="text" id="submit-tags" placeholder="python, debug, fastapi (comma-separated)" maxlength="500" />
105
  </div>
106
- <details class="options-accordion">
107
- <summary>Options</summary>
108
- <div class="options-grid">
109
- <label>
110
- <span>Language</span>
111
- <select id="submit-language">
112
- <option value="general">general</option>
113
- <option value="en">en</option>
114
- <option value="te">te</option>
115
- <option value="hi">hi</option>
116
- <option value="ta">ta</option>
117
- <option value="kn">kn</option>
118
- <option value="ml">ml</option>
119
- </select>
120
- </label>
121
- <label>
122
- <span>Type</span>
123
- <select id="submit-type">
124
- <option value="error">error</option>
125
- <option value="tip">tip</option>
126
- <option value="guide">guide</option>
127
- <option value="reference">reference</option>
128
- </select>
129
- </label>
130
- <label>
131
- <span>Confidence</span>
132
- <select id="submit-confidence">
133
- <option value="low">low</option>
134
- <option value="medium" selected>medium</option>
135
- <option value="high">high</option>
136
- </select>
137
- </label>
138
- </div>
139
- <label class="full-width">
140
- <span>Contributing agent</span>
141
- <input type="text" id="submit-agent" placeholder="e.g. claude, user (optional)" maxlength="200" />
142
- </label>
143
- </details>
144
- <button type="button" id="submit-btn" class="btn primary">Submit tip</button>
145
  </div>
146
- <div class="submit-message-wrap">
147
- <p id="submit-msg" class="message" aria-live="polite"></p>
148
- </div>
149
- </div>
150
- </section>
151
 
152
- <section id="search-panel" class="panel" role="tabpanel" hidden>
153
- <div class="search-bar">
154
- <label for="search-query" class="sr-only">Search</label>
155
- <input type="text" id="search-query" placeholder="Search tips by keyword…" maxlength="500" />
156
- <div class="search-filters">
157
- <label>
158
- <span>Language</span>
159
- <select id="search-language">
160
- <option value="">All</option>
161
- <option value="general">general</option>
162
- <option value="en">en</option>
163
- <option value="te">te</option>
164
- <option value="hi">hi</option>
165
- <option value="ta">ta</option>
166
- <option value="kn">kn</option>
167
- <option value="ml">ml</option>
168
- </select>
169
- </label>
170
- <label>
171
- <span>Type</span>
172
- <select id="search-type">
173
- <option value="">All</option>
174
- <option value="error">error</option>
175
- <option value="tip">tip</option>
176
- <option value="guide">guide</option>
177
- <option value="reference">reference</option>
178
- </select>
179
- </label>
180
- <label>
181
- <span>Max results</span>
182
- <input type="number" id="search-limit" min="5" max="50" value="10" />
183
- </label>
184
  </div>
185
- <button type="button" id="search-btn" class="btn primary">Search</button>
 
 
 
 
 
 
 
 
186
  </div>
187
- <div id="search-results" class="feed" aria-busy="false">
188
- <div class="search-empty">Enter a search query and click Search to find tips.</div>
 
 
 
189
  </div>
190
- </section>
191
  </div>
 
192
  <div id="auth-modal" class="auth-modal" hidden aria-modal="true" aria-hidden="true" role="dialog" aria-label="Sign in or create account">
193
  <div class="auth-modal-backdrop"></div>
194
  <div class="auth-modal-box">
@@ -200,7 +226,7 @@
200
  <label>Password <input type="password" id="auth-password-signin" required /></label>
201
  <button type="submit" class="btn primary">Sign in</button>
202
  </form>
203
- <p class="auth-modal-switch">Dont have an account? <button type="button" id="auth-show-register" class="auth-link">Create account</button></p>
204
  </div>
205
  <div id="auth-modal-register" class="auth-modal-panel" style="display: none;">
206
  <h3 class="auth-modal-title">Create account</h3>
 
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
  <title>Yantrabodha Tips</title>
7
  <link rel="preconnect" href="https://fonts.googleapis.com" />
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
 
11
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
12
  </head>
13
  <body>
14
+ <div class="app-layout">
15
+ <!-- Top bar: logo, search, auth -->
16
+ <header class="top-bar">
17
+ <button type="button" class="top-bar-menu-btn" id="sidebar-toggle" aria-label="Toggle menu">
18
+ <span class="menu-icon"></span>
19
+ </button>
20
+ <a href="/" class="top-bar-logo" aria-label="Home">
21
+ <span class="logo-text">Yantrabodha</span>
22
+ </a>
23
+ <div class="top-bar-search">
24
+ <label for="search-query" class="sr-only">Search tips</label>
25
+ <input type="text" id="search-query" class="search-input-with-icon" placeholder="Find anything" maxlength="500" />
26
+ </div>
27
+ <div id="auth-header" class="top-bar-auth">
28
+ <div id="auth-signin" class="auth-signin-row" style="display: none;">
29
+ <button type="button" id="auth-signin-btn" class="btn secondary btn-sm">Sign in with Hugging Face</button>
30
+ <span class="auth-sep">or</span>
31
+ <button type="button" id="auth-app-signin-btn" class="btn secondary btn-sm">Sign in</button>
32
+ <button type="button" id="auth-app-register-btn" class="btn primary btn-sm">Create account</button>
33
  </div>
34
+ <div id="auth-loggedin" class="auth-loggedin" style="display: none;">
35
+ <img id="auth-loggedin-img" class="auth-avatar" src="" alt="" />
36
+ <span id="auth-loggedin-name" class="auth-name"></span>
37
+ <button type="button" id="auth-signout-btn" class="btn secondary btn-sm">Sign out</button>
 
 
 
 
 
 
 
 
38
  </div>
39
  </div>
40
  </header>
41
 
42
+ <!-- Left sidebar: nav + filters -->
43
+ <aside class="left-sidebar" id="left-sidebar">
44
+ <nav class="sidebar-nav" role="navigation">
45
+ <a href="#" class="sidebar-nav-item active" data-view="browse" id="nav-browse">
46
+ <span class="sidebar-nav-icon">&#127968;</span>
47
+ <span>Browse</span>
48
+ </a>
49
+ <a href="#" class="sidebar-nav-item" data-view="submit" id="nav-submit">
50
+ <span class="sidebar-nav-icon">&#9998;</span>
51
+ <span>Submit Tip</span>
52
+ </a>
53
+ </nav>
54
+ <div class="sidebar-filters">
55
+ <h3 class="sidebar-filters-title">Filters</h3>
56
+ <label class="sidebar-filter-label">
57
  <span>Time</span>
58
  <select id="browse-time">
59
+ <option value="all">All times</option>
60
  <option value="today">Today</option>
61
  <option value="this_week">This week</option>
62
  </select>
63
  </label>
64
+ <label class="sidebar-filter-label">
65
  <span>Language</span>
66
  <select id="browse-language">
67
+ <option value="all">All languages</option>
68
  <option value="general">general</option>
69
  <option value="python">python</option>
70
  <option value="typescript">typescript</option>
 
81
  <option value="other">other</option>
82
  </select>
83
  </label>
84
+ <label class="sidebar-filter-label">
85
  <span>Type</span>
86
  <select id="browse-type">
87
+ <option value="all">All types</option>
88
  <option value="error">error</option>
89
  <option value="pattern">pattern</option>
90
  <option value="tip">tip</option>
 
92
  <option value="reference">reference</option>
93
  </select>
94
  </label>
95
+ <label class="sidebar-filter-label">
96
+ <span>Max results (search)</span>
97
+ <select id="search-limit">
98
+ <option value="5">5</option>
99
+ <option value="10" selected>10</option>
100
+ <option value="25">25</option>
101
+ <option value="50">50</option>
102
+ </select>
103
+ </label>
104
  </div>
105
+ <div id="sidebar-auth-mobile" class="sidebar-auth-mobile" aria-hidden="true">
106
+ <button type="button" id="auth-signout-sidebar-btn" class="btn secondary btn-sm">Sign out</button>
107
  </div>
108
+ </aside>
109
+ <div class="sidebar-overlay" id="sidebar-overlay" aria-hidden="true"></div>
110
 
111
+ <!-- Main content: feed, submit form, or search results -->
112
+ <main class="main-content">
113
+ <section id="browse-panel" class="panel active" role="tabpanel">
114
+ <div id="browse-feed" class="feed" aria-busy="false">
115
+ <div class="empty-state">Loading…</div>
116
+ </div>
117
+ <p id="browse-comment-msg" class="message" aria-live="polite"></p>
118
+ </section>
119
+
120
+ <section id="post-detail-panel" class="panel" role="tabpanel" hidden>
121
+ <div class="post-detail-header">
122
+ <button type="button" id="post-detail-back" class="btn secondary btn-sm">← Back to feed</button>
123
+ </div>
124
+ <div id="post-detail-content" class="post-detail-article"></div>
125
+ <div id="post-detail-comment-row" class="card-comment-row"></div>
126
+ <div id="post-detail-comments-list" class="post-detail-comments-list"></div>
127
+ <p id="post-detail-comment-msg" class="message" aria-live="polite"></p>
128
+ </section>
129
+
130
+ <section id="submit-panel" class="panel" role="tabpanel" hidden>
131
+ <div class="submit-layout">
132
+ <div class="submit-form">
133
+ <div class="group">
134
+ <label for="submit-title">Title</label>
135
+ <input type="text" id="submit-title" placeholder="Short, descriptive title for the tip" maxlength="500" />
136
+ </div>
137
+ <div class="group">
138
+ <label for="submit-body">Body</label>
139
+ <textarea id="submit-body" rows="6" placeholder="Write the tip content here. Include steps, examples, or explanations."></textarea>
140
+ </div>
141
+ <div class="group">
142
+ <label for="submit-tags">Tags</label>
143
+ <input type="text" id="submit-tags" placeholder="python, debug, fastapi (comma-separated)" maxlength="500" />
144
+ </div>
145
+ <details class="options-accordion">
146
+ <summary>Options</summary>
147
+ <div class="options-grid">
148
+ <label>
149
+ <span>Language</span>
150
+ <select id="submit-language">
151
+ <option value="general">general</option>
152
+ <option value="en">en</option>
153
+ <option value="te">te</option>
154
+ <option value="hi">hi</option>
155
+ <option value="ta">ta</option>
156
+ <option value="kn">kn</option>
157
+ <option value="ml">ml</option>
158
+ </select>
159
+ </label>
160
+ <label>
161
+ <span>Type</span>
162
+ <select id="submit-type">
163
+ <option value="error">error</option>
164
+ <option value="tip">tip</option>
165
+ <option value="guide">guide</option>
166
+ <option value="reference">reference</option>
167
+ </select>
168
+ </label>
169
+ <label>
170
+ <span>Confidence</span>
171
+ <select id="submit-confidence">
172
+ <option value="low">low</option>
173
+ <option value="medium" selected>medium</option>
174
+ <option value="high">high</option>
175
+ </select>
176
+ </label>
177
+ </div>
178
+ <label class="full-width">
179
+ <span>Contributing agent</span>
180
+ <input type="text" id="submit-agent" placeholder="e.g. claude, user (optional)" maxlength="200" />
181
+ </label>
182
+ </details>
183
+ <button type="button" id="submit-btn" class="btn primary">Submit tip</button>
184
  </div>
185
+ <div class="submit-message-wrap">
186
+ <p id="submit-msg" class="message" aria-live="polite"></p>
 
187
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  </div>
189
+ </section>
 
 
 
 
190
 
191
+ <section id="search-panel" class="panel" role="tabpanel" hidden>
192
+ <div class="search-results-header">
193
+ <button type="button" id="search-back-to-feed" class="btn secondary btn-sm">Back to feed</button>
194
+ <h2 class="search-results-title">Search results</h2>
195
+ </div>
196
+ <div id="search-results" class="feed" aria-busy="false">
197
+ <div class="search-empty">Use the search bar above to find tips.</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  </div>
199
+ </section>
200
+ </main>
201
+
202
+ <!-- Right sidebar: about, recent tips -->
203
+ <aside class="right-sidebar" id="right-sidebar">
204
+ <div class="sidebar-widget about-widget">
205
+ <h3 class="widget-title">About</h3>
206
+ <p class="about-text">Yantrabodha is an open-source knowledge base for tips, errors, and patterns. Browse, submit, and search.</p>
207
+ <p id="about-tip-count" class="about-tip-count"></p>
208
  </div>
209
+ <div class="sidebar-widget recent-tips-widget">
210
+ <h3 class="widget-title">Recent Tips</h3>
211
+ <div id="recent-tips-list" class="recent-tips-list">
212
+ <div class="recent-tips-loading">Loading…</div>
213
+ </div>
214
  </div>
215
+ </aside>
216
  </div>
217
+
218
  <div id="auth-modal" class="auth-modal" hidden aria-modal="true" aria-hidden="true" role="dialog" aria-label="Sign in or create account">
219
  <div class="auth-modal-backdrop"></div>
220
  <div class="auth-modal-box">
 
226
  <label>Password <input type="password" id="auth-password-signin" required /></label>
227
  <button type="submit" class="btn primary">Sign in</button>
228
  </form>
229
+ <p class="auth-modal-switch">Don't have an account? <button type="button" id="auth-show-register" class="auth-link">Create account</button></p>
230
  </div>
231
  <div id="auth-modal-register" class="auth-modal-panel" style="display: none;">
232
  <h3 class="auth-modal-title">Create account</h3>
api/static/styles.css CHANGED
@@ -1,4 +1,4 @@
1
- /* Yantrabodha — polished static UI */
2
 
3
  :root {
4
  --font-sans: "DM Sans", system-ui, -apple-system, sans-serif;
@@ -26,6 +26,9 @@
26
  --radius-sm: 8px;
27
  --radius-input: 8px;
28
  --radius-full: 9999px;
 
 
 
29
  }
30
 
31
  *,
@@ -34,70 +37,364 @@
34
  box-sizing: border-box;
35
  }
36
 
 
 
 
 
37
  body {
38
  font-family: var(--font-sans);
39
  color: var(--text);
40
- background: linear-gradient(180deg, #f8fafc 0%, var(--bg) 12rem);
41
  margin: 0;
42
  padding: 0;
43
  line-height: 1.6;
44
  -webkit-font-smoothing: antialiased;
45
  min-height: 100vh;
 
 
 
 
46
  }
47
 
48
- .container {
49
- max-width: 720px;
50
- margin: 0 auto;
51
- padding: 2rem 1.5rem;
52
- }
53
-
54
- @media (max-width: 480px) {
55
- .container {
56
- padding: 1.25rem 1rem;
57
- }
58
-
59
- .header h1 {
60
- font-size: 1.9rem;
61
- }
62
  }
63
 
64
- /* Header */
65
- .header {
66
- margin-bottom: 1.75rem;
67
- padding-bottom: 1.5rem;
 
 
 
 
68
  border-bottom: 1px solid var(--border);
69
- box-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
 
 
 
70
  }
71
 
72
- .header h1 {
73
- margin: 0 0 0.4rem 0;
74
  font-family: var(--font-display);
75
- font-size: 2.35rem;
76
  font-weight: 700;
77
- letter-spacing: -0.03em;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  color: var(--text);
79
- line-height: 1.2;
 
 
80
  }
81
 
82
- .subtitle {
83
- margin: 0;
84
- color: var(--text-muted);
85
- font-size: 1rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  font-weight: 500;
 
 
 
 
 
 
 
 
 
87
  }
88
 
89
- .header-top {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  display: flex;
91
- flex-wrap: wrap;
92
- justify-content: space-between;
93
- align-items: flex-start;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  gap: 1rem;
 
95
  }
96
 
97
- .auth-header {
98
- flex-shrink: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  }
100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  .auth-loggedin {
102
  display: flex;
103
  align-items: center;
@@ -250,153 +547,29 @@ body.auth-modal-open {
250
  color: var(--primary-hover);
251
  }
252
 
253
- .auth-link:focus-visible {
254
- outline: 2px solid var(--primary);
255
- outline-offset: 2px;
256
- border-radius: 2px;
257
- }
258
-
259
- .auth-modal-error {
260
- margin: 0.85rem 0 0 0;
261
- padding: 0.5rem 0;
262
- font-size: 0.875rem;
263
- color: #b91c1c;
264
- line-height: 1.4;
265
- }
266
-
267
- /* Tabs — pill style */
268
- .tabs {
269
- display: flex;
270
- gap: 0.5rem;
271
- margin-bottom: 1.5rem;
272
- padding: 0.35rem;
273
- background: var(--bg-card);
274
- border-radius: var(--radius);
275
- border: 1px solid var(--border);
276
- box-shadow: var(--shadow);
277
- }
278
-
279
- .tab {
280
- flex: 1;
281
- padding: 0.7rem 1rem;
282
- font-size: 0.9rem;
283
- font-weight: 500;
284
- color: var(--text-muted);
285
- background: transparent;
286
- border: none;
287
- border-radius: var(--radius-sm);
288
- cursor: pointer;
289
- transition: color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
290
- }
291
-
292
- .tab:hover {
293
- color: var(--text);
294
- background: var(--bg);
295
- }
296
-
297
- .tab:focus-visible {
298
- outline: none;
299
- box-shadow: var(--shadow-focus);
300
- }
301
-
302
- .tab.active {
303
- color: var(--primary);
304
- background: var(--primary-light);
305
- box-shadow: 0 1px 3px rgba(79, 70, 229, 0.15);
306
- }
307
-
308
- .panel {
309
- display: none;
310
- }
311
-
312
- .panel.active {
313
- display: block;
314
- }
315
-
316
- /* Filters */
317
- .filters {
318
- display: flex;
319
- flex-wrap: wrap;
320
- gap: 1rem;
321
- margin-bottom: 1.25rem;
322
- padding: 1rem 1.1rem;
323
- background: rgba(255, 255, 255, 0.7);
324
- border-radius: var(--radius);
325
- border: 1px solid var(--border);
326
- }
327
-
328
- .filters label,
329
- .search-filters label {
330
- display: flex;
331
- flex-direction: column;
332
- gap: 0.4rem;
333
- font-size: 0.75rem;
334
- font-weight: 700;
335
- color: var(--text-muted);
336
- text-transform: uppercase;
337
- letter-spacing: 0.04em;
338
- }
339
-
340
- .filters select,
341
- .search-filters select,
342
- .search-filters input[type="number"] {
343
- padding: 0.55rem 0.85rem;
344
- font-size: 0.9rem;
345
- font-family: inherit;
346
- border: 1px solid var(--border);
347
- border-radius: var(--radius-input);
348
- min-width: 130px;
349
- background: var(--bg-card);
350
- color: var(--text);
351
- cursor: pointer;
352
- transition: border-color 0.15s, box-shadow 0.15s;
353
- }
354
-
355
- .filters select:focus,
356
- .search-filters select:focus,
357
- .search-filters input[type="number"]:focus,
358
- input:focus,
359
- textarea:focus,
360
- select:focus {
361
- outline: none;
362
- border-color: var(--primary);
363
- box-shadow: var(--shadow-focus);
364
- }
365
-
366
- .comment-author-row {
367
- margin: 0 0 1.25rem 0;
368
- }
369
-
370
- .comment-author-row label {
371
- display: block;
372
- font-size: 0.75rem;
373
- font-weight: 700;
374
- margin-bottom: 0.4rem;
375
- color: var(--text-muted);
376
- text-transform: uppercase;
377
- letter-spacing: 0.04em;
378
  }
379
 
380
- #browse-comment-author {
381
- max-width: 260px;
382
- padding: 0.55rem 0.85rem;
383
- font-size: 0.9rem;
384
- font-family: inherit;
385
- border: 1px solid var(--border);
386
- border-radius: var(--radius-input);
387
- background: var(--bg-card);
388
  }
389
 
390
- /* Feed */
391
  .feed {
392
  min-height: 140px;
393
  }
394
 
395
- /* Empty state */
396
  .empty-state {
397
  text-align: center;
398
  padding: 4rem 2.5rem;
399
- background: rgba(255, 255, 255, 0.8);
400
  border-radius: var(--radius);
401
  border: 2px dashed var(--border);
402
  box-shadow: var(--shadow);
@@ -432,27 +605,110 @@ select:focus {
432
  box-shadow: var(--shadow);
433
  }
434
 
435
- /* Article cards — Reddit-like */
436
  .article-card-wrapper {
437
  border: 1px solid var(--border);
438
- border-left: 3px solid var(--primary);
439
  border-radius: var(--radius);
440
  background: var(--bg-card);
441
  margin-bottom: 1.25rem;
442
- box-shadow: var(--shadow-lg);
443
  overflow: hidden;
444
  transition: box-shadow 0.2s ease, border-color 0.2s ease;
445
  }
446
 
447
  .article-card-wrapper:hover {
448
- box-shadow: 0 12px 48px rgba(0, 0, 0, 0.1), 0 4px 12px rgba(0, 0, 0, 0.05);
449
- border-left-color: var(--accent);
 
 
 
 
 
 
 
450
  }
451
 
452
  .card-html {
453
  padding: 0;
454
  }
455
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456
  .article-card-inner {
457
  padding: 1.5rem 1.65rem;
458
  background: var(--bg-card);
@@ -674,10 +930,86 @@ select:focus {
674
  margin-top: 0.5rem;
675
  }
676
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
677
  .card-comment-row {
678
- background: #fafbfc;
679
- border-top: 1px solid var(--border);
680
- padding: 0.75rem 1.5rem 1.25rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
681
  }
682
 
683
  .comment-input {
@@ -697,7 +1029,7 @@ select:focus {
697
  margin-top: 0.25rem;
698
  }
699
 
700
- /* Buttons */
701
  .btn {
702
  padding: 0.6rem 1.25rem;
703
  font-size: 0.9rem;
@@ -718,6 +1050,35 @@ select:focus {
718
  transform: scale(0.98);
719
  }
720
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
721
  .btn.primary {
722
  background: var(--primary);
723
  color: white;
@@ -739,7 +1100,7 @@ select:focus {
739
  background: var(--border);
740
  }
741
 
742
- /* Messages */
743
  .message {
744
  margin: 0.85rem 0 0 0;
745
  font-size: 0.9rem;
@@ -747,17 +1108,12 @@ select:focus {
747
  min-height: 1.5em;
748
  }
749
 
750
- /* Submit panel */
751
  .submit-layout {
752
  display: grid;
753
  grid-template-columns: 2fr 1fr;
754
  gap: 2rem;
755
- }
756
-
757
- @media (max-width: 640px) {
758
- .submit-layout {
759
- grid-template-columns: 1fr;
760
- }
761
  }
762
 
763
  .submit-form .group {
@@ -860,44 +1216,6 @@ select:focus {
860
  min-height: 2.5rem;
861
  }
862
 
863
- /* Search panel */
864
- .search-bar {
865
- margin-bottom: 1.5rem;
866
- }
867
-
868
- .search-bar > input[type="text"] {
869
- width: 100%;
870
- max-width: 480px;
871
- padding: 0.75rem 1rem;
872
- font-size: 1rem;
873
- font-family: inherit;
874
- border: 1px solid var(--border);
875
- border-radius: var(--radius-input);
876
- margin-bottom: 1rem;
877
- background: var(--bg-card);
878
- transition: border-color 0.15s, box-shadow 0.15s;
879
- }
880
-
881
- .search-filters {
882
- display: flex;
883
- flex-wrap: wrap;
884
- gap: 1rem;
885
- align-items: flex-end;
886
- margin-bottom: 1rem;
887
- padding: 1rem 1.1rem;
888
- background: rgba(255, 255, 255, 0.7);
889
- border-radius: var(--radius);
890
- border: 1px solid var(--border);
891
- }
892
-
893
- #search-btn {
894
- margin-top: 0.35rem;
895
- padding: 0.7rem 1.5rem;
896
- font-size: 0.95rem;
897
- font-weight: 600;
898
- box-shadow: 0 2px 8px rgba(79, 70, 229, 0.25);
899
- }
900
-
901
  .sr-only {
902
  position: absolute;
903
  width: 1px;
@@ -909,3 +1227,197 @@ select:focus {
909
  white-space: nowrap;
910
  border: 0;
911
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Yantrabodha — Reddit-style layout */
2
 
3
  :root {
4
  --font-sans: "DM Sans", system-ui, -apple-system, sans-serif;
 
26
  --radius-sm: 8px;
27
  --radius-input: 8px;
28
  --radius-full: 9999px;
29
+ --sidebar-left-width: 240px;
30
+ --sidebar-right-width: 300px;
31
+ --top-bar-height: 56px;
32
  }
33
 
34
  *,
 
37
  box-sizing: border-box;
38
  }
39
 
40
+ html {
41
+ -webkit-text-size-adjust: 100%;
42
+ }
43
+
44
  body {
45
  font-family: var(--font-sans);
46
  color: var(--text);
47
+ background: var(--bg);
48
  margin: 0;
49
  padding: 0;
50
  line-height: 1.6;
51
  -webkit-font-smoothing: antialiased;
52
  min-height: 100vh;
53
+ overflow-x: hidden;
54
+ /* Safe area for notched devices */
55
+ padding-left: env(safe-area-inset-left);
56
+ padding-right: env(safe-area-inset-right);
57
  }
58
 
59
+ /* ——— App layout: top bar + three columns ——— */
60
+ .app-layout {
61
+ display: grid;
62
+ grid-template-columns: var(--sidebar-left-width) 1fr var(--sidebar-right-width);
63
+ grid-template-rows: var(--top-bar-height) minmax(0, 1fr);
64
+ grid-template-areas:
65
+ "topbar topbar topbar"
66
+ "left main right";
67
+ height: 100vh;
68
+ max-height: 100vh;
69
+ overflow: hidden;
 
 
 
70
  }
71
 
72
+ .top-bar {
73
+ grid-area: topbar;
74
+ display: flex;
75
+ align-items: center;
76
+ gap: 1rem;
77
+ padding: 0 1rem;
78
+ padding-top: env(safe-area-inset-top);
79
+ background: var(--bg-card);
80
  border-bottom: 1px solid var(--border);
81
+ position: sticky;
82
+ top: 0;
83
+ z-index: 100;
84
+ box-shadow: var(--shadow);
85
  }
86
 
87
+ .top-bar-logo {
 
88
  font-family: var(--font-display);
89
+ font-size: 1.5rem;
90
  font-weight: 700;
91
+ color: var(--primary);
92
+ text-decoration: none;
93
+ letter-spacing: -0.02em;
94
+ flex-shrink: 0;
95
+ position: relative;
96
+ z-index: 1;
97
+ }
98
+
99
+ .top-bar-logo:hover {
100
+ color: var(--primary-hover);
101
+ }
102
+
103
+ .top-bar-menu-btn {
104
+ display: none;
105
+ align-items: center;
106
+ justify-content: center;
107
+ min-width: 44px;
108
+ min-height: 44px;
109
+ width: 44px;
110
+ height: 44px;
111
+ padding: 0;
112
+ background: none;
113
+ border: none;
114
+ border-radius: var(--radius-sm);
115
+ cursor: pointer;
116
  color: var(--text);
117
+ -webkit-tap-highlight-color: transparent;
118
+ position: relative;
119
+ z-index: 1;
120
  }
121
 
122
+ .top-bar-menu-btn:hover {
123
+ background: var(--bg);
124
+ }
125
+
126
+ .menu-icon {
127
+ display: block;
128
+ width: 20px;
129
+ height: 2px;
130
+ background: currentColor;
131
+ box-shadow: 0 -6px 0 currentColor, 0 6px 0 currentColor;
132
+ }
133
+
134
+ .top-bar-search {
135
+ position: absolute;
136
+ left: 50%;
137
+ transform: translateX(-50%);
138
+ width: 100%;
139
+ max-width: 560px;
140
+ display: flex;
141
+ align-items: center;
142
+ box-sizing: border-box;
143
+ padding: 0 0.5rem;
144
+ z-index: 0;
145
+ }
146
+
147
+ .top-bar-search input,
148
+ .top-bar-search .search-input-with-icon {
149
+ width: 100%;
150
+ padding: 0.5rem 1rem 0.5rem 2.5rem;
151
+ font-size: 0.9375rem;
152
+ font-family: inherit;
153
+ border: 1px solid var(--border);
154
+ border-radius: var(--radius-full);
155
+ background: var(--bg);
156
+ transition: border-color 0.15s, box-shadow 0.15s;
157
+ }
158
+
159
+ .top-bar-search input:focus,
160
+ .top-bar-search .search-input-with-icon:focus {
161
+ outline: none;
162
+ border-color: var(--primary);
163
+ box-shadow: var(--shadow-focus);
164
+ }
165
+
166
+ .top-bar-search input.search-input-with-icon {
167
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.35-4.35'/%3E%3C/svg%3E");
168
+ background-repeat: no-repeat;
169
+ background-position: 0.75rem center;
170
+ background-size: 1.125rem;
171
+ padding-left: 2.5rem;
172
+ }
173
+
174
+ .top-bar-auth {
175
+ flex-shrink: 0;
176
+ margin-left: auto;
177
+ position: relative;
178
+ z-index: 1;
179
+ }
180
+
181
+ /* ——— Left sidebar ——— */
182
+ .left-sidebar {
183
+ grid-area: left;
184
+ background: var(--bg-card);
185
+ border-right: 1px solid var(--border);
186
+ overflow-y: auto;
187
+ overflow-x: hidden;
188
+ padding: 1rem 0;
189
+ min-height: 0; /* allow grid item to shrink and scroll */
190
+ display: flex;
191
+ flex-direction: column;
192
+ }
193
+
194
+ .sidebar-auth-mobile {
195
+ display: none;
196
+ margin-top: auto;
197
+ padding: 1rem 1rem 0;
198
+ border-top: 1px solid var(--border);
199
+ }
200
+
201
+ .sidebar-nav {
202
+ display: flex;
203
+ flex-direction: column;
204
+ gap: 0.25rem;
205
+ margin-bottom: 1.5rem;
206
+ }
207
+
208
+ .sidebar-nav-item {
209
+ display: flex;
210
+ align-items: center;
211
+ gap: 0.5rem;
212
+ padding: 0.5rem 1rem;
213
+ font-size: 0.9375rem;
214
  font-weight: 500;
215
+ color: var(--text-muted);
216
+ text-decoration: none;
217
+ border-radius: var(--radius-sm);
218
+ transition: background 0.15s, color 0.15s;
219
+ }
220
+
221
+ .sidebar-nav-item:hover {
222
+ background: var(--bg);
223
+ color: var(--text);
224
  }
225
 
226
+ .sidebar-nav-item.active {
227
+ background: var(--primary-light);
228
+ color: var(--primary);
229
+ }
230
+
231
+ .sidebar-nav-icon {
232
+ font-size: 1.1rem;
233
+ line-height: 1;
234
+ }
235
+
236
+ .sidebar-filters {
237
+ padding: 0 1rem;
238
+ border-top: 1px solid var(--border);
239
+ padding-top: 1rem;
240
+ }
241
+
242
+ .sidebar-filters-title {
243
+ margin: 0 0 0.75rem 0;
244
+ font-size: 0.7rem;
245
+ font-weight: 700;
246
+ color: var(--text-light);
247
+ text-transform: uppercase;
248
+ letter-spacing: 0.05em;
249
+ }
250
+
251
+ .sidebar-filter-label {
252
  display: flex;
253
+ flex-direction: column;
254
+ gap: 0.35rem;
255
+ margin-bottom: 0.75rem;
256
+ font-size: 0.75rem;
257
+ font-weight: 600;
258
+ color: var(--text-muted);
259
+ text-transform: uppercase;
260
+ letter-spacing: 0.04em;
261
+ }
262
+
263
+ .sidebar-filter-label:last-child {
264
+ margin-bottom: 0;
265
+ }
266
+
267
+ .sidebar-filter-label select {
268
+ padding: 0.5rem 0.75rem;
269
+ font-size: 0.875rem;
270
+ font-family: inherit;
271
+ border: 1px solid var(--border);
272
+ border-radius: var(--radius-input);
273
+ background: var(--bg-card);
274
+ color: var(--text);
275
+ cursor: pointer;
276
+ width: 100%;
277
+ }
278
+
279
+ .sidebar-filter-label select:focus {
280
+ outline: none;
281
+ border-color: var(--primary);
282
+ }
283
+
284
+ /* ——— Main content ——— */
285
+ .main-content {
286
+ grid-area: main;
287
+ min-width: 0;
288
+ min-height: 0; /* allow grid item to shrink and scroll */
289
+ overflow-y: auto;
290
+ overflow-x: hidden;
291
+ padding: 1.5rem;
292
+ background: var(--bg);
293
+ }
294
+
295
+ .panel {
296
+ display: none;
297
+ }
298
+
299
+ .panel.active {
300
+ display: block;
301
+ }
302
+
303
+ .search-results-header {
304
+ display: flex;
305
+ align-items: center;
306
  gap: 1rem;
307
+ margin-bottom: 1rem;
308
  }
309
 
310
+ .search-results-title {
311
+ margin: 0;
312
+ font-size: 1.25rem;
313
+ font-weight: 600;
314
+ color: var(--text);
315
+ }
316
+
317
+ /* ——— Right sidebar ——— */
318
+ .right-sidebar {
319
+ grid-area: right;
320
+ background: var(--bg-card);
321
+ border-left: 1px solid var(--border);
322
+ overflow-y: auto;
323
+ overflow-x: hidden;
324
+ padding: 1rem;
325
+ min-height: 0; /* allow grid item to shrink and scroll */
326
+ }
327
+
328
+ .sidebar-widget {
329
+ background: var(--bg);
330
+ border-radius: var(--radius);
331
+ border: 1px solid var(--border);
332
+ padding: 1rem;
333
+ margin-bottom: 1rem;
334
+ }
335
+
336
+ .sidebar-widget:last-child {
337
+ margin-bottom: 0;
338
+ }
339
+
340
+ .widget-title {
341
+ margin: 0 0 0.75rem 0;
342
+ font-size: 0.7rem;
343
+ font-weight: 700;
344
+ color: var(--text-light);
345
+ text-transform: uppercase;
346
+ letter-spacing: 0.05em;
347
+ }
348
+
349
+ .recent-tips-list {
350
+ font-size: 0.875rem;
351
+ }
352
+
353
+ .recent-tips-loading,
354
+ .recent-tips-empty {
355
+ color: var(--text-light);
356
+ font-size: 0.875rem;
357
+ }
358
+
359
+ .recent-tip-item {
360
+ display: block;
361
+ padding: 0.5rem 0;
362
+ border-bottom: 1px solid var(--border);
363
+ color: var(--text);
364
+ text-decoration: none;
365
+ line-height: 1.35;
366
+ transition: color 0.15s;
367
  }
368
 
369
+ .recent-tip-item:last-child {
370
+ border-bottom: none;
371
+ }
372
+
373
+ .recent-tip-item:hover {
374
+ color: var(--primary);
375
+ }
376
+
377
+ .recent-tip-item .recent-tip-time {
378
+ font-size: 0.75rem;
379
+ color: var(--text-light);
380
+ margin-left: 0.25rem;
381
+ }
382
+
383
+ .about-text {
384
+ margin: 0 0 0.5rem 0;
385
+ font-size: 0.875rem;
386
+ color: var(--text-muted);
387
+ line-height: 1.5;
388
+ }
389
+
390
+ .about-tip-count {
391
+ margin: 0;
392
+ font-size: 0.8125rem;
393
+ color: var(--text-light);
394
+ font-weight: 500;
395
+ }
396
+
397
+ /* ——— Auth (top bar) ——— */
398
  .auth-loggedin {
399
  display: flex;
400
  align-items: center;
 
547
  color: var(--primary-hover);
548
  }
549
 
550
+ .auth-link:focus-visible {
551
+ outline: 2px solid var(--primary);
552
+ outline-offset: 2px;
553
+ border-radius: 2px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
554
  }
555
 
556
+ .auth-modal-error {
557
+ margin: 0.85rem 0 0 0;
558
+ padding: 0.5rem 0;
559
+ font-size: 0.875rem;
560
+ color: #b91c1c;
561
+ line-height: 1.4;
 
 
562
  }
563
 
564
+ /* ——— Feed & panels ——— */
565
  .feed {
566
  min-height: 140px;
567
  }
568
 
 
569
  .empty-state {
570
  text-align: center;
571
  padding: 4rem 2.5rem;
572
+ background: var(--bg-card);
573
  border-radius: var(--radius);
574
  border: 2px dashed var(--border);
575
  box-shadow: var(--shadow);
 
605
  box-shadow: var(--shadow);
606
  }
607
 
608
+ /* ——— Article cards ——— */
609
  .article-card-wrapper {
610
  border: 1px solid var(--border);
 
611
  border-radius: var(--radius);
612
  background: var(--bg-card);
613
  margin-bottom: 1.25rem;
614
+ box-shadow: var(--shadow);
615
  overflow: hidden;
616
  transition: box-shadow 0.2s ease, border-color 0.2s ease;
617
  }
618
 
619
  .article-card-wrapper:hover {
620
+ box-shadow: var(--shadow-md);
621
+ }
622
+
623
+ .article-card-wrapper.card-compact {
624
+ cursor: pointer;
625
+ }
626
+
627
+ .article-card-wrapper.card-compact:hover {
628
+ border-color: var(--border-strong);
629
  }
630
 
631
  .card-html {
632
  padding: 0;
633
  }
634
 
635
+ /* ——— Compact card (feed list) ——— */
636
+ .card-compact-inner {
637
+ padding: 1rem 1.25rem;
638
+ }
639
+
640
+ .card-compact-top {
641
+ display: flex;
642
+ align-items: center;
643
+ justify-content: space-between;
644
+ gap: 1rem;
645
+ margin-bottom: 0.5rem;
646
+ }
647
+
648
+ .card-compact-top .card-author-row {
649
+ margin-bottom: 0;
650
+ }
651
+
652
+ .card-compact-meta {
653
+ display: flex;
654
+ align-items: center;
655
+ gap: 0.5rem;
656
+ flex-shrink: 0;
657
+ }
658
+
659
+ .card-compact-title {
660
+ font-weight: 600;
661
+ font-size: 1.1rem;
662
+ color: var(--text);
663
+ margin: 0 0 0.5rem 0;
664
+ line-height: 1.35;
665
+ }
666
+
667
+ .card-compact-preview {
668
+ font-size: 0.875rem;
669
+ color: var(--text-muted);
670
+ line-height: 1.45;
671
+ margin: 0 0 0.5rem 0;
672
+ }
673
+
674
+ .card-compact-preview.article-body h1,
675
+ .card-compact-preview.article-body h2,
676
+ .card-compact-preview.article-body h3 {
677
+ margin-top: 0.85rem;
678
+ margin-bottom: 0.4rem;
679
+ font-weight: 600;
680
+ color: var(--text);
681
+ }
682
+
683
+ .card-compact-preview.article-body p {
684
+ margin-bottom: 0.5rem;
685
+ }
686
+
687
+ .card-engagement {
688
+ font-size: 0.8rem;
689
+ color: var(--text-light);
690
+ display: flex;
691
+ align-items: center;
692
+ gap: 0.35rem;
693
+ }
694
+
695
+ .card-engagement span:nth-child(odd) {
696
+ color: var(--text-muted);
697
+ }
698
+
699
+ /* ——— Post detail panel ——— */
700
+ .post-detail-header {
701
+ margin-bottom: 1rem;
702
+ }
703
+
704
+ .post-detail-article {
705
+ margin-bottom: 1rem;
706
+ }
707
+
708
+ .post-detail-article .article-card-wrapper {
709
+ margin-bottom: 0;
710
+ }
711
+
712
  .article-card-inner {
713
  padding: 1.5rem 1.65rem;
714
  background: var(--bg-card);
 
930
  margin-top: 0.5rem;
931
  }
932
 
933
+ /* Comments list below comment box (post detail) — no dividers between comments */
934
+ .post-detail-comments-list {
935
+ margin-top: 1.5rem;
936
+ }
937
+
938
+ .post-detail-comments-list .comments-section {
939
+ margin: 0;
940
+ }
941
+
942
+ .post-detail-comments-list .comments-header {
943
+ margin-bottom: 0.75rem;
944
+ }
945
+
946
+ .post-detail-comments-list .comments-thread {
947
+ margin: 0;
948
+ }
949
+
950
+ .post-detail-comments-list .comment-line {
951
+ margin-bottom: 1rem;
952
+ padding: 0.6rem 0 0.6rem 0.75rem;
953
+ border-left: 2px solid var(--border);
954
+ margin-left: 0.25rem;
955
+ border-bottom: none;
956
+ }
957
+
958
+ .post-detail-comments-list .comment-line:last-child {
959
+ margin-bottom: 0;
960
+ }
961
+
962
  .card-comment-row {
963
+ padding: 1rem 0 0.5rem;
964
+ }
965
+
966
+ .comment-box-container {
967
+ border: 1px solid var(--border);
968
+ border-radius: var(--radius);
969
+ background: var(--bg-card);
970
+ overflow: hidden;
971
+ box-shadow: var(--shadow);
972
+ }
973
+
974
+ .comment-box-container .comment-input,
975
+ .comment-box-textarea {
976
+ width: 100%;
977
+ padding: 1.25rem 0.75rem 0.5rem;
978
+ font-size: 0.875rem;
979
+ font-family: inherit;
980
+ border: none;
981
+ border-radius: 0;
982
+ resize: vertical;
983
+ min-height: 56px;
984
+ background: var(--bg-card);
985
+ display: block;
986
+ box-sizing: border-box;
987
+ }
988
+
989
+ .comment-box-container .comment-input:focus,
990
+ .comment-box-textarea:focus {
991
+ outline: none;
992
+ }
993
+
994
+ .comment-box-actions {
995
+ display: flex;
996
+ justify-content: flex-end;
997
+ align-items: center;
998
+ gap: 0.5rem;
999
+ padding: 0.4rem 0.75rem;
1000
+ background: var(--bg-card);
1001
+ }
1002
+
1003
+ .comment-box-submit {
1004
+ margin: 0;
1005
+ }
1006
+
1007
+ .comment-box-signin {
1008
+ padding: 1rem 1.25rem;
1009
+ }
1010
+
1011
+ .comment-box-signin .signin-to-comment {
1012
+ margin: 0 0 0.75rem 0;
1013
  }
1014
 
1015
  .comment-input {
 
1029
  margin-top: 0.25rem;
1030
  }
1031
 
1032
+ /* ——— Buttons ——— */
1033
  .btn {
1034
  padding: 0.6rem 1.25rem;
1035
  font-size: 0.9rem;
 
1050
  transform: scale(0.98);
1051
  }
1052
 
1053
+ /* Minimum touch target for mobile */
1054
+ .btn,
1055
+ .sidebar-nav-item,
1056
+ a.top-bar-logo,
1057
+ .recent-tip-item,
1058
+ .tag-pill {
1059
+ -webkit-tap-highlight-color: transparent;
1060
+ }
1061
+
1062
+ @media (pointer: coarse) {
1063
+ .btn,
1064
+ .btn-sm {
1065
+ min-height: 44px;
1066
+ min-width: 44px;
1067
+ padding: 0.5rem 1rem;
1068
+ }
1069
+ .btn-sm {
1070
+ min-width: 44px;
1071
+ padding: 0.5rem 0.85rem;
1072
+ }
1073
+ .sidebar-nav-item {
1074
+ min-height: 44px;
1075
+ padding: 0.65rem 1rem;
1076
+ }
1077
+ .recent-tip-item {
1078
+ padding: 0.65rem 0;
1079
+ }
1080
+ }
1081
+
1082
  .btn.primary {
1083
  background: var(--primary);
1084
  color: white;
 
1100
  background: var(--border);
1101
  }
1102
 
1103
+ /* ——— Messages ——— */
1104
  .message {
1105
  margin: 0.85rem 0 0 0;
1106
  font-size: 0.9rem;
 
1108
  min-height: 1.5em;
1109
  }
1110
 
1111
+ /* ——— Submit panel ——— */
1112
  .submit-layout {
1113
  display: grid;
1114
  grid-template-columns: 2fr 1fr;
1115
  gap: 2rem;
1116
+ max-width: 720px;
 
 
 
 
 
1117
  }
1118
 
1119
  .submit-form .group {
 
1216
  min-height: 2.5rem;
1217
  }
1218
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1219
  .sr-only {
1220
  position: absolute;
1221
  width: 1px;
 
1227
  white-space: nowrap;
1228
  border: 0;
1229
  }
1230
+
1231
+ /* ——— Responsive: collapse sidebars ——— */
1232
+ @media (max-width: 1024px) {
1233
+ .app-layout {
1234
+ grid-template-columns: 1fr var(--sidebar-right-width);
1235
+ grid-template-areas:
1236
+ "topbar topbar"
1237
+ "main right";
1238
+ }
1239
+
1240
+ .left-sidebar {
1241
+ position: fixed;
1242
+ left: 0;
1243
+ top: calc(var(--top-bar-height) + env(safe-area-inset-top, 0px));
1244
+ bottom: 0;
1245
+ width: var(--sidebar-left-width);
1246
+ padding-left: env(safe-area-inset-left, 0px);
1247
+ z-index: 90;
1248
+ transform: translateX(-100%);
1249
+ transition: transform 0.2s ease;
1250
+ box-shadow: var(--shadow-lg);
1251
+ }
1252
+
1253
+ .left-sidebar.is-open {
1254
+ transform: translateX(0);
1255
+ }
1256
+
1257
+ .sidebar-overlay {
1258
+ display: none;
1259
+ position: fixed;
1260
+ left: 0;
1261
+ right: 0;
1262
+ bottom: 0;
1263
+ top: calc(var(--top-bar-height) + env(safe-area-inset-top, 0px));
1264
+ background: rgba(0, 0, 0, 0.35);
1265
+ z-index: 85;
1266
+ -webkit-tap-highlight-color: transparent;
1267
+ }
1268
+
1269
+ .left-sidebar.is-open ~ .sidebar-overlay {
1270
+ display: block;
1271
+ }
1272
+
1273
+ .top-bar-menu-btn {
1274
+ display: flex;
1275
+ }
1276
+ }
1277
+
1278
+ @media (max-width: 768px) {
1279
+ .app-layout {
1280
+ grid-template-columns: 1fr;
1281
+ grid-template-areas:
1282
+ "topbar"
1283
+ "main";
1284
+ }
1285
+
1286
+ /* Hide "Yantrabodha" logo text in top bar on mobile */
1287
+ .top-bar-logo .logo-text {
1288
+ display: none;
1289
+ }
1290
+
1291
+ /* Hide top-bar logged-in auth on mobile (Sign out moves to left sidebar) */
1292
+ .top-bar-auth #auth-loggedin {
1293
+ display: none !important;
1294
+ }
1295
+
1296
+ .sidebar-auth-mobile.is-logged-in {
1297
+ display: block;
1298
+ }
1299
+
1300
+ .right-sidebar {
1301
+ display: none;
1302
+ }
1303
+
1304
+ .main-content {
1305
+ padding: 1rem;
1306
+ padding-bottom: calc(1rem + env(safe-area-inset-bottom));
1307
+ }
1308
+
1309
+ .submit-layout {
1310
+ grid-template-columns: 1fr;
1311
+ }
1312
+
1313
+ .top-bar-search {
1314
+ max-width: none;
1315
+ min-width: 0;
1316
+ }
1317
+
1318
+ .auth-signin-row {
1319
+ flex-wrap: wrap;
1320
+ }
1321
+
1322
+ /* Compact cards: stack meta and tighten padding */
1323
+ .card-compact-inner {
1324
+ padding: 0.85rem 1rem;
1325
+ }
1326
+
1327
+ .card-compact-top {
1328
+ flex-wrap: wrap;
1329
+ gap: 0.5rem;
1330
+ }
1331
+
1332
+ .card-compact-title {
1333
+ font-size: 1rem;
1334
+ }
1335
+
1336
+ .card-compact-preview {
1337
+ font-size: 0.8125rem;
1338
+ }
1339
+
1340
+ .article-card-inner {
1341
+ padding: 1rem 1.15rem;
1342
+ }
1343
+
1344
+ .search-results-header {
1345
+ flex-wrap: wrap;
1346
+ gap: 0.5rem;
1347
+ }
1348
+
1349
+ .empty-state,
1350
+ .search-empty {
1351
+ padding: 2rem 1.25rem;
1352
+ }
1353
+
1354
+ .auth-modal-box {
1355
+ margin: 1rem;
1356
+ max-height: calc(100vh - 2rem);
1357
+ overflow-y: auto;
1358
+ }
1359
+
1360
+ .comment-box-container .comment-input,
1361
+ .comment-box-textarea {
1362
+ min-height: 80px;
1363
+ font-size: 16px; /* prevents zoom on focus on iOS */
1364
+ }
1365
+ }
1366
+
1367
+ @media (max-width: 480px) {
1368
+ .top-bar {
1369
+ padding: 0 0.5rem;
1370
+ gap: 0.5rem;
1371
+ }
1372
+
1373
+ .top-bar-logo {
1374
+ font-size: 1.25rem;
1375
+ }
1376
+
1377
+ .top-bar-search {
1378
+ flex: 1;
1379
+ min-width: 0;
1380
+ }
1381
+
1382
+ .top-bar-search input,
1383
+ .top-bar-search .search-input-with-icon {
1384
+ font-size: 16px; /* prevents zoom on focus on iOS */
1385
+ min-width: 0;
1386
+ }
1387
+
1388
+ /* Auth: compact so buttons don’t overflow */
1389
+ .auth-signin-row {
1390
+ gap: 0.35rem;
1391
+ }
1392
+
1393
+ .auth-signin-row .btn-sm {
1394
+ font-size: 0.75rem;
1395
+ padding: 0.45rem 0.6rem;
1396
+ }
1397
+
1398
+ .auth-loggedin {
1399
+ flex-wrap: wrap;
1400
+ gap: 0.35rem;
1401
+ }
1402
+
1403
+ .auth-loggedin .auth-name {
1404
+ display: none; /* hide name on very small screens to save space */
1405
+ }
1406
+
1407
+ .auth-modal-box {
1408
+ margin: 0.5rem;
1409
+ padding: 1.25rem 1rem;
1410
+ }
1411
+
1412
+ .main-content {
1413
+ padding: 0.75rem;
1414
+ }
1415
+
1416
+ .card-compact-inner {
1417
+ padding: 0.75rem 0.85rem;
1418
+ }
1419
+
1420
+ .options-grid {
1421
+ grid-template-columns: 1fr;
1422
+ }
1423
+ }