style(ui): refactor reader layout grid, fix focus mode layout collapse, fix SVG rendering dots, reorganize intelligence toolbar (#69)

Reorganized the reader toolbar and layout grid to improve visual consistency and layout robustness in Focus Mode. Fixed outline SVG rendering bugs that caused icons to show as solid dots.

Closes #70

---------

Co-authored-by: Marek Jasiński <jasins.marek@gmail.com>
Reviewed-on: #69
Co-authored-by: Antigravity <antigravity@google.com>
Co-committed-by: Antigravity <antigravity@google.com>
This commit was merged in pull request #69.
This commit is contained in:
2026-06-05 09:51:29 +00:00
committed by Marek Jaisński
parent 081c6f7940
commit f18663426b
24 changed files with 2022 additions and 571 deletions
@@ -2,106 +2,129 @@
@switch (Name.ToLowerInvariant())
{
case "home":
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<polyline points="9 22 9 12 15 12 15 22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<polyline points="9 22 9 12 15 12 15 22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
case "map":
<polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="8" y1="2" x2="8" y2="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="16" y1="6" x2="16" y2="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="8" y1="2" x2="8" y2="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="16" y1="6" x2="16" y2="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
case "share":
case "share-2":
<circle cx="18" cy="5" r="3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<circle cx="6" cy="12" r="3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<circle cx="18" cy="19" r="3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<circle cx="18" cy="5" r="3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<circle cx="6" cy="12" r="3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<circle cx="18" cy="19" r="3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
case "help-circle":
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="12" y1="17" x2="12.01" y2="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="12" y1="17" x2="12.01" y2="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
case "robot":
<path d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h5a2 2 0 0 1 2 2v2a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2a2 2 0 0 1-2-2v-2a2 2 0 0 1 2-2V9c0-1.1.9-2 2-2h5V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2zM8 11v4h8v-4H8zm-2 0H4v4h2v-4zm14 0h-2v4h2v-4z" />
<path d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h5a2 2 0 0 1 2 2v2a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2a2 2 0 0 1-2-2v-2a2 2 0 0 1 2-2V9c0-1.1.9-2 2-2h5V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2zM8 11v4h8v-4H8zm-2 0H4v4h2v-4zm14 0h-2v4h2v-4z" />
break;
case "play":
<path d="M8 5v14l11-7z" />
<path d="M8 5v14l11-7z" />
break;
case "check":
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
break;
case "search":
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" />
<circle cx="11" cy="11" r="8" fill="none" stroke="currentColor" stroke-width="2" />
<line x1="21" y1="21" x2="16.65" y2="16.65" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
break;
case "message-square":
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
case "diamond":
<path d="M12 3L3 12L12 21L21 12L12 3Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M12 3L3 12L12 21L21 12L12 3Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
case "layout":
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="3" y1="9" x2="21" y2="9" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="9" y1="21" x2="9" y2="9" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="3" y1="9" x2="21" y2="9" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="9" y1="21" x2="9" y2="9" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
case "book":
case "book-open":
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
case "user":
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<circle cx="12" cy="7" r="4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<circle cx="12" cy="7" r="4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
case "settings":
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
case "bookmark":
<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" />
<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
case "target":
<circle cx="12" cy="12" r="10" /><circle cx="12" cy="12" r="6" /><circle cx="12" cy="12" r="2" />
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" />
<circle cx="12" cy="12" r="6" fill="none" stroke="currentColor" stroke-width="2" />
<circle cx="12" cy="12" r="2" fill="none" stroke="currentColor" stroke-width="2" />
break;
case "trash":
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M10 11v6M14 11v6" />
<polyline points="3 6 5 6 21 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="10" y1="11" x2="10" y2="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="14" y1="11" x2="14" y2="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
case "mail":
<rect width="20" height="16" x="2" y="4" rx="2" /><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
<rect width="20" height="16" x="2" y="4" rx="2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
case "lock":
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" />
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
case "eye":
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" /><circle cx="12" cy="12" r="3" />
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
case "eye-off":
<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24" /><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68" /><path d="M6.61 6.61A13.52 13.52 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61" /><line x1="2" x2="22" y1="2" y2="22" />
<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M6.61 6.61A13.52 13.52 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="2" x2="22" y1="2" y2="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
case "arrow-left":
<path d="M19 12H5M12 19l-7-7 7-7" />
<line x1="19" y1="12" x2="5" y2="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<polyline points="12 19 5 12 12 5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
case "arrow-right":
<path d="M5 12h14M12 5l7 7-7 7" />
<line x1="5" y1="12" x2="19" y2="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<polyline points="12 5 19 12 12 19" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
case "log-out":
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
case "chevron-left":
<polyline points="15 18 9 12 15 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<polyline points="15 18 9 12 15 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
case "chevron-right":
<polyline points="9 18 15 12 9 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<polyline points="9 18 15 12 9 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
case "x":
case "close":
<line x1="18" y1="6" x2="6" y2="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="6" y1="6" x2="18" y2="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="18" y1="6" x2="6" y2="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="6" y1="6" x2="18" y2="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
case "sun":
<circle cx="12" cy="12" r="4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
case "moon":
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
default:
<!-- Fallback circle -->
<circle cx="12" cy="12" r="10" />
<!-- Fallback circle -->
<circle cx="12" cy="12" r="10" />
break;
}
</svg>

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1,47 @@
@namespace NexusReader.UI.Shared.Components.Molecules
<div class="nexus-callout-box nexus-callout-@Type.ToString().ToLower() @Class">
@if (!string.IsNullOrEmpty(Title))
{
<div class="nexus-callout-header">
@if (Type == CalloutType.Warning || Type == CalloutType.Error)
{
<NexusIcon Name="warning" Size="16" Class="nexus-callout-icon" />
}
else if (Type == CalloutType.Success)
{
<NexusIcon Name="check" Size="16" Class="nexus-callout-icon" />
}
else
{
<NexusIcon Name="info" Size="16" Class="nexus-callout-icon" />
}
<span class="nexus-callout-title">@Title</span>
</div>
}
<div class="nexus-callout-body">
@ChildContent
</div>
</div>
@code {
public enum CalloutType
{
Info,
Warning,
Success,
Error
}
[Parameter]
public CalloutType Type { get; set; } = CalloutType.Info;
[Parameter]
public string? Title { get; set; }
[Parameter]
public RenderFragment? ChildContent { get; set; }
[Parameter]
public string Class { get; set; } = string.Empty;
}
@@ -0,0 +1,122 @@
.nexus-callout-box {
padding: 1rem 1.25rem;
margin: 1.5rem 0 1.5rem 0;
border-radius: 0 8px 8px 0;
font-family: var(--nexus-font-sans, sans-serif);
font-size: 0.95rem;
line-height: 1.5;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
border: 1px solid transparent;
border-left-width: 4px;
}
/* Light / Dark default support via variables or custom colors */
.nexus-callout-box {
background-color: rgba(255, 255, 255, 0.02);
color: #e2e8f0;
}
/* Info style */
.nexus-callout-info {
border-left-color: var(--nexus-neon, #00ff99);
}
/* Warning style */
.nexus-callout-warning {
border-left-color: #eab308; /* warning yellow */
background-color: rgba(234, 179, 8, 0.03);
}
/* Success style */
.nexus-callout-success {
border-left-color: #10b981; /* success green */
background-color: rgba(16, 185, 129, 0.03);
}
/* Error style */
.nexus-callout-error {
border-left-color: #f43f5e; /* error red */
background-color: rgba(244, 63, 94, 0.03);
}
.nexus-callout-header {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
margin-bottom: 0.5rem;
text-transform: uppercase;
font-size: 0.8rem;
letter-spacing: 0.05em;
}
.nexus-callout-info .nexus-callout-header {
color: var(--nexus-neon, #00ff99);
}
.nexus-callout-warning .nexus-callout-header {
color: #eab308;
}
.nexus-callout-success .nexus-callout-header {
color: #10b981;
}
.nexus-callout-error .nexus-callout-header {
color: #f43f5e;
}
.nexus-callout-icon {
flex-shrink: 0;
}
.nexus-callout-body {
opacity: 0.9;
}
/* Light theme support */
.theme-light .nexus-callout-box {
background-color: #fcfcfb;
border: 1px solid rgba(0, 0, 0, 0.03);
border-left-width: 4px;
color: #44403c;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.015);
}
.theme-light .nexus-callout-info {
border-left-color: #10b981;
background-color: rgba(16, 185, 129, 0.04);
}
.theme-light .nexus-callout-info .nexus-callout-header {
color: #059669;
}
.theme-light .nexus-callout-warning {
border-left-color: #d97706;
background-color: rgba(217, 119, 6, 0.04);
}
.theme-light .nexus-callout-warning .nexus-callout-header {
color: #d97706;
}
.theme-light .nexus-callout-success {
border-left-color: #10b981;
background-color: rgba(16, 185, 129, 0.04);
}
.theme-light .nexus-callout-success .nexus-callout-header {
color: #059669;
}
.theme-light .nexus-callout-error {
border-left-color: #e11d48;
background-color: rgba(225, 29, 72, 0.04);
}
.theme-light .nexus-callout-error .nexus-callout-header {
color: #e11d48;
}
@@ -1,45 +1,43 @@
@using NexusReader.UI.Shared.Services
@using NexusReader.Application.Abstractions.Services
@inject IFocusModeService FocusMode
@inject IKnowledgeService KnowledgeService
@inject IIdentityService IdentityService
@inject NavigationManager NavigationManager
@inject IThemeService ThemeService
@inject IKnowledgeService KnowledgeService
@implements IDisposable
<aside class="intelligence-toolbar">
<div class="toolbar-top">
<button class="toolbar-item" @onclick='() => NavigationManager.NavigateTo("/")' title="Back to Dashboard">
<NexusIcon Name="arrow-left" Size="20" />
</button>
<button class="toolbar-item active" title="Chat">
<NexusIcon Name="message-square" Size="20" />
</button>
</div>
<div class="toolbar-middle">
<button class="toolbar-item" title="Settings">
<NexusIcon Name="settings" Size="20" />
</button>
<button class="toolbar-item" title="Bookmarks">
<NexusIcon Name="bookmark" Size="20" />
</button>
<button class="toolbar-item" title="Search">
<NexusIcon Name="search" Size="20" />
</button>
<button class="toolbar-item danger" @onclick="HandleClearCache" title="Clear AI Cache">
<NexusIcon Name="trash" Size="20" />
</button>
@if (FocusMode.IsFocusModeActive)
{
<button class="toolbar-item active" @onclick="FocusMode.ToggleAsync" title="Focus Mode Active (Click to Exit)">
<NexusIcon Name="target" Size="20" />
</button>
}
else
{
<button class="toolbar-item active" @onclick="FocusMode.ToggleAsync" title="Chat Active (Click to Focus)">
<NexusIcon Name="message-square" Size="20" />
</button>
}
</div>
<div class="toolbar-bottom">
<button class="toolbar-item @(FocusMode.IsFocusModeActive ? "active focus-active" : "")"
@onclick="FocusMode.ToggleAsync" title="Focus Mode (F)">
<NexusIcon Name="target" Size="20" />
<div class="toolbar-separator"></div>
<button class="toolbar-item" @onclick="ThemeService.ToggleTheme" title="Przełącz motyw">
<NexusIcon Name="@(ThemeService.IsLightMode ? "sun" : "moon")" Size="20" />
</button>
<button class="toolbar-item" @onclick='() => NavigationManager.NavigateTo("/")' title="Global Hub">
<NexusIcon Name="layers" Size="20" />
</button>
<button class="toolbar-item logout-item" @onclick="HandleLogout" title="Exit">
<NexusIcon Name="log-out" Size="20" />
<div class="toolbar-separator"></div>
<button class="toolbar-item clear-cache-item" @onclick="HandleClearCache" title="Wyczyść pamięć AI">
<NexusIcon Name="trash" Size="20" />
</button>
</div>
</aside>
@@ -48,11 +46,11 @@
protected override void OnInitialized()
{
FocusMode.OnFocusModeChanged += HandleUpdate;
ThemeService.OnThemeChanged += HandleThemeChangedAsync;
}
private async Task HandleClearCache()
{
// For now, a simple console log confirm or just do it
Console.WriteLine("[IntelligenceToolbar] Requesting cache clear...");
var result = await KnowledgeService.ClearCacheAsync();
if (result.IsSuccess)
@@ -61,16 +59,13 @@
}
}
private async Task HandleLogout()
{
await IdentityService.LogoutAsync();
NavigationManager.NavigateTo("/account/logout-form", true);
}
private Task HandleUpdate() => InvokeAsync(StateHasChanged);
private Task HandleThemeChangedAsync() => InvokeAsync(StateHasChanged);
public void Dispose()
{
FocusMode.OnFocusModeChanged -= HandleUpdate;
ThemeService.OnThemeChanged -= HandleThemeChangedAsync;
}
}
@@ -71,26 +71,53 @@
transform: rotate(180deg);
}
.toolbar-item.danger:hover {
color: #ff4d4d;
background: rgba(255, 77, 77, 0.1);
.toolbar-separator {
width: 24px;
height: 1px;
background: rgba(255, 255, 255, 0.08);
margin: 0.2rem 0;
}
.toolbar-item.logout-item {
margin-top: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.08);
padding-top: 1.5rem;
height: auto;
width: 100%;
display: flex;
justify-content: center;
border-radius: 0;
color: #444;
/* Light mode overrides */
.theme-light .intelligence-toolbar {
background: #f5f5f4;
border-right: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: inset -2px 0 10px rgba(0, 0, 0, 0.02);
}
.toolbar-item.logout-item:hover {
color: #ff4d4d;
background: none;
filter: drop-shadow(0 0 8px rgba(255, 77, 77, 0.4));
.theme-light .toolbar-item {
color: #78716c;
}
.theme-light .toolbar-item:hover {
color: #10b981;
background: rgba(16, 185, 129, 0.05);
box-shadow: 0 0 10px rgba(16, 185, 129, 0.1);
filter: none;
}
.theme-light .toolbar-item.active {
color: #10b981;
background: rgba(16, 185, 129, 0.08);
box-shadow: 0 0 15px rgba(16, 185, 129, 0.15);
filter: none;
}
.theme-light .toolbar-item.active::after {
background: #10b981;
box-shadow: none;
}
.theme-light .toolbar-item.focus-active {
color: #10b981;
filter: none;
}
.theme-light .toolbar-separator {
background: rgba(0, 0, 0, 0.08);
}
@@ -335,3 +335,175 @@
box-shadow: none;
}
/* Light mode overrides */
.theme-light .knowledge-check {
background: #fafaf9;
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.02);
}
.theme-light .header-title {
color: #1c1917;
}
.theme-light .question-text {
color: #44403c;
}
.theme-light .option-item {
background: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.08);
}
.theme-light .option-item:hover {
background: #f5f5f4;
}
.theme-light .option-item.selected {
border-color: #10b981;
background: rgba(16, 185, 129, 0.04);
}
.theme-light .option-letter {
color: #059669;
}
.theme-light .option-text {
color: #292524;
}
.theme-light .option-correct {
border-color: #10b981 !important;
background: rgba(16, 185, 129, 0.08) !important;
}
.theme-light .option-incorrect {
border-color: #f43f5e !important;
background: rgba(244, 63, 94, 0.08) !important;
}
.theme-light .option-revealed-correct {
border-color: #10b981 !important;
background: rgba(16, 185, 129, 0.06) !important;
box-shadow: 0 0 8px rgba(16, 185, 129, 0.1);
}
.theme-light .loading-state.shimmer {
background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.03), transparent);
color: #10b981;
text-shadow: none;
}
.theme-light .submit-btn {
background: rgba(0, 0, 0, 0.03);
border: 1px solid rgba(0, 0, 0, 0.15);
color: #78716c;
}
.theme-light .submit-btn:not(:disabled) {
background: #10b981;
color: #ffffff;
border-color: #10b981;
}
.theme-light .submitted-title {
color: #1c1917;
}
.theme-light .submitted-text {
color: #78716c;
}
.theme-light .score-card {
background: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.06);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.02);
}
.theme-light .score-num {
color: #10b981;
text-shadow: none;
}
.theme-light .score-divider {
color: #e7e5e4;
}
.theme-light .score-total {
color: #292524;
}
.theme-light .score-percent {
color: #78716c;
}
.theme-light .reset-quiz-btn {
border: 1px solid rgba(0, 0, 0, 0.15);
color: #44403c;
}
.theme-light .reset-quiz-btn:hover {
background: rgba(0, 0, 0, 0.03);
border-color: #1c1917;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.03);
}
.theme-light .empty-title {
color: #1c1917;
}
.theme-light .empty-text {
color: #78716c;
}
.theme-light .empty-icon-wrapper {
background: rgba(16, 185, 129, 0.02);
border: 1px solid rgba(16, 185, 129, 0.1);
box-shadow: 0 0 20px rgba(16, 185, 129, 0.02);
}
.theme-light .empty-quiz-state:hover .empty-icon-wrapper {
background: rgba(16, 185, 129, 0.06);
border-color: rgba(16, 185, 129, 0.3);
box-shadow: 0 0 25px rgba(16, 185, 129, 0.1);
}
.theme-light .generate-quiz-btn {
background: rgba(16, 185, 129, 0.05);
border: 1px solid #10b981;
color: #10b981;
text-shadow: none;
box-shadow: 0 0 15px rgba(16, 185, 129, 0.05);
}
.theme-light .generate-quiz-btn:not(:disabled):hover {
background: #10b981;
color: #ffffff;
box-shadow: 0 0 25px rgba(16, 185, 129, 0.2);
transform: translateY(-2px);
}
.theme-light .generate-quiz-btn:disabled {
border-color: rgba(0, 0, 0, 0.1);
background: rgba(0, 0, 0, 0.02);
color: #a8a29e;
box-shadow: none;
}
.theme-light .success-icon-wrapper {
background: rgba(16, 185, 129, 0.05);
border: 1px solid rgba(16, 185, 129, 0.2);
box-shadow: 0 0 20px rgba(16, 185, 129, 0.08);
}
.theme-light .success-glow {
color: #10b981;
filter: none;
}
.theme-light .neon-glow {
color: #10b981;
filter: none;
}
@@ -3,49 +3,47 @@
@using NexusReader.Application.DTOs.AI
@inject KnowledgeCoordinator Coordinator
@inject IReaderInteractionService InteractionService
@inject IQuizStateService QuizService
@inject IJSRuntime JS
@if (IsVisible)
{
<div class="selection-ai-panel expanded @(PositionBelow ? "below" : "")" style="@PanelStyle">
<div class="ai-bubble">
<div class="ai-avatar">
<div class="avatar-ring"></div>
<NexusIcon Name="robot" Size="48" Class="neon-pulse" />
<div class="avatar-label">
<span class="name">E-Czytnik</span>
<span class="role">Asystent AI</span>
</div>
</div>
<div class="ai-content">
@if (IsLoading)
{
<div class="loading-state">
<div class="shimmer">Skanowanie fragmentu...</div>
</div>
}
else if (Packet != null)
{
<div class="summary-box">
<NexusTypography Variant="NexusTypography.TypographyVariant.UI">@Packet.Summary</NexusTypography>
</div>
<div class="ai-actions">
<button class="action-btn neon-border" @onclick="GenerateFullQuiz">Generuj Quiz dla całej strony</button>
<button class="action-btn ghost" @onclick="CloseAsync">Zamknij</button>
</div>
}
else
{
<div class="summary-box">
<NexusTypography Variant="NexusTypography.TypographyVariant.UI">Wykryto ciekawy fragment! Czy chcesz, abym wygenerował podsumowanie lub quiz z tego rozdziału?</NexusTypography>
</div>
<div class="ai-actions">
<button class="action-btn neon-border" @onclick="RequestSummary">Podsumuj zaznaczenie</button>
<button class="action-btn ghost" @onclick="CloseAsync">Pomiń</button>
</div>
}
</div>
<div class="bubble-pointer"></div>
</div>
<div class="selection-ai-panel @(_positionBelow ? "below" : "")" style="@_style">
<button id="summary-btn" class="toolbar-btn primary @(IsLoadingSummary ? "loading" : "") @(IsAnyLoading ? "disabled cursor-not-allowed opacity-50" : "")"
disabled="@IsAnyLoading"
@onclick="RequestSummaryAsync">
@if (IsLoadingSummary)
{
<svg class="animate-spin h-4 w-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" style="animation: spin 1s linear infinite; width: 14px; height: 14px; color: currentColor; display: inline-block; margin-right: 4px;">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" style="opacity: 0.25;"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" style="opacity: 0.75;"></path>
</svg>
<span class="btn-text">Podsumowywanie...</span>
}
else
{
<NexusIcon Name="book-open" Size="14" Class="btn-icon" />
<span class="btn-text">Podsumuj</span>
}
</button>
<div class="toolbar-divider"></div>
<button id="quiz-btn" class="toolbar-btn secondary @(IsLoadingQuiz ? "loading" : "") @(IsAnyLoading ? "disabled cursor-not-allowed opacity-50" : "")"
disabled="@IsAnyLoading"
@onclick="GenerateQuizAsync">
@if (IsLoadingQuiz)
{
<svg class="animate-spin h-4 w-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" style="animation: spin 1s linear infinite; width: 14px; height: 14px; color: currentColor; display: inline-block; margin-right: 4px;">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" style="opacity: 0.25;"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" style="opacity: 0.75;"></path>
</svg>
<span class="btn-text">Generowanie...</span>
}
else
{
<NexusIcon Name="target" Size="14" Class="btn-icon" />
<span class="btn-text">Quiz</span>
}
</button>
</div>
}
@@ -56,47 +54,145 @@
[Parameter] public string FullPageContent { get; set; } = string.Empty;
private bool IsVisible => !string.IsNullOrEmpty(SelectedText) && Coordinates != null;
private bool IsLoading = false;
private KnowledgePacket? Packet;
private bool PositionBelow => Coordinates != null && Coordinates.Top < 320;
private bool IsLoadingSummary = false;
private bool IsLoadingQuiz = false;
private bool IsAnyLoading => IsLoadingSummary || IsLoadingQuiz;
private string _style = "visibility: hidden; opacity: 0; pointer-events: none;";
private bool _positionBelow = false;
private SelectionCoordinates? _lastCoordinates;
protected override void OnParametersSet()
{
Console.WriteLine($"[SelectionAiPanel] Parameters set. SelectedText: {SelectedText.Length} chars, Coordinates: {Coordinates?.Top}, PositionBelow: {PositionBelow}");
// Reset packet when selection changes
Packet = null;
}
private string PanelStyle => Coordinates != null
? string.Create(System.Globalization.CultureInfo.InvariantCulture,
$"top: {(PositionBelow ? Coordinates.Top + 35 : Coordinates.Top - 15):F1}px !important; " +
$"left: {Math.Clamp(Coordinates.Left + Coordinates.Width / 2, 280, 1600):F1}px !important; " +
$"transform: translate(-50%, {(PositionBelow ? "0" : "-100%")}) !important;")
: "";
private async Task RequestSummary()
{
IsLoading = true;
var contextPrompt = !string.IsNullOrWhiteSpace(FullPageContent)
? $"ANALYSIS CONTEXT (Full Page Content):\n{FullPageContent}\n\nUSER SELECTION TO SUMMARIZE:\n"
: "";
Console.WriteLine($"[SelectionAiPanel] Parameters set. SelectedText: {SelectedText.Length} chars, Coordinates: {Coordinates?.Top}");
var result = await Coordinator.RequestSummaryAndQuizAsync($"{contextPrompt}{SelectedText}");
Packet = result.IsSuccess ? result.Value : null;
IsLoading = false;
if (Coordinates != _lastCoordinates)
{
_lastCoordinates = Coordinates;
_style = "visibility: hidden; opacity: 0; pointer-events: none;";
_positionBelow = false;
}
// Reset loading states when parameters change
IsLoadingSummary = false;
IsLoadingQuiz = false;
}
private async Task GenerateFullQuiz()
protected override async Task OnAfterRenderAsync(bool firstRender)
{
IsLoading = true;
await Coordinator.RequestSummaryAndQuizAsync(FullPageContent);
IsLoading = false;
await CloseAsync();
if (IsVisible && _style.Contains("visibility: hidden"))
{
try
{
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js");
var result = await module.InvokeAsync<PositionResult>("positionToolbar");
if (result != null)
{
_style = string.Create(System.Globalization.CultureInfo.InvariantCulture,
$"left: {result.Left:F1}px !important; " +
$"top: {result.Top:F1}px !important; " +
$"visibility: visible !important; " +
$"opacity: 1 !important; " +
$"pointer-events: auto !important;");
_positionBelow = result.Below;
StateHasChanged();
}
}
catch (Exception ex)
{
Console.WriteLine($"[SelectionAiPanel] Error positioning toolbar: {ex.Message}");
}
}
}
private async Task RequestSummaryAsync()
{
if (IsAnyLoading) return;
IsLoadingSummary = true;
StateHasChanged();
try
{
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js");
var selectedText = await module.InvokeAsync<string>("getSelectionText");
if (string.IsNullOrWhiteSpace(selectedText))
{
selectedText = SelectedText;
}
if (!string.IsNullOrWhiteSpace(selectedText))
{
var contextPrompt = !string.IsNullOrWhiteSpace(FullPageContent)
? $"ANALYSIS CONTEXT (Full Page Content):\n{FullPageContent}\n\nUSER SELECTION TO SUMMARIZE:\n"
: "";
_ = Coordinator.StartSelectionSummaryAsync($"{contextPrompt}{selectedText}");
await CloseAsync();
await InteractionService.RequestAssistant();
}
}
catch (Exception ex)
{
Console.WriteLine($"[SelectionAiPanel] Error requesting summary: {ex.Message}");
}
finally
{
IsLoadingSummary = false;
StateHasChanged();
}
}
private async Task GenerateQuizAsync()
{
if (IsAnyLoading) return;
IsLoadingQuiz = true;
StateHasChanged();
try
{
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js");
var selectedText = await module.InvokeAsync<string>("getSelectionText");
if (string.IsNullOrWhiteSpace(selectedText))
{
selectedText = SelectedText;
}
if (!string.IsNullOrWhiteSpace(selectedText))
{
var contextPrompt = !string.IsNullOrWhiteSpace(FullPageContent)
? $"ANALYSIS CONTEXT (Full Page Content):\n{FullPageContent}\n\nUSER SELECTION TO SUMMARIZE:\n"
: "";
var result = await Coordinator.RequestSummaryAndQuizAsync($"{contextPrompt}{selectedText}");
if (result.IsSuccess)
{
await CloseAsync();
await QuizService.RequestQuiz(BlockId);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"[SelectionAiPanel] Error generating quiz: {ex.Message}");
}
finally
{
IsLoadingQuiz = false;
StateHasChanged();
}
}
private async Task CloseAsync()
{
Packet = null;
await InteractionService.NotifyTextSelected(string.Empty, string.Empty, null!);
}
private class PositionResult
{
public double Left { get; set; }
public double Top { get; set; }
public bool Below { get; set; }
}
}
@@ -1,158 +1,149 @@
.selection-ai-panel {
position: fixed;
z-index: 9999;
width: 550px;
max-width: 90vw;
animation: fadeInScale 0.2s ease-out;
pointer-events: auto;
position: absolute;
z-index: 10000;
display: flex;
align-items: center;
background: rgba(24, 24, 28, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5), 0 12px 28px rgba(0, 0, 0, 0.4);
padding: 4px 6px;
gap: 4px;
pointer-events: none; /* Controlled by inline styles */
user-select: none;
animation: fadeInScale 0.18s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes fadeInScale {
from { opacity: 0; transform: translate(-50%, -90%) scale(0.95); }
to { opacity: 1; transform: translate(-50%, -100%) scale(1); }
from {
opacity: 0;
transform: scale(0.96);
}
to {
opacity: 1;
transform: scale(1);
}
}
.ai-bubble {
position: relative;
display: flex;
flex-direction: row;
gap: 1.5rem;
padding: 1.5rem;
background: rgba(18, 18, 18, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
color: #fff;
.selection-ai-panel.below {
animation: fadeInScaleBelow 0.18s cubic-bezier(0.16, 1, 0.3, 1);
}
.ai-avatar {
@keyframes fadeInScaleBelow {
from {
opacity: 0;
transform: scale(0.96);
}
to {
opacity: 1;
transform: scale(1);
}
}
.toolbar-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
min-width: 100px;
}
.avatar-label {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.avatar-label .name {
gap: 6px;
padding: 6px 12px;
background: transparent;
border: none;
border-radius: 6px;
color: #e4e4e7; /* zinc-200 */
font-size: 0.8rem;
font-weight: 600;
color: #fff;
}
.avatar-label .role {
font-size: 0.7rem;
opacity: 0.6;
}
.neon-pulse {
color: #00ff99;
filter: drop-shadow(0 0 8px #00ff99);
animation: pulse 2s infinite ease-in-out;
}
@keyframes pulse {
0% { transform: scale(1); filter: drop-shadow(0 0 8px #00ff99); }
50% { transform: scale(1.05); filter: drop-shadow(0 0 15px #00ff99); }
100% { transform: scale(1); filter: drop-shadow(0 0 8px #00ff99); }
}
.ai-content {
display: flex;
flex-direction: column;
gap: 1rem;
justify-content: center;
}
.summary-box {
font-size: 0.95rem;
line-height: 1.5;
color: #e0e0e0;
max-height: 40vh;
overflow-y: auto;
padding-right: 8px;
}
.summary-box::-webkit-scrollbar {
width: 4px;
}
.summary-box::-webkit-scrollbar-thumb {
background: rgba(0, 255, 153, 0.3);
border-radius: 2px;
}
.ai-actions {
display: flex;
gap: 1rem;
}
.action-btn {
padding: 0.5rem 1.2rem;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
white-space: nowrap;
font-family: inherit;
}
.action-btn.ghost {
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.2);
color: #aaa;
.toolbar-btn:hover:not(.disabled) {
background: rgba(255, 255, 255, 0.05);
color: #ffffff;
}
.action-btn.neon-border {
background: rgba(0, 255, 153, 0.1);
border: 1px solid #00ff99;
color: #00ff99;
.toolbar-btn.primary {
color: var(--nexus-neon, #00ff99);
}
.action-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 255, 153, 0.2);
.toolbar-btn.primary:hover:not(.disabled) {
background: rgba(0, 255, 153, 0.08);
box-shadow: 0 0 12px rgba(0, 255, 153, 0.15);
}
.bubble-pointer {
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
.toolbar-btn.disabled {
opacity: 0.35;
cursor: not-allowed;
pointer-events: none;
}
.selection-ai-panel:not(.below) .bubble-pointer {
bottom: -10px;
border-top: 10px solid rgba(18, 18, 18, 0.95);
.toolbar-divider {
width: 1px;
height: 16px;
background: rgba(255, 255, 255, 0.1);
}
.selection-ai-panel.below .bubble-pointer {
top: -10px;
border-bottom: 10px solid rgba(18, 18, 18, 0.95);
.btn-icon {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.loading-state {
padding: 1rem;
.spinner-inline {
width: 12px;
height: 12px;
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
display: inline-block;
flex-shrink: 0;
}
.shimmer {
background: linear-gradient(90deg, transparent, rgba(0, 255, 153, 0.2), transparent);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
padding: 0.5rem;
border-radius: 4px;
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes shimmer {
from { background-position: 200% 0; }
to { background-position: -200% 0; }
.opacity-50 {
opacity: 0.5 !important;
}
.cursor-not-allowed {
cursor: not-allowed !important;
}
/* Light mode overrides */
.theme-light .selection-ai-panel {
background: rgba(254, 254, 254, 0.95);
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05), 0 10px 30px rgba(0, 0, 0, 0.04);
}
.theme-light .toolbar-btn {
color: #57524e;
}
.theme-light .toolbar-btn:hover:not(.disabled) {
background: rgba(0, 0, 0, 0.04);
color: #1c1917;
}
.theme-light .toolbar-btn.primary {
color: #10b981;
}
.theme-light .toolbar-btn.primary:hover:not(.disabled) {
background: rgba(16, 185, 129, 0.06);
box-shadow: 0 0 12px rgba(16, 185, 129, 0.1);
}
.theme-light .toolbar-divider {
background: rgba(0, 0, 0, 0.08);
}
@@ -41,7 +41,7 @@
</div>
</div>
<div class="verification-state" style="@(IsVerifying ? "display:flex;" : "display:none;")">
<div class="verification-state" style="@((IsVerifying && !IsIngesting) ? "display:flex;" : "display:none;")">
@if (Metadata != null)
{
<div class="verification-layout">
@@ -196,52 +196,37 @@
margin-top: 1rem;
}
.btn {
font-family: var(--nexus-font-sans);
font-weight: 600;
padding: 0.75rem 1.5rem;
border-radius: 8px;
border: 1px solid transparent;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 0.85rem;
letter-spacing: 0.5px;
display: inline-flex;
align-items: center;
justify-content: center;
text-transform: uppercase;
::deep .nexus-btn.btn-primary {
background: var(--nexus-neon, #00ffaa) !important;
color: #050505 !important;
border-color: transparent !important;
box-shadow: 0 4px 12px rgba(var(--nexus-accent-rgb, 0, 255, 170), 0.2) !important;
}
.btn-primary {
background: var(--nexus-neon, #00ffaa);
color: #050505;
box-shadow: 0 4px 12px rgba(var(--nexus-accent-rgb, 0, 255, 170), 0.2);
::deep .nexus-btn.btn-primary:hover:not(:disabled) {
background: #00e699 !important;
transform: translateY(-2px) !important;
box-shadow: 0 6px 20px rgba(var(--nexus-accent-rgb, 0, 255, 170), 0.4) !important;
}
.btn-primary:hover {
background: #00e699;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(var(--nexus-accent-rgb, 0, 255, 170), 0.4);
::deep .nexus-btn.btn-primary:active:not(:disabled) {
transform: translateY(0) !important;
}
.btn-primary:active {
transform: translateY(0);
::deep .nexus-btn.btn-secondary {
background: rgba(255, 255, 255, 0.03) !important;
color: var(--nexus-text) !important;
border: 1px solid rgba(255, 255, 255, 0.1) !important;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.03);
color: var(--nexus-text);
border: 1px solid rgba(255, 255, 255, 0.1);
::deep .nexus-btn.btn-secondary:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.08) !important;
border-color: rgba(255, 255, 255, 0.3) !important;
transform: translateY(-2px) !important;
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
.btn-secondary:active {
transform: translateY(0);
::deep .nexus-btn.btn-secondary:active:not(:disabled) {
transform: translateY(0) !important;
}
/* Verification State */
@@ -357,27 +342,30 @@
to { transform: scale(1.2); opacity: 0.8; }
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
filter: grayscale(1);
::deep .nexus-btn:disabled:not(.btn-loading) {
opacity: 0.4 !important;
cursor: not-allowed !important;
filter: grayscale(1) !important;
}
.btn-loading {
position: relative;
::deep .nexus-btn.btn-loading {
position: relative !important;
color: transparent !important;
opacity: 1 !important;
cursor: wait !important;
filter: none !important;
}
.btn-loading::after {
content: "";
position: absolute;
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.2);
border-top-color: var(--nexus-neon, #00ffaa);
border-radius: 50%;
animation: spin 0.8s linear infinite;
filter: drop-shadow(0 0 4px var(--nexus-neon, #00ffaa));
::deep .nexus-btn.btn-loading::after {
content: "" !important;
position: absolute !important;
width: 20px !important;
height: 20px !important;
border: 2px solid rgba(255, 255, 255, 0.2) !important;
border-top-color: var(--nexus-neon, #00ffaa) !important;
border-radius: 50% !important;
animation: spin 0.8s linear infinite !important;
filter: drop-shadow(0 0 4px var(--nexus-neon, #00ffaa)) !important;
}
/* Indexing State */
@@ -12,21 +12,21 @@
<div class="knowledge-graph-container @(GraphService.IsLoading ? "loading" : "")" id="@ContainerId">
@if (GraphService.IsLoading || GraphService.CurrentGraphData == null)
{
<div class="loading-state">
<div class="preloader-robot">
<NexusIcon Name="robot" Size="64" Class="neon-pulse" />
<div class="scan-line"></div>
<div class="loading-state">
<div class="preloader-robot">
<NexusIcon Name="robot" Size="64" Class="neon-pulse" />
<div class="scan-line"></div>
</div>
<NexusTypography Variant="NexusTypography.TypographyVariant.UI">Mapowanie relacji rozdziału...</NexusTypography>
</div>
<NexusTypography Variant="NexusTypography.TypographyVariant.UI">Mapowanie relacji rozdziału...</NexusTypography>
</div>
}
else
{
<div class="graph-controls">
<button class="zoom-btn" @onclick="ZoomIn" title="Zoom In">+</button>
<button class="zoom-btn" @onclick="ZoomOut" title="Zoom Out"></button>
<button class="zoom-btn reset" @onclick="ZoomReset" title="Reset">⟲</button>
</div>
<div class="graph-controls">
<button class="zoom-btn" @onclick="ZoomIn" title="Zoom In">+</button>
<button class="zoom-btn" @onclick="ZoomOut" title="Zoom Out"></button>
<button class="zoom-btn reset" @onclick="ZoomReset" title="Reset">⟲</button>
</div>
}
</div>
@@ -49,7 +49,7 @@
private async Task HandleGraphUpdate()
{
if (_module == null) return;
if (GraphService.CurrentGraphData == null)
{
await _module.InvokeVoidAsync("clear");
@@ -58,7 +58,7 @@
{
await _module.InvokeVoidAsync("updateData", GraphService.CurrentGraphData);
}
await InvokeAsync(StateHasChanged);
}
@@ -78,7 +78,7 @@
if (firstRender)
{
await InitializeGraphAsync();
if (GraphService.CurrentGraphData != null)
{
await HandleGraphUpdate();
@@ -101,7 +101,7 @@
public async Task OnNodeClicked(string nodeId)
{
await InteractionService.NotifyNodeSelected(nodeId);
if (OnNodeSelected.HasDelegate)
{
await OnNodeSelected.InvokeAsync(nodeId);
@@ -128,7 +128,7 @@
GraphService.OnGraphUpdated -= HandleGraphUpdate;
GraphService.OnActiveNodeChanged -= HandleActiveNodeChange;
GraphService.OnLoadingChanged -= HandleLoadingChange;
try
{
if (_module is not null)
@@ -138,7 +138,7 @@
}
}
catch { }
_dotNetHelper?.Dispose();
}
}
@@ -10,7 +10,7 @@
position: relative;
}
.knowledge-graph-container.loading > ::deep svg {
.knowledge-graph-container.loading> ::deep svg {
display: none !important;
}
@@ -93,9 +93,20 @@
}
@keyframes robot-pulse {
0% { transform: scale(1); filter: drop-shadow(0 0 10px var(--nexus-neon)); }
50% { transform: scale(1.1); filter: drop-shadow(0 0 25px var(--nexus-neon)); }
100% { transform: scale(1); filter: drop-shadow(0 0 10px var(--nexus-neon)); }
0% {
transform: scale(1);
filter: drop-shadow(0 0 10px var(--nexus-neon));
}
50% {
transform: scale(1.1);
filter: drop-shadow(0 0 25px var(--nexus-neon));
}
100% {
transform: scale(1);
filter: drop-shadow(0 0 10px var(--nexus-neon));
}
}
.scan-line {
@@ -111,9 +122,17 @@
}
@keyframes scan {
0% { top: 0; }
50% { top: 100%; }
100% { top: 0; }
0% {
top: 0;
}
50% {
top: 100%;
}
100% {
top: 0;
}
}
::deep .nexus-node-active {
@@ -124,11 +143,24 @@
}
::deep @keyframes neon-flash {
0% { filter: brightness(1) drop-shadow(0 0 0px var(--nexus-neon)); }
50% { filter: brightness(3) drop-shadow(0 0 30px var(--nexus-neon)); }
100% { filter: brightness(1) drop-shadow(0 0 0px var(--nexus-neon)); }
0% {
filter: brightness(1) drop-shadow(0 0 0px var(--nexus-neon));
}
50% {
filter: brightness(3) drop-shadow(0 0 30px var(--nexus-neon));
}
100% {
filter: brightness(1) drop-shadow(0 0 0px var(--nexus-neon));
}
}
::deep .neon-flash-node {
animation: neon-flash 0.8s ease-out;
}
.knowledge-graph-container ::deep svg {
background: radial-gradient(circle, #1a1a1a 0%, #121212 100%);
transition: background 0.3s ease;
}
@@ -99,6 +99,7 @@
private bool _isMobile = false;
private DotNetObjectReference<ReaderCanvas>? _selfReference;
private IJSObjectReference? _viewportModule;
private IJSObjectReference? _selectionModule;
protected override async Task OnInitializedAsync()
{
@@ -201,10 +202,13 @@
{
try
{
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js");
if (_selectionModule == null)
{
_selectionModule = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js");
}
if (_selfReference != null)
{
await module.InvokeVoidAsync("initSelectionListener", _selfReference, _containerRef);
await _selectionModule.InvokeVoidAsync("initSelectionListener", _selfReference, _containerRef);
}
}
catch (Exception ex)
@@ -440,6 +444,19 @@
InteractionService.OnTextSelected -= HandleTextSelected;
SyncService.OnProgressReceived -= HandleSyncProgressReceived;
try
{
if (_selectionModule != null)
{
await _selectionModule.InvokeVoidAsync("destroySelectionListener");
await _selectionModule.DisposeAsync();
}
}
catch (Exception ex)
{
Logger.LogDebug(ex, "Failed to destroy JS selection listener.");
}
try
{
if (_viewportModule != null)
@@ -6,7 +6,7 @@
padding: 2rem 0;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
/* Dedicated Scrollbar Styling */
scrollbar-width: thin;
scrollbar-color: rgba(0, 255, 153, 0.2) transparent;
@@ -30,18 +30,40 @@
background-color: rgba(0, 255, 153, 0.5);
}
.reader-canvas.theme-dark {
background-color: #121214;
}
.reader-canvas.theme-light {
background-color: #F9F9F9; /* Paper-white requirement */
background-color: #f4f1ea;
/* Warm light beige/gray background */
}
.reader-flow-container {
max-width: 800px;
margin: 0 auto;
max-width: 680px;
margin: 2rem auto;
min-height: calc(100vh - 180px);
display: flex;
flex-direction: column;
gap: 1.5rem;
position: relative;
padding: 0 1.5rem 15rem 1.5rem; /* Large padding-bottom for reachability */
padding: 3rem 4rem 15rem 4rem;
/* Large padding-bottom for reachability, plus comfortable side margins */
border-radius: 12px;
box-sizing: border-box;
transition: background-color 0.3s, box-shadow 0.3s, border-color 0.3s;
}
.theme-dark .reader-flow-container {
background-color: #1a1a1e;
border: 1px solid rgba(255, 255, 255, 0.03);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2);
}
.theme-light .reader-flow-container {
background-color: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.04);
box-shadow: 0 4px 20px rgba(139, 130, 115, 0.12);
}
.block-wrapper {
@@ -57,24 +79,49 @@
line-height: 1.65 !important;
letter-spacing: -0.01em !important;
font-size: 1.15rem;
font-weight: 300;
font-weight: 400;
text-align: left !important;
color: #e4e4e7;
/* Off-white with light gray tint */
}
.theme-light ::deep .nexus-ebook {
color: #1a1a1a;
color: #292524;
/* Warm charcoal for legibility */
}
/* Callout Box styling for legacy blockquote segments */
::deep .nexus-ebook blockquote {
background-color: rgba(255, 255, 255, 0.02);
border-left: 4px solid var(--nexus-neon);
padding: 1rem 1.25rem;
margin: 1.5rem 0 1.5rem 0;
border-radius: 0 8px 8px 0;
font-size: 1.05rem;
color: #e2e8f0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.theme-light ::deep .nexus-ebook blockquote {
background-color: rgba(245, 158, 11, 0.04);
border-left: 4px solid #f59e0b;
color: #44403c;
}
/* Technical Code Block Container */
::deep .nexus-ebook pre {
background-color: #2d2d2d; /* Dark theme for code for better contrast */
background-color: #2d2d2d;
/* Dark theme for code for better contrast */
color: #e0e0e0;
padding: 1.25rem;
border-radius: 8px;
margin: 2rem 0;
overflow-x: auto;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
border-left: 4px solid var(--nexus-neon); /* Nexus neon accent */
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
border-left: 4px solid var(--nexus-neon);
/* Nexus neon accent */
/* Dedicated Scrollbar for Code */
scrollbar-width: thin;
scrollbar-color: rgba(0, 255, 153, 0.3) transparent;
@@ -101,7 +148,8 @@
/* Inline Code Highlight */
::deep .nexus-ebook p code {
background-color: rgba(0, 0, 0, 0.05);
color: #d63384; /* Classic differentiator for inline code */
color: #d63384;
/* Classic differentiator for inline code */
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-size: 0.9em;
@@ -153,9 +201,20 @@
}
@keyframes pulse-small {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.1); opacity: 0.8; }
100% { transform: scale(1); opacity: 1; }
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
100% {
transform: scale(1);
opacity: 1;
}
}
/* Chapter Loading Overlay and Spinners */
@@ -246,29 +305,48 @@
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes scaleIn {
from { transform: scale(0.9) translateY(10px); opacity: 0; }
to { transform: scale(1) translateY(0); opacity: 1; }
from {
transform: scale(0.9) translateY(10px);
opacity: 0;
}
to {
transform: scale(1) translateY(0);
opacity: 1;
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* MOBILE READER UI OVERRIDES */
@media (max-width: 768px) {
.reader-canvas {
padding-top: 54px !important;
padding-bottom: 80px !important; /* Ensure content is clear of bottom toolbar */
padding-bottom: 80px !important;
/* Ensure content is clear of bottom toolbar */
}
.reader-flow-container {
padding-bottom: 4rem; /* Safe breathing room */
padding-bottom: 4rem;
/* Safe breathing room */
}
}
@@ -338,7 +416,20 @@
}
.theme-light .nexus-mobile-chapter-title {
color: #1a1a1a;
color: #292524;
}
.theme-light .nexus-mobile-escape-btn {
color: #78716c;
}
.theme-light .nexus-mobile-escape-btn:hover {
color: #10b981;
background-color: rgba(16, 185, 129, 0.05);
}
.theme-light .nexus-mobile-escape-btn:active {
background-color: rgba(16, 185, 129, 0.08);
}
.nexus-chapter-nav-btn {
@@ -372,4 +463,4 @@
.theme-light .nexus-chapter-nav-btn:hover:not(:disabled) {
background: rgba(0, 0, 0, 0.06);
}
}
@@ -1,64 +1,99 @@
.reader-footer {
position: relative;
height: 50px;
background: #F9F9F9;
border-top: 1px solid rgba(0, 0, 0, 0.08);
position: absolute;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
width: min(600px, 90%);
height: 54px;
background: rgba(24, 24, 27, 0.6);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 9999px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
padding: 0 1.5rem;
z-index: 10;
flex-shrink: 0;
z-index: 100;
transition: background 0.3s, border-color 0.3s, box-shadow 0.3s;
}
.theme-light .reader-footer {
background: rgba(254, 254, 254, 0.75);
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.04), 0 1px 3px rgba(0, 0, 0, 0.02);
}
.footer-content {
display: flex;
align-items: center;
width: 100%;
gap: 1.5rem;
gap: 1rem;
justify-content: space-between;
}
.navigation-controls {
display: grid;
grid-template-columns: 32px 1fr 32px;
display: flex;
align-items: center;
gap: 0.75rem;
width: 260px;
flex-shrink: 0;
flex: 1;
min-width: 0;
}
.nav-btn {
background: white;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 6px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 50%;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
color: #333;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out, border-color 0.2s ease-in-out, transform 0.2s ease-in-out;
color: #a1a1aa; /* Zinc-400 default contrast */
}
.nav-btn:hover:not(:disabled) {
background: #f0f0f0;
transform: translateY(-1px);
.nav-btn:hover:not(:disabled),
.nav-btn:focus:not(:disabled) {
background: rgba(255, 255, 255, 0.08);
border-color: var(--nexus-neon, #00ff99);
color: var(--nexus-neon, #00ff99); /* Brand neon green hover/focus signal */
transform: scale(1.05);
outline: none;
}
.nav-btn:disabled {
opacity: 0.3;
opacity: 0.25;
cursor: not-allowed;
}
.theme-light .nav-btn {
background: rgba(0, 0, 0, 0.02);
border-color: rgba(0, 0, 0, 0.08);
color: #78716c; /* Warm stone-500 */
}
.theme-light .nav-btn:hover:not(:disabled),
.theme-light .nav-btn:focus:not(:disabled) {
background: rgba(16, 185, 129, 0.05);
border-color: #10b981;
color: #10b981;
outline: none;
}
.chapter-info {
display: flex;
flex-direction: column;
align-items: center;
align-items: flex-start;
justify-content: center;
min-width: 0;
overflow: hidden;
color: #333;
flex: 1;
color: #e2e8f0; /* Slate-200 for clean high readability */
}
.theme-light .chapter-info {
color: #292524; /* Warm charcoal for legibility */
}
.chapter-title {
@@ -68,42 +103,66 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
line-height: 1.2;
}
.chapter-count {
opacity: 0.5;
font-size: 0.75rem;
color: #a1a1aa; /* Zinc-400 for secondary info clarity */
font-size: 0.7rem;
}
.theme-light .chapter-count {
color: #78716c; /* Warm stone-500 secondary info */
}
.progress-container {
flex: 1;
height: 6px;
background: rgba(0, 0, 0, 0.05);
border-radius: 3px;
width: 80px;
height: 4px;
background: rgba(255, 255, 255, 0.08);
border-radius: 2px;
overflow: hidden;
margin: 0 1rem;
margin: 0 0.25rem;
flex-shrink: 0;
}
.theme-light .progress-container {
background: rgba(0, 0, 0, 0.08);
}
.progress-bar {
height: 100%;
background: #2ECC71;
border-radius: 3px;
background: var(--nexus-neon, #00ff99);
border-radius: 2px;
transition: width 0.3s ease;
}
.theme-light .progress-bar {
background: #10b981;
}
.meta-info {
display: flex;
align-items: center;
gap: 1rem;
font-size: 0.75rem;
color: #888;
gap: 0.75rem;
font-size: 0.7rem;
color: #a1a1aa;
flex-shrink: 0;
font-family: monospace;
}
.theme-light .meta-info {
color: #78716c;
}
.battery {
display: flex;
align-items: center;
gap: 0.3rem;
}
/* RWD constraint: floating toolbar visible ONLY on desktop (min-width: 1024px) */
@media (max-width: 1023px) {
.reader-footer {
display: none !important;
}
}