mohsin-devs commited on
Commit
8b0de68
Β·
1 Parent(s): 4e3ead6

Fix link saving and polish link UI

Browse files
Files changed (5) hide show
  1. index.html +27 -1
  2. js/main.js +55 -3
  3. server/routes/api.py +12 -2
  4. server/storage/hf.py +64 -135
  5. styles.css +124 -0
index.html CHANGED
@@ -149,6 +149,21 @@
149
  </div>
150
  </section>
151
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  <section id="linksSection">
153
  <div class="section-header">
154
  <h2>Your Links</h2>
@@ -216,6 +231,7 @@
216
  <div class="input-group">
217
  <label class="input-label">URL *</label>
218
  <input type="url" id="linkUrlInput" placeholder="https://example.com" autocomplete="off">
 
219
  </div>
220
  <div class="input-group">
221
  <label class="input-label">Title</label>
@@ -225,7 +241,17 @@
225
  <label class="input-label">Description</label>
226
  <textarea id="linkDescriptionInput" placeholder="Add notes about this link..." rows="3"></textarea>
227
  </div>
 
 
 
 
 
 
 
 
 
228
  <div class="modal-footer">
 
229
  <button class="btn-secondary" id="cancelLinkBtn">Cancel</button>
230
  <button class="btn-primary" id="confirmLinkBtn"><i class="ph-fill ph-check"></i> Save Link</button>
231
  </div>
@@ -269,6 +295,6 @@
269
  <script>
270
  window.__DOCVAULT_REMOTE_API_BASE__ = 'https://mohsin-devs-docvault.hf.space/api';
271
  </script>
272
- <script type="module" src="js/main.js?v=8"></script>
273
  </body>
274
  </html>
 
149
  </div>
150
  </section>
151
 
152
+ <section class="links-insights">
153
+ <div class="links-note-chip">
154
+ <i class="ph-fill ph-globe-hemisphere-west"></i>
155
+ <span>Save articles, dashboards, and internal tools</span>
156
+ </div>
157
+ <div class="links-note-chip">
158
+ <i class="ph-fill ph-lightning"></i>
159
+ <span>Open, copy, edit, and clean up links quickly</span>
160
+ </div>
161
+ <div class="links-note-chip">
162
+ <i class="ph-fill ph-note-pencil"></i>
163
+ <span>Keep notes next to each saved website</span>
164
+ </div>
165
+ </section>
166
+
167
  <section id="linksSection">
168
  <div class="section-header">
169
  <h2>Your Links</h2>
 
231
  <div class="input-group">
232
  <label class="input-label">URL *</label>
233
  <input type="url" id="linkUrlInput" placeholder="https://example.com" autocomplete="off">
234
+ <p class="input-help">You can paste `hf.com` or a full `https://...` link.</p>
235
  </div>
236
  <div class="input-group">
237
  <label class="input-label">Title</label>
 
241
  <label class="input-label">Description</label>
242
  <textarea id="linkDescriptionInput" placeholder="Add notes about this link..." rows="3"></textarea>
243
  </div>
244
+ <div class="link-preview-panel" id="linkPreviewPanel">
245
+ <div class="link-preview-icon">
246
+ <i class="ph-fill ph-globe"></i>
247
+ </div>
248
+ <div class="link-preview-content">
249
+ <strong id="linkPreviewTitle">Website preview</strong>
250
+ <span id="linkPreviewUrl">A normalized URL preview will appear here.</span>
251
+ </div>
252
+ </div>
253
  <div class="modal-footer">
254
+ <span class="shortcut-hint">Ctrl/Cmd + Enter to save</span>
255
  <button class="btn-secondary" id="cancelLinkBtn">Cancel</button>
256
  <button class="btn-primary" id="confirmLinkBtn"><i class="ph-fill ph-check"></i> Save Link</button>
257
  </div>
 
295
  <script>
296
  window.__DOCVAULT_REMOTE_API_BASE__ = 'https://mohsin-devs-docvault.hf.space/api';
297
  </script>
298
+ <script type="module" src="js/main.js?v=9"></script>
299
  </body>
300
  </html>
js/main.js CHANGED
@@ -198,6 +198,8 @@ class App {
198
  document.getElementById('linkUrlInput').addEventListener('keydown', (e) => {
199
  if (e.key === 'Enter') document.getElementById('linkTitleInput').focus();
200
  });
 
 
201
 
202
  document.getElementById('linkDescriptionInput').addEventListener('keydown', (e) => {
203
  if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
@@ -350,6 +352,51 @@ class App {
350
  return new Date(value).toLocaleDateString();
351
  }
352
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  buildStarredDisplayFiles() {
354
  const cachedByPath = new Map(this.state.cachedFiles.map((file) => [file.path, file]));
355
  const recentByPath = new Map(this.state.recent.map((file) => [file.path, file]));
@@ -494,7 +541,10 @@ class App {
494
  <i class="ph-fill ph-link"></i>
495
  </div>
496
  <div class="link-card-content">
497
- <div class="link-title">${this.escapeHtml(link.title || link.url)}</div>
 
 
 
498
  <a href="${this.escapeHtml(link.url)}" target="_blank" rel="noopener noreferrer" class="link-url" title="${this.escapeHtml(link.url)}">
499
  ${this.escapeHtml(link.url)}
500
  </a>
@@ -949,6 +999,7 @@ class App {
949
  }
950
 
951
  modal.classList.add('active');
 
952
  setTimeout(() => document.getElementById('linkUrlInput').focus(), 50);
953
  }
954
 
@@ -958,12 +1009,13 @@ class App {
958
  const descriptionInput = document.getElementById('linkDescriptionInput');
959
  const confirmBtn = document.getElementById('confirmLinkBtn');
960
 
961
- const url = this.normalizeUrl(urlInput.value.trim());
 
962
  const title = titleInput.value.trim();
963
  const description = descriptionInput.value.trim();
964
 
965
  if (!url) {
966
- this.ui.showToast('URL is required', 'error');
967
  return;
968
  }
969
 
 
198
  document.getElementById('linkUrlInput').addEventListener('keydown', (e) => {
199
  if (e.key === 'Enter') document.getElementById('linkTitleInput').focus();
200
  });
201
+ document.getElementById('linkUrlInput').addEventListener('input', () => this.updateLinkPreview());
202
+ document.getElementById('linkTitleInput').addEventListener('input', () => this.updateLinkPreview());
203
 
204
  document.getElementById('linkDescriptionInput').addEventListener('keydown', (e) => {
205
  if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
 
352
  return new Date(value).toLocaleDateString();
353
  }
354
 
355
+ parseUrl(url) {
356
+ if (!url) return null;
357
+ try {
358
+ return new URL(this.normalizeUrl(url));
359
+ } catch (err) {
360
+ return null;
361
+ }
362
+ }
363
+
364
+ getLinkHostname(url) {
365
+ const parsed = this.parseUrl(url);
366
+ if (!parsed) return 'Unknown host';
367
+ return parsed.hostname.replace(/^www\./, '');
368
+ }
369
+
370
+ updateLinkPreview() {
371
+ const rawUrl = document.getElementById('linkUrlInput')?.value.trim() || '';
372
+ const rawTitle = document.getElementById('linkTitleInput')?.value.trim() || '';
373
+ const previewTitle = document.getElementById('linkPreviewTitle');
374
+ const previewUrl = document.getElementById('linkPreviewUrl');
375
+ const previewPanel = document.getElementById('linkPreviewPanel');
376
+ if (!previewTitle || !previewUrl || !previewPanel) return;
377
+
378
+ const parsed = this.parseUrl(rawUrl);
379
+ if (!rawUrl) {
380
+ previewPanel.classList.remove('is-valid', 'is-invalid');
381
+ previewTitle.textContent = 'Website preview';
382
+ previewUrl.textContent = 'A normalized URL preview will appear here.';
383
+ return;
384
+ }
385
+
386
+ if (!parsed) {
387
+ previewPanel.classList.remove('is-valid');
388
+ previewPanel.classList.add('is-invalid');
389
+ previewTitle.textContent = rawTitle || 'Invalid link';
390
+ previewUrl.textContent = 'Enter a valid website like https://example.com';
391
+ return;
392
+ }
393
+
394
+ previewPanel.classList.remove('is-invalid');
395
+ previewPanel.classList.add('is-valid');
396
+ previewTitle.textContent = rawTitle || this.getLinkHostname(parsed.href);
397
+ previewUrl.textContent = parsed.href;
398
+ }
399
+
400
  buildStarredDisplayFiles() {
401
  const cachedByPath = new Map(this.state.cachedFiles.map((file) => [file.path, file]));
402
  const recentByPath = new Map(this.state.recent.map((file) => [file.path, file]));
 
541
  <i class="ph-fill ph-link"></i>
542
  </div>
543
  <div class="link-card-content">
544
+ <div class="link-card-topline">
545
+ <div class="link-title">${this.escapeHtml(link.title || link.url)}</div>
546
+ <span class="link-domain-badge">${this.escapeHtml(this.getLinkHostname(link.url))}</span>
547
+ </div>
548
  <a href="${this.escapeHtml(link.url)}" target="_blank" rel="noopener noreferrer" class="link-url" title="${this.escapeHtml(link.url)}">
549
  ${this.escapeHtml(link.url)}
550
  </a>
 
999
  }
1000
 
1001
  modal.classList.add('active');
1002
+ this.updateLinkPreview();
1003
  setTimeout(() => document.getElementById('linkUrlInput').focus(), 50);
1004
  }
1005
 
 
1009
  const descriptionInput = document.getElementById('linkDescriptionInput');
1010
  const confirmBtn = document.getElementById('confirmLinkBtn');
1011
 
1012
+ const parsedUrl = this.parseUrl(urlInput.value.trim());
1013
+ const url = parsedUrl ? parsedUrl.href : '';
1014
  const title = titleInput.value.trim();
1015
  const description = descriptionInput.value.trim();
1016
 
1017
  if (!url) {
1018
+ this.ui.showToast('Enter a valid website URL', 'error');
1019
  return;
1020
  }
1021
 
server/routes/api.py CHANGED
@@ -1,6 +1,7 @@
1
  """API routes for DocVault."""
2
 
3
  import mimetypes
 
4
 
5
  import requests
6
  from flask import Blueprint, Response, jsonify, request, stream_with_context
@@ -30,6 +31,14 @@ def _storage():
30
  return get_storage()
31
 
32
 
 
 
 
 
 
 
 
 
33
  def _item_type(storage, user_id: str, path: str) -> str | None:
34
  if hasattr(storage, "get_item_type"):
35
  return storage.get_item_type(user_id, path)
@@ -288,8 +297,7 @@ def add_link():
288
  if not url:
289
  return jsonify({"success": False, "error": "url is required"}), 400
290
 
291
- # Validate URL format
292
- if not (url.startswith("http://") or url.startswith("https://")):
293
  return jsonify({"success": False, "error": "Invalid URL format"}), 400
294
 
295
  if not title:
@@ -342,6 +350,8 @@ def update_link():
342
 
343
  if not link_id:
344
  return jsonify({"success": False, "error": "link_id is required"}), 400
 
 
345
 
346
  result = _storage().update_link(user_id, link_id, url, title, description)
347
  return jsonify(result), 200 if result.get("success") else 400
 
1
  """API routes for DocVault."""
2
 
3
  import mimetypes
4
+ from urllib.parse import urlparse
5
 
6
  import requests
7
  from flask import Blueprint, Response, jsonify, request, stream_with_context
 
31
  return get_storage()
32
 
33
 
34
+ def _is_valid_http_url(value: str) -> bool:
35
+ try:
36
+ parsed = urlparse(value)
37
+ except Exception:
38
+ return False
39
+ return parsed.scheme in {"http", "https"} and bool(parsed.netloc)
40
+
41
+
42
  def _item_type(storage, user_id: str, path: str) -> str | None:
43
  if hasattr(storage, "get_item_type"):
44
  return storage.get_item_type(user_id, path)
 
297
  if not url:
298
  return jsonify({"success": False, "error": "url is required"}), 400
299
 
300
+ if not _is_valid_http_url(url):
 
301
  return jsonify({"success": False, "error": "Invalid URL format"}), 400
302
 
303
  if not title:
 
350
 
351
  if not link_id:
352
  return jsonify({"success": False, "error": "link_id is required"}), 400
353
+ if url and not _is_valid_http_url(url):
354
+ return jsonify({"success": False, "error": "Invalid URL format"}), 400
355
 
356
  result = _storage().update_link(user_id, link_id, url, title, description)
357
  return jsonify(result), 200 if result.get("success") else 400
server/storage/hf.py CHANGED
@@ -2,7 +2,9 @@
2
 
3
  from __future__ import annotations
4
 
 
5
  import os
 
6
  from datetime import datetime, timezone
7
  from typing import Any, Dict, List
8
 
@@ -55,6 +57,50 @@ class HuggingFaceStorageManager(StorageInterface):
55
  def _timestamp(self) -> str:
56
  return datetime.now(timezone.utc).isoformat()
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  def _validate_relative_path(self, path: str, label: str = "path") -> str:
59
  normalized = PathValidator._normalize_relative_path(path)
60
  if not PathValidator.is_valid_path(normalized):
@@ -503,55 +549,23 @@ class HuggingFaceStorageManager(StorageInterface):
503
  def add_link(
504
  self, user_id: str, url: str, title: str, description: str = ""
505
  ) -> Dict[str, Any]:
506
- import json
507
- import uuid
508
-
509
  self._ensure_token()
510
 
511
  try:
512
- # Get existing links
513
- links_repo_path = self._user_repo_path(user_id, ".links/data.json")
514
- repo_files = self._list_repo_files()
515
-
516
- links = []
517
- if links_repo_path in repo_files:
518
- try:
519
- content = self.api.hf_hub_download(
520
- repo_id=config.HF_REPO_ID,
521
- filename=links_repo_path,
522
- repo_type=config.HF_REPO_TYPE,
523
- )
524
- with open(content, 'r') as f:
525
- links = json.load(f)
526
- except Exception as e:
527
- logger.warning(f"Failed to load existing links: {e}")
528
- links = []
529
-
530
- # Add new link
531
  link_id = str(uuid.uuid4())
 
532
  new_link = {
533
  "id": link_id,
534
  "url": url,
535
  "title": title,
536
  "description": description,
537
- "created_at": self._timestamp(),
538
- "updated_at": self._timestamp(),
539
  }
540
  links.append(new_link)
541
 
542
- # Save links
543
- operations = [
544
- CommitOperationAdd(
545
- path_in_repo=links_repo_path,
546
- path_or_fileobj=json.dumps(links, indent=2),
547
- )
548
- ]
549
- self.api.create_commit(
550
- repo_id=config.HF_REPO_ID,
551
- repo_type=config.HF_REPO_TYPE,
552
- operations=operations,
553
- commit_message=f"Add link: {title}",
554
- )
555
 
556
  return {
557
  "success": True,
@@ -566,36 +580,11 @@ class HuggingFaceStorageManager(StorageInterface):
566
  }
567
 
568
  def get_links(self, user_id: str) -> Dict[str, Any]:
569
- import json
570
-
571
  try:
572
- links_repo_path = self._user_repo_path(user_id, ".links/data.json")
573
- repo_files = self._list_repo_files()
574
-
575
- if links_repo_path not in repo_files:
576
- return {
577
- "success": True,
578
- "links": [],
579
- }
580
-
581
- try:
582
- content = self.api.hf_hub_download(
583
- repo_id=config.HF_REPO_ID,
584
- filename=links_repo_path,
585
- repo_type=config.HF_REPO_TYPE,
586
- )
587
- with open(content, 'r') as f:
588
- links = json.load(f)
589
- return {
590
- "success": True,
591
- "links": links,
592
- }
593
- except Exception as e:
594
- logger.warning(f"Failed to load links: {e}")
595
- return {
596
- "success": True,
597
- "links": [],
598
- }
599
  except Exception as exc:
600
  logger.error(f"get_links failed: {exc}", exc_info=True)
601
  return {
@@ -605,51 +594,24 @@ class HuggingFaceStorageManager(StorageInterface):
605
  }
606
 
607
  def delete_link(self, user_id: str, link_id: str) -> Dict[str, Any]:
608
- import json
609
-
610
  self._ensure_token()
611
 
612
  try:
613
- links_repo_path = self._user_repo_path(user_id, ".links/data.json")
614
- repo_files = self._list_repo_files()
615
-
616
- if links_repo_path not in repo_files:
617
  return {
618
  "success": False,
619
  "error": "No links found",
620
  }
621
 
622
- try:
623
- content = self.api.hf_hub_download(
624
- repo_id=config.HF_REPO_ID,
625
- filename=links_repo_path,
626
- repo_type=config.HF_REPO_TYPE,
627
- )
628
- with open(content, 'r') as f:
629
- links = json.load(f)
630
- except Exception as e:
631
- logger.warning(f"Failed to load links: {e}")
632
  return {
633
  "success": False,
634
- "error": "Failed to load links",
635
  }
636
 
637
- # Remove link by ID
638
- links = [link for link in links if link.get("id") != link_id]
639
-
640
- # Save updated links
641
- operations = [
642
- CommitOperationAdd(
643
- path_in_repo=links_repo_path,
644
- path_or_fileobj=json.dumps(links, indent=2),
645
- )
646
- ]
647
- self.api.create_commit(
648
- repo_id=config.HF_REPO_ID,
649
- repo_type=config.HF_REPO_TYPE,
650
- operations=operations,
651
- commit_message=f"Delete link: {link_id}",
652
- )
653
 
654
  return {
655
  "success": True,
@@ -665,36 +627,15 @@ class HuggingFaceStorageManager(StorageInterface):
665
  def update_link(
666
  self, user_id: str, link_id: str, url: str = "", title: str = "", description: str = ""
667
  ) -> Dict[str, Any]:
668
- import json
669
-
670
  self._ensure_token()
671
 
672
  try:
673
- links_repo_path = self._user_repo_path(user_id, ".links/data.json")
674
- repo_files = self._list_repo_files()
675
-
676
- if links_repo_path not in repo_files:
677
  return {
678
  "success": False,
679
  "error": "No links found",
680
  }
681
-
682
- try:
683
- content = self.api.hf_hub_download(
684
- repo_id=config.HF_REPO_ID,
685
- filename=links_repo_path,
686
- repo_type=config.HF_REPO_TYPE,
687
- )
688
- with open(content, 'r') as f:
689
- links = json.load(f)
690
- except Exception as e:
691
- logger.warning(f"Failed to load links: {e}")
692
- return {
693
- "success": False,
694
- "error": "Failed to load links",
695
- }
696
-
697
- # Update link by ID
698
  updated = False
699
  for link in links:
700
  if link.get("id") == link_id:
@@ -714,19 +655,7 @@ class HuggingFaceStorageManager(StorageInterface):
714
  "error": "Link not found",
715
  }
716
 
717
- # Save updated links
718
- operations = [
719
- CommitOperationAdd(
720
- path_in_repo=links_repo_path,
721
- path_or_fileobj=json.dumps(links, indent=2),
722
- )
723
- ]
724
- self.api.create_commit(
725
- repo_id=config.HF_REPO_ID,
726
- repo_type=config.HF_REPO_TYPE,
727
- operations=operations,
728
- commit_message=f"Update link: {link_id}",
729
- )
730
 
731
  return {
732
  "success": True,
 
2
 
3
  from __future__ import annotations
4
 
5
+ import json
6
  import os
7
+ import uuid
8
  from datetime import datetime, timezone
9
  from typing import Any, Dict, List
10
 
 
57
  def _timestamp(self) -> str:
58
  return datetime.now(timezone.utc).isoformat()
59
 
60
+ def _links_repo_path(self, user_id: str) -> str:
61
+ return self._user_repo_path(user_id, ".links/data.json")
62
+
63
+ def _load_links(self, user_id: str) -> List[Dict[str, Any]]:
64
+ links_repo_path = self._links_repo_path(user_id)
65
+ repo_files = self._list_repo_files()
66
+
67
+ if links_repo_path not in repo_files:
68
+ return []
69
+
70
+ try:
71
+ content_path = self.api.hf_hub_download(
72
+ repo_id=config.HF_REPO_ID,
73
+ filename=links_repo_path,
74
+ repo_type=config.HF_REPO_TYPE,
75
+ )
76
+ with open(content_path, "r", encoding="utf-8") as file_obj:
77
+ links = json.load(file_obj)
78
+ except Exception as exc:
79
+ logger.warning("Failed to load links: %s", exc)
80
+ return []
81
+
82
+ if not isinstance(links, list):
83
+ logger.warning("Links payload is not a list for user %s", user_id)
84
+ return []
85
+
86
+ return [link for link in links if isinstance(link, dict)]
87
+
88
+ def _save_links(
89
+ self, user_id: str, links: List[Dict[str, Any]], commit_message: str
90
+ ) -> None:
91
+ payload = json.dumps(links, indent=2, ensure_ascii=True).encode("utf-8")
92
+ self.api.create_commit(
93
+ repo_id=config.HF_REPO_ID,
94
+ repo_type=config.HF_REPO_TYPE,
95
+ operations=[
96
+ CommitOperationAdd(
97
+ path_in_repo=self._links_repo_path(user_id),
98
+ path_or_fileobj=payload,
99
+ )
100
+ ],
101
+ commit_message=commit_message,
102
+ )
103
+
104
  def _validate_relative_path(self, path: str, label: str = "path") -> str:
105
  normalized = PathValidator._normalize_relative_path(path)
106
  if not PathValidator.is_valid_path(normalized):
 
549
  def add_link(
550
  self, user_id: str, url: str, title: str, description: str = ""
551
  ) -> Dict[str, Any]:
 
 
 
552
  self._ensure_token()
553
 
554
  try:
555
+ links = self._load_links(user_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
556
  link_id = str(uuid.uuid4())
557
+ now = self._timestamp()
558
  new_link = {
559
  "id": link_id,
560
  "url": url,
561
  "title": title,
562
  "description": description,
563
+ "created_at": now,
564
+ "updated_at": now,
565
  }
566
  links.append(new_link)
567
 
568
+ self._save_links(user_id, links, f"Add link: {title}")
 
 
 
 
 
 
 
 
 
 
 
 
569
 
570
  return {
571
  "success": True,
 
580
  }
581
 
582
  def get_links(self, user_id: str) -> Dict[str, Any]:
 
 
583
  try:
584
+ return {
585
+ "success": True,
586
+ "links": self._load_links(user_id),
587
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
588
  except Exception as exc:
589
  logger.error(f"get_links failed: {exc}", exc_info=True)
590
  return {
 
594
  }
595
 
596
  def delete_link(self, user_id: str, link_id: str) -> Dict[str, Any]:
 
 
597
  self._ensure_token()
598
 
599
  try:
600
+ links = self._load_links(user_id)
601
+ if not links:
 
 
602
  return {
603
  "success": False,
604
  "error": "No links found",
605
  }
606
 
607
+ updated_links = [link for link in links if link.get("id") != link_id]
608
+ if len(updated_links) == len(links):
 
 
 
 
 
 
 
 
609
  return {
610
  "success": False,
611
+ "error": "Link not found",
612
  }
613
 
614
+ self._save_links(user_id, updated_links, f"Delete link: {link_id}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
615
 
616
  return {
617
  "success": True,
 
627
  def update_link(
628
  self, user_id: str, link_id: str, url: str = "", title: str = "", description: str = ""
629
  ) -> Dict[str, Any]:
 
 
630
  self._ensure_token()
631
 
632
  try:
633
+ links = self._load_links(user_id)
634
+ if not links:
 
 
635
  return {
636
  "success": False,
637
  "error": "No links found",
638
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
639
  updated = False
640
  for link in links:
641
  if link.get("id") == link_id:
 
655
  "error": "Link not found",
656
  }
657
 
658
+ self._save_links(user_id, links, f"Update link: {link_id}")
 
 
 
 
 
 
 
 
 
 
 
 
659
 
660
  return {
661
  "success": True,
styles.css CHANGED
@@ -497,6 +497,32 @@ body {
497
  gap: 16px;
498
  }
499
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
  .links-stat-card,
501
  .links-search {
502
  background: var(--card);
@@ -546,6 +572,64 @@ body {
546
  color: var(--text-main);
547
  }
548
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
549
  /* ── SECTION HEADERS ── */
550
  .section-header {
551
  display: flex;
@@ -1040,6 +1124,13 @@ body {
1040
  margin-bottom: 8px;
1041
  }
1042
 
 
 
 
 
 
 
 
1043
  .input-group input {
1044
  width: 100%;
1045
  padding: 12px 16px;
@@ -1061,9 +1152,17 @@ body {
1061
  .modal-footer {
1062
  display: flex;
1063
  justify-content: flex-end;
 
 
1064
  gap: 12px;
1065
  }
1066
 
 
 
 
 
 
 
1067
  /* ── BUTTONS ── */
1068
  .btn-primary {
1069
  padding: 10px 20px;
@@ -1641,6 +1740,9 @@ body {
1641
  .links-hero h2 { font-size: 24px; }
1642
  .links-toolbar { grid-template-columns: 1fr; }
1643
  .links-stat-card strong { font-size: 22px; }
 
 
 
1644
 
1645
  /* Mobile Menu Toggle */
1646
  .menu-toggle {
@@ -1816,6 +1918,14 @@ body {
1816
  min-width: 0;
1817
  }
1818
 
 
 
 
 
 
 
 
 
1819
  .link-icon {
1820
  width: 40px;
1821
  height: 40px;
@@ -1837,6 +1947,20 @@ body {
1837
  line-height: 1.3;
1838
  }
1839
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1840
  .link-url {
1841
  font-size: 12px;
1842
  color: var(--primary-color);
 
497
  gap: 16px;
498
  }
499
 
500
+ .links-insights {
501
+ display: flex;
502
+ flex-wrap: wrap;
503
+ gap: 10px;
504
+ margin-top: -6px;
505
+ }
506
+
507
+ .links-note-chip {
508
+ display: inline-flex;
509
+ align-items: center;
510
+ gap: 8px;
511
+ padding: 10px 14px;
512
+ border-radius: 999px;
513
+ background: rgba(255, 255, 255, 0.9);
514
+ border: 1px solid var(--border-color);
515
+ color: var(--text-muted);
516
+ font-size: 13px;
517
+ font-weight: 500;
518
+ box-shadow: var(--shadow-sm);
519
+ }
520
+
521
+ .links-note-chip i {
522
+ color: var(--primary-color);
523
+ font-size: 16px;
524
+ }
525
+
526
  .links-stat-card,
527
  .links-search {
528
  background: var(--card);
 
572
  color: var(--text-main);
573
  }
574
 
575
+ .link-preview-panel {
576
+ display: flex;
577
+ align-items: center;
578
+ gap: 14px;
579
+ padding: 14px 16px;
580
+ border: 1px solid var(--border-color);
581
+ border-radius: 14px;
582
+ background: linear-gradient(180deg, #f8fafc, #f1f5f9);
583
+ margin-bottom: 20px;
584
+ transition: all 0.2s ease;
585
+ }
586
+
587
+ .link-preview-panel.is-valid {
588
+ border-color: rgba(16, 185, 129, 0.35);
589
+ background: linear-gradient(180deg, #ecfdf5, #f0fdf4);
590
+ }
591
+
592
+ .link-preview-panel.is-invalid {
593
+ border-color: rgba(239, 68, 68, 0.35);
594
+ background: linear-gradient(180deg, #fef2f2, #fff7ed);
595
+ }
596
+
597
+ .link-preview-icon {
598
+ width: 44px;
599
+ height: 44px;
600
+ border-radius: 12px;
601
+ background: #ffffff;
602
+ color: var(--primary-color);
603
+ display: flex;
604
+ align-items: center;
605
+ justify-content: center;
606
+ font-size: 20px;
607
+ box-shadow: var(--shadow-sm);
608
+ flex-shrink: 0;
609
+ }
610
+
611
+ .link-preview-panel.is-invalid .link-preview-icon {
612
+ color: var(--danger-color);
613
+ }
614
+
615
+ .link-preview-content {
616
+ min-width: 0;
617
+ display: flex;
618
+ flex-direction: column;
619
+ gap: 4px;
620
+ }
621
+
622
+ .link-preview-content strong {
623
+ font-size: 14px;
624
+ color: var(--text-main);
625
+ }
626
+
627
+ .link-preview-content span {
628
+ font-size: 12px;
629
+ color: var(--text-muted);
630
+ word-break: break-word;
631
+ }
632
+
633
  /* ── SECTION HEADERS ── */
634
  .section-header {
635
  display: flex;
 
1124
  margin-bottom: 8px;
1125
  }
1126
 
1127
+ .input-help {
1128
+ margin-top: 8px;
1129
+ font-size: 12px;
1130
+ line-height: 1.5;
1131
+ color: var(--text-muted);
1132
+ }
1133
+
1134
  .input-group input {
1135
  width: 100%;
1136
  padding: 12px 16px;
 
1152
  .modal-footer {
1153
  display: flex;
1154
  justify-content: flex-end;
1155
+ align-items: center;
1156
+ flex-wrap: wrap;
1157
  gap: 12px;
1158
  }
1159
 
1160
+ .shortcut-hint {
1161
+ margin-right: auto;
1162
+ font-size: 12px;
1163
+ color: var(--text-muted);
1164
+ }
1165
+
1166
  /* ── BUTTONS ── */
1167
  .btn-primary {
1168
  padding: 10px 20px;
 
1740
  .links-hero h2 { font-size: 24px; }
1741
  .links-toolbar { grid-template-columns: 1fr; }
1742
  .links-stat-card strong { font-size: 22px; }
1743
+ .links-note-chip { width: 100%; justify-content: center; }
1744
+ .link-card-topline { flex-direction: column; }
1745
+ .shortcut-hint { width: 100%; margin-right: 0; }
1746
 
1747
  /* Mobile Menu Toggle */
1748
  .menu-toggle {
 
1918
  min-width: 0;
1919
  }
1920
 
1921
+ .link-card-topline {
1922
+ display: flex;
1923
+ align-items: flex-start;
1924
+ justify-content: space-between;
1925
+ gap: 10px;
1926
+ margin-bottom: 6px;
1927
+ }
1928
+
1929
  .link-icon {
1930
  width: 40px;
1931
  height: 40px;
 
1947
  line-height: 1.3;
1948
  }
1949
 
1950
+ .link-domain-badge {
1951
+ display: inline-flex;
1952
+ align-items: center;
1953
+ white-space: nowrap;
1954
+ padding: 5px 10px;
1955
+ border-radius: 999px;
1956
+ background: var(--hover-bg);
1957
+ color: var(--text-muted);
1958
+ border: 1px solid var(--border-color);
1959
+ font-size: 11px;
1960
+ font-weight: 700;
1961
+ letter-spacing: 0.3px;
1962
+ }
1963
+
1964
  .link-url {
1965
  font-size: 12px;
1966
  color: var(--primary-color);