feat(creator): overhaul Creator flow, editor duplication, and staging setup #83

Merged
mjasin merged 15 commits from feature/stage3-book-versioning into develop 2026-06-15 17:15:43 +00:00
5 changed files with 217 additions and 557 deletions
Showing only changes of commit 00ebee8628 - Show all commits
@@ -1,218 +0,0 @@
@page "/creator/edit/{BookId:guid}"
@attribute [Authorize]
@using System.Net.Http.Json
@using Microsoft.Extensions.Logging
@using NexusReader.Application.DTOs.Creator
@inject HttpClient Http
@inject NavigationManager NavigationManager
@inject ILogger<Creator> Logger
<PageTitle>Workspace Autora | Nexus Reader</PageTitle>
<div class="workspace-container">
<!-- Left Sidebar for Chapter Selection -->
<aside class="workspace-sidebar glass-panel">
<div class="sidebar-header">
<button type="button" class="back-dashboard-btn" @onclick="NavigateToDashboard">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
<span>Dashboard</span>
</button>
<h3 class="sidebar-title">Rozdziały</h3>
</div>
<nav class="chapters-nav">
@if (_chaptersLoading)
{
<div class="sidebar-loading">
<div class="spinner-glow small"></div>
<span>Ładowanie spisu...</span>
</div>
}
else if (_chapters == null || !_chapters.Any())
{
<div class="sidebar-empty">Brak rozdziałów w tej wersji.</div>
}
else
{
<ul class="chapters-list">
@foreach (var ch in _chapters)
{
<li class="chapter-item @(ch.Id == _activeChapterId ? "active" : "")" @onclick="() => LoadChapterContentAsync(ch.Id)">
<span class="chapter-order">@ch.SortOrder.</span>
<span class="chapter-name" title="@ch.Title">@ch.Title</span>
</li>
}
</ul>
}
</nav>
</aside>
<!-- Right Workspace Area -->
<main class="workspace-content">
@if (_contentLoading)
{
<div class="editor-loading-placeholder glass-panel">
<div class="spinner-glow"></div>
<h3 class="loading-title">Wczytywanie treści rozdziału...</h3>
<p>Przygotowywanie edytora Zen Mode i sprawdzanie kopii zapasowych w LocalStorage...</p>
</div>
}
else if (_activeChapterId == Guid.Empty)
{
<div class="workspace-empty glass-panel">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 1 1 3 3L12 15l-4 1 1-4z"></path>
</svg>
<h3>Wybierz rozdział z listy</h3>
<p>Kliknij na dowolny tytuł w panelu bocznym, aby rozpocząć pisanie lub edycję.</p>
</div>
}
else
{
<div class="editor-workspace-card glass-panel" spellcheck="false">
<div class="editor-header-meta">
<h2 class="active-chapter-title">@_activeChapterTitle</h2>
<span class="chapter-id-badge">ID: @_activeChapterId.ToString().Substring(0, 8)...</span>
</div>
<div class="editor-growing-area">
<MarkdownEditor @ref="_editorRef"
InitialMarkdown="@_chapterMarkdown"
ChapterId="@_activeChapterId"
ServerTimestamp="@_serverTimestamp"
OnSave="HandleSave"
ShowFetchButton="true"
Height="100%" />
</div>
</div>
}
</main>
</div>
@code {
[Parameter]
public Guid? BookId { get; set; }
private MarkdownEditor? _editorRef;
private bool _chaptersLoading = true;
private bool _contentLoading = false;
private List<ChapterItemDto> _chapters = new();
private Guid _activeChapterId = Guid.Empty;
private string _activeChapterTitle = string.Empty;
private string _chapterMarkdown = string.Empty;
private DateTime _serverTimestamp = DateTime.UtcNow;
public class ChapterItemDto
{
public Guid Id { get; set; }
public string Title { get; set; } = string.Empty;
public int SortOrder { get; set; }
}
public class ChapterDetailsDto
{
public Guid Id { get; set; }
public string Title { get; set; } = string.Empty;
public string MarkdownContent { get; set; } = string.Empty;
}
protected override async Task OnParametersSetAsync()
{
await base.OnParametersSetAsync();
if (BookId.HasValue && BookId.Value != Guid.Empty)
{
await LoadBookChaptersAsync();
}
else
{
_chaptersLoading = false;
_chapters.Clear();
_activeChapterId = Guid.Empty;
_chapterMarkdown = string.Empty;
}
}
private async Task LoadBookChaptersAsync()
{
_chaptersLoading = true;
StateHasChanged();
try
{
_chapters = await Http.GetFromJsonAsync<List<ChapterItemDto>>($"api/creator/books/{BookId}/chapters") ?? new();
// Extract the query parameter chapterId if available
var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
Guid targetChapterId = Guid.Empty;
if (Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query).TryGetValue("chapterId", out var chapterValue))
{
Guid.TryParse(chapterValue, out targetChapterId);
}
if (targetChapterId != Guid.Empty && _chapters.Any(c => c.Id == targetChapterId))
{
await LoadChapterContentAsync(targetChapterId);
}
else if (_chapters.Any())
{
await LoadChapterContentAsync(_chapters.First().Id);
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to load book chapters.");
}
finally
{
_chaptersLoading = false;
StateHasChanged();
}
}
private async Task LoadChapterContentAsync(Guid chapterId)
{
if (chapterId == Guid.Empty) return;
_contentLoading = true;
_activeChapterId = chapterId;
_activeChapterTitle = _chapters.FirstOrDefault(c => c.Id == chapterId)?.Title ?? "Rozdział";
StateHasChanged();
try
{
var details = await Http.GetFromJsonAsync<ChapterDetailsDto>($"api/chapters/{chapterId}");
if (details != null)
{
_chapterMarkdown = details.MarkdownContent;
_serverTimestamp = DateTime.UtcNow; // Used to check database sync freshness
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to load chapter content.");
_chapterMarkdown = string.Empty;
}
finally
{
_contentLoading = false;
StateHasChanged();
}
}
private void HandleSave(string markdown)
{
_chapterMarkdown = markdown;
Logger.LogInformation("Saved markdown content length: {Length}", markdown.Length);
}
private void NavigateToDashboard()
{
NavigationManager.NavigateTo("/creator");
}
}
@@ -1,275 +0,0 @@
.workspace-container {
display: flex;
min-height: calc(100vh - 64px); /* assuming top navbar is 64px */
width: 100%;
background: var(--bg-base);
animation: fade-in 0.4s ease-out;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
/* --- Left Sidebar --- */
.workspace-sidebar {
width: 280px;
flex-shrink: 0;
border-right: 1px solid var(--border);
background: var(--bg-surface);
display: flex;
flex-direction: column;
padding: 1.5rem 0;
z-index: 10;
}
.sidebar-header {
padding: 0 1.5rem 1.5rem;
border-bottom: 1px dashed var(--border);
display: flex;
flex-direction: column;
gap: 1rem;
}
.back-dashboard-btn {
background: transparent;
border: none;
color: var(--text-muted);
font-size: 0.85rem;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 0.25rem;
cursor: pointer;
padding: 0.25rem 0;
transition: color 0.2s;
width: fit-content;
}
.back-dashboard-btn:hover {
color: var(--text-main);
}
.sidebar-title {
font-family: var(--nexus-font-serif, serif);
font-size: 1.25rem;
font-weight: 700;
color: var(--text-main);
margin: 0;
}
.chapters-nav {
flex: 1;
overflow-y: auto;
padding: 1rem 0;
}
.sidebar-loading, .sidebar-empty {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 2rem 1.5rem;
color: var(--text-muted);
font-size: 0.85rem;
text-align: center;
}
.chapters-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.chapter-item {
padding: 0.75rem 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
transition: all 0.2s ease;
border-left: 3px solid transparent;
color: var(--text-muted);
}
.chapter-item:hover {
background: rgba(255, 255, 255, 0.02);
color: var(--text-main);
}
.chapter-item.active {
background: rgba(16, 185, 129, 0.03);
border-left-color: var(--accent);
color: var(--text-main);
font-weight: 600;
}
.chapter-order {
font-size: 0.8rem;
opacity: 0.5;
}
.chapter-name {
font-size: 0.9rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* --- Right Content Workspace --- */
.workspace-content {
flex: 1;
padding: 2.5rem;
display: flex;
flex-direction: column;
overflow-y: auto;
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
.workspace-empty, .editor-loading-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 4rem 2rem;
gap: 1.5rem;
height: 100%;
min-height: 400px;
}
.workspace-empty svg {
color: var(--text-muted);
opacity: 0.4;
}
.workspace-empty h3, .loading-title {
font-family: var(--nexus-font-serif, serif);
font-size: 1.5rem;
font-weight: 600;
color: var(--text-main);
margin: 0;
}
.workspace-empty p {
font-size: 0.95rem;
color: var(--text-muted);
max-width: 400px;
line-height: 1.5;
}
.editor-workspace-card {
display: flex;
flex-direction: column;
gap: 1.5rem;
padding: 2rem;
height: 100%;
min-height: 500px;
}
.editor-header-meta {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 1rem;
border-bottom: 1px dashed var(--border);
}
.active-chapter-title {
font-family: var(--nexus-font-serif, serif);
font-size: 1.75rem;
font-weight: 700;
color: var(--text-main);
margin: 0;
}
.chapter-id-badge {
font-size: 0.75rem;
color: var(--text-muted);
padding: 4px 10px;
background: var(--bg-base);
border: 1px solid var(--border);
border-radius: 6px;
text-transform: uppercase;
}
.editor-growing-area {
flex: 1;
display: flex;
flex-direction: column;
}
/* Glassmorphism Panel styles */
.glass-panel {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.03);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.spinner-glow {
width: 36px;
height: 36px;
border: 3px solid rgba(16, 185, 129, 0.1);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin-glow 1s linear infinite;
box-shadow: 0 0 10px rgba(16, 185, 129, 0.2);
}
@keyframes spin-glow {
100% { transform: rotate(360deg); }
}
/* --- Mobile View Adjustments --- */
@media (max-width: 992px) {
.workspace-sidebar {
width: 220px;
}
.workspace-content {
padding: 1.5rem;
}
}
@media (max-width: 768px) {
.workspace-container {
flex-direction: column;
}
.workspace-sidebar {
width: 100%;
border-right: none;
border-bottom: 1px solid var(--border);
padding: 1rem 0;
}
.chapters-list {
flex-direction: row;
overflow-x: auto;
padding: 0 1rem;
}
.chapter-item {
padding: 0.5rem 1rem;
border-left: none;
border-bottom: 3px solid transparent;
white-space: nowrap;
}
.chapter-item.active {
border-bottom-color: var(--accent);
}
.sidebar-header {
padding: 0 1rem 0.5rem;
border-bottom: none;
}
.workspace-content {
padding: 1rem;
}
.active-chapter-title {
font-size: 1.35rem;
}
}
@@ -372,7 +372,7 @@
{
if (book.FirstChapterId.HasValue)
{
NavigationManager.NavigateTo($"/creator/edit/{book.Id}?chapterId={book.FirstChapterId.Value}");
NavigationManager.NavigateTo($"/creator/edit/{book.Id}/{book.FirstChapterId.Value}");
}
else
{
@@ -1,3 +1,4 @@
@page "/creator/edit/{BookId}"
@page "/creator/edit/{BookId}/{ChapterId}"
@layout MainHubLayout
@attribute [Authorize]
@@ -6,29 +7,37 @@
<div class="chapters-sidebar">
<h2>Rozdziały</h2>
<div class="chapter-item active">
<span>1. Rozdział 1: Wprowadzenie d...</span>
</div>
<div class="chapter-item">
<span>2. Rozdział 2: Zabezpieczenia i...</span>
<div class="chapters-list-wrapper">
<div class="chapter-item active">
<i class="fa-solid fa-book-open chapter-icon"></i>
<span>1. Rozdział 1: Wprowadzenie do Zen Mode</span>
</div>
<div class="chapter-item">
<i class="fa-solid fa-lock chapter-icon"></i>
<span>2. Rozdział 2: Zabezpieczenia i Architektura</span>
</div>
</div>
</div>
<div class="editor-workspace-area">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<div class="editor-header-row">
<h1 class="chapter-title">Rozdział 1: Wprowadzenie do Zen Mode</h1>
<span class="chapter-id-badge">ID: @GetSafeChapterIdPrefix()...</span>
<div class="chapter-meta-zone">
<span class="chapter-id-badge">ID: @ChapterId</span>
</div>
</div>
<div class="editor-canvas-card">
<div class="milkdown-premium-container" spellcheck="false">
<div id="editor"></div>
</div>
<div class="editor-footer-bar">
<div class="cloud-status">
<i class="fa-solid fa-circle-check"></i>
<span>Saved to Cloud</span>
<div class="cloud-status-container">
<span class="cloud-status-pulse"></span>
<span class="cloud-status-text">Saved to Cloud</span>
</div>
<button class="btn-nexus-premium" @onclick="FetchContent">
@@ -36,25 +45,19 @@
<i class="fa-solid fa-arrow-right"></i>
</button>
</div>
</div>
</div>
</div>
@code {
[Parameter]
public string BookId { get; set; } = string.Empty;
[Parameter] public string BookId { get; set; } = string.Empty;
[Parameter] public string ChapterId { get; set; } = string.Empty;
private string _retrievedMarkdown = string.Empty;
[Parameter]
public string ChapterId { get; set; } = string.Empty;
private void FetchContent()
private async Task FetchContent()
{
// Actual retrieval logic can go here
}
private string GetSafeChapterIdPrefix()
{
if (string.IsNullOrEmpty(ChapterId)) return "N/A";
return ChapterId.Length >= 8 ? ChapterId.Substring(0, 8) : ChapterId;
// Tutaj trafia Twoja logika wyciągania zawartości z edytora Milkdown
await Task.CompletedTask;
}
}
@@ -1,55 +1,73 @@
/* --- 1. BLOKADA VIEWPORTU (ZERO GLOBAL SCROLL) --- */
/* ==========================================================================
NEXUSREADER CREATOR EDIT MODE - PREMIUM SAAS CORE STYLES
========================================================================== */
/* 1. MASTER WRAPPER (Zgładzenie globalnego scrolla przeglądarki) */
.creator-edit-fullscreen-wrapper {
width: 100% !important;
max-width: 100% !important;
height: calc(100vh - 4rem) !important; /* Idealne odcięcie od topbaru platformy */
margin: 0;
padding: 0 !important; /* Likwidujemy marginesy zewnętrzne dla pełnego ekranu roboczego */
display: flex;
overflow: hidden !important; /* Całkowity zakaz przewijania głównego okna */
height: calc(100vh - 4rem) !important; /* Sztywne cięcie pod wysokość topbaru platformy */
margin: 0 !important;
padding: 0 !important;
display: flex !important;
overflow: hidden !important; /* Całkowity zakaz scrollowania okna głównego */
background-color: var(--bg-base);
box-sizing: border-box;
}
/* --- 2. LUKSUSOWY PANEL BOCZNY ROZDZIAŁÓW --- */
/* 2. MATTE CHAPTERS SIDEBAR (Luksusowy panel boczny) */
.chapters-sidebar {
width: 280px !important;
width: 290px !important;
flex-shrink: 0;
background-color: #16161a !important; /* Matowy, głęboki odcień grafitu */
background-color: #141417 !important; /* Matowy głęboki antracyt z readera */
border-right: 1px solid rgba(255, 255, 255, 0.05) !important;
display: flex;
flex-direction: column;
padding: 2rem 1rem !important;
overflow-y: auto;
padding: 2rem 1.25rem !important;
box-sizing: border-box;
}
.theme-light .chapters-sidebar {
background-color: #ede9df !important; /* Harmonijna, ciemniejsza sepia dla jasnego motywu */
background-color: #ede9df !important; /* Dopasowanie do motywu Warm Paper */
border-right: 1px solid var(--border) !important;
}
.chapters-sidebar h2 {
font-size: 1.25rem;
font-size: 0.85rem;
font-weight: 700;
color: var(--text-main);
margin-bottom: 1.5rem;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--text-muted);
margin: 0 0 1.5rem 0;
padding-left: 0.5rem;
}
.chapters-list-wrapper {
display: flex;
flex-direction: column;
gap: 4px;
}
/* Elementy listy rozdziałów */
.chapter-item {
display: flex;
align-items: center;
padding: 10px 14px !important;
margin-bottom: 4px;
gap: 12px;
padding: 12px 14px !important;
border-radius: 8px;
color: var(--text-muted);
font-size: 0.95rem;
cursor: pointer;
transition: all 0.2s ease;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
border-left: 3px solid transparent !important;
}
/* Stan aktywny wybranego rozdziału (Zunifikowany z systemem) */
.chapter-item i.chapter-icon {
font-size: 0.9rem;
color: var(--text-muted);
}
/* Aktywny stan unifikacji wizualnej */
.chapter-item.active {
background-color: rgba(0, 255, 153, 0.04) !important;
color: var(--accent) !important;
@@ -57,6 +75,10 @@
border-left: 3px solid var(--accent) !important;
}
.chapter-item.active i.chapter-icon {
color: var(--accent) !important;
}
.theme-light .chapter-item.active {
background-color: rgba(16, 185, 129, 0.06) !important;
}
@@ -66,38 +88,52 @@
color: var(--text-main);
}
/* --- 3. OBSZAR WORKSPACE (KREATOR CENTRUM) --- */
/* 3. WORKSPACE CORE (Główna oś edytora) */
.editor-workspace-area {
flex-grow: 1;
display: flex;
flex-direction: column;
height: 100%;
padding: 2.5rem 3.5rem 2rem 3.5rem !important; /* Odpowiedni, dostojny margines Zen Mode */
box-sizing: border-box;
overflow: hidden;
padding: 2.5rem 3.5rem 1.5rem 3.5rem !important; /* Większy oddech boczny dla trybu Zen */
}
/* Tytuł rozdziału u góry */
/* Nagłówek i ID bez ryzyka kolizji */
.editor-header-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
flex-shrink: 0;
width: 100%;
}
.editor-workspace-area h1.chapter-title {
font-size: 2.2rem;
font-weight: 700;
color: var(--text-main);
margin: 0 0 1.5rem 0;
margin: 0;
letter-spacing: -0.5px;
}
/* Dyskretny indykator ID rozdziału */
.chapter-id-badge {
font-family: 'Azeret Mono', monospace;
font-size: 0.75rem;
color: var(--text-muted);
background: rgba(255, 255, 255, 0.02);
padding: 2px 8px;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.05);
opacity: 0.6;
background: rgba(255, 255, 255, 0.03);
padding: 5px 12px;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.08);
white-space: nowrap;
}
/* --- 4. KARTA ROBOCZA EDYTORA I SCROLLBAR --- */
.theme-light .chapter-id-badge {
background: rgba(0, 0, 0, 0.02);
border: 1px solid var(--border);
}
/* 4. PREMIUM CANVAS CARD (Rozciąganie Flex na 100% wysokości) */
.editor-canvas-card {
background-color: var(--bg-surface) !important;
border: 1px solid var(--border) !important;
@@ -105,37 +141,151 @@
padding: 2.5rem !important;
display: flex;
flex-direction: column;
flex-grow: 1; /* Wymusza rozciągnięcie do samego paska stopki */
overflow: hidden;
box-sizing: border-box;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
}
.milkdown-premium-container {
flex-grow: 1;
overflow: hidden; /* Kontrola przewijania wyłącznie wewnątrz tekstu */
display: flex;
flex-direction: column;
overflow: hidden;
width: 100%;
}
/* --- DEEP SELECTORS DLA PROSEMIRROR (MILKDOWN RENDER) --- */
::deep .milkdown {
background: transparent !important;
box-shadow: none !important;
border: none !important;
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
width: 100%;
}
::deep .ProseMirror {
color: var(--text-main) !important;
background-color: transparent !important;
font-family: inherit !important;
font-size: 1.1rem !important;
line-height: 1.75 !important;
flex-grow: 1;
overflow-y: auto !important; /* Przewija się tylko tekst książki! */
padding-right: 20px !important;
overflow-y: auto !important; /* Scroll pojawia się TYLKO na tekście rozdziału */
padding-right: 15px !important;
outline: none !important;
box-sizing: border-box;
width: 100%;
}
/* --- 5. ZINTEGROWANY PASEK STANU I AKCJI (FOOTER) --- */
/* Kontekst zaznaczenia zunifikowany z readerem */
::deep .ProseMirror ::selection {
background-color: rgba(0, 255, 153, 0.25) !important;
color: inherit !important;
}
.theme-light ::deep .ProseMirror ::selection {
background-color: rgba(16, 185, 129, 0.2) !important;
}
/* Popover / dymek formatowania zintegrowany z czytnikiem */
::deep .milkdown .popover,
::deep .milkdown-popover,
::deep .prosemirror-bubble-menu {
background-color: #1e1e22 !important;
border: 1px solid rgba(255, 255, 255, 0.08) !important;
border-radius: 12px !important;
padding: 6px 10px !important;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5) !important;
display: flex !important;
align-items: center !important;
gap: 12px !important;
}
.theme-light ::deep .milkdown .popover,
.theme-light ::deep .prosemirror-bubble-menu {
background-color: #ffffff !important;
border: 1px solid var(--border) !important;
box-shadow: 0 10px 30px rgba(45, 42, 38, 0.06) !important;
}
/* Customowy, dyskretny scrollbar dla tekstu książki */
::deep .ProseMirror::-webkit-scrollbar {
width: 6px;
}
::deep .ProseMirror::-webkit-scrollbar-track {
background: transparent;
}
::deep .ProseMirror::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px;
}
/* 5. FIXED FOOTER BAR (Zintegrowany dół pancernej karty) */
.editor-footer-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1.5rem;
padding-top: 1rem;
padding-top: 1.25rem;
border-top: 1px solid var(--border);
flex-shrink: 0;
flex-shrink: 0; /* Gwarancja, że pasek nigdy nie ucieknie poza kartę */
width: 100%;
}
.cloud-status {
/* Status zapisu w chmurze z pulsującą diodą akcentową */
.cloud-status-container {
display: flex;
align-items: center;
gap: 8px;
gap: 10px;
}
.cloud-status-pulse {
width: 8px;
height: 8px;
background-color: var(--accent);
border-radius: 50%;
display: inline-block;
box-shadow: 0 0 10px var(--accent);
position: relative;
}
.cloud-status-text {
font-family: 'Azeret Mono', monospace;
font-size: 0.85rem;
color: var(--text-muted);
}
.cloud-status i {
color: var(--accent); /* Zielona dioda zapisu statusu */
/* Przycisk akcji premium */
.btn-nexus-premium {
background-color: var(--accent) !important;
color: #121214 !important;
font-weight: 700;
font-size: 0.95rem;
padding: 10px 22px;
border: none !important;
border-radius: 8px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 10px;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 14px var(--accent-glow);
}
.theme-light .btn-nexus-premium {
color: #ffffff !important;
}
.btn-nexus-premium:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px var(--accent-glow);
}
.btn-nexus-premium:active {
transform: translateY(0);
}