v3.0.1: qualité & stabilité — tests, sécurité, UX consolidée
🧪 Tests: 73 → 130 (+57 tests) - Upload API, labels sync, commit history (401/400/502/cached) - File create/update (sha=null, sha=abc) - Admin delete cascade (12 tests: oauth, tags, workspaces, pages…) - Gitea status (linked/unlinked/disconnect) - OAuth link mode (session, redirect, callback) 🔒 Sécurité - ContentSecurityPolicyMiddleware (CSP headers) - RateLimitMiddleware (100 req/min/IP) - Pydantic models: ErrorResponse, SuccessResponse - Input validation upload (10MB, allowed extensions) - Pydantic request models 🎨 UX - static/css/design-tokens.css (500 lines, thèmes + skeletons) - Toast system: 16 alert() remplacés par toast() - 59 lignes CSS dupliquées retirées (6 templates) - Skeletons cohérents sur toutes les vues ⚡ Performance - Cache TTL sur get_file_commits (consistant avec les autres méthodes)
This commit is contained in:
+47
-21
@@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>FlowDeck</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||
<link rel="stylesheet" href="/static/css/design-tokens.css?v=1.0">
|
||||
<link rel="stylesheet" href="/static/css/app.css?v=2.4.6">
|
||||
<script src="https://unpkg.com/[email protected]"></script>
|
||||
<script src="https://unpkg.com/[email protected]" defer></script>
|
||||
@@ -333,11 +334,6 @@
|
||||
<!-- Uncollapse button → inside topbar, no overlap -->
|
||||
<!-- (Moved into topbar block below) -->
|
||||
|
||||
|
||||
<style>
|
||||
[x-cloak] { display: none !important; }
|
||||
</style>
|
||||
|
||||
<!-- ═══════════ MAIN ═══════════ -->
|
||||
<div class="main-wrapper">
|
||||
|
||||
@@ -497,6 +493,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ TOAST NOTIFICATIONS ============ -->
|
||||
<div class="toast-container">
|
||||
<template x-for="t in toasts" :key="t.id">
|
||||
<div class="toast" :class="'toast-' + t.type"
|
||||
x-show="true"
|
||||
x-transition:enter=""
|
||||
x-transition:leave=""
|
||||
x-text="t.msg"></div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -517,6 +524,17 @@
|
||||
sidebarCollapsed: false,
|
||||
mobileSidebarOpen: false,
|
||||
showInvite: true,
|
||||
// -- Toast notifications --
|
||||
toasts: [],
|
||||
toast(msg, type) {
|
||||
type = type || 'info';
|
||||
var id = Date.now() + Math.random();
|
||||
this.toasts.push({ id: id, msg: msg, type: type });
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
self.toasts = self.toasts.filter(function(t) { return t.id !== id; });
|
||||
}, 3000);
|
||||
},
|
||||
wsHeaderHover: false,
|
||||
workspaceName: '{{ workspace_name }}',
|
||||
workspaceInitial: '{{ workspace_initial }}',
|
||||
@@ -620,7 +638,7 @@
|
||||
this.sidebarCollapsed = true;
|
||||
window.location.href = url;
|
||||
},
|
||||
openQuickFind() { alert('Quick Find — Ctrl+K'); },
|
||||
openQuickFind() { this.toast('Quick Find — Ctrl+K', 'info'); },
|
||||
|
||||
// ── Tree state ──
|
||||
expandedNodes: {},
|
||||
@@ -725,11 +743,11 @@
|
||||
// Reload to update sidebar
|
||||
location.reload();
|
||||
})
|
||||
.catch(e => alert('Favorite action failed'));
|
||||
.catch(e => this.toast('Favorite action failed', 'error'));
|
||||
break;
|
||||
case 'copyLink':
|
||||
const url = window.location.origin + '/' + pageId;
|
||||
navigator.clipboard.writeText(url).then(() => alert('Link copied!'));
|
||||
navigator.clipboard.writeText(url).then(() => this.toast('Link copied!', 'success'));
|
||||
break;
|
||||
case 'duplicate':
|
||||
fetch(`/board/api/pages?title=${encodeURIComponent(pageName + ' copy')}§ion=Private&project=${encodeURIComponent(this.workspaceKey)}`, {
|
||||
@@ -737,7 +755,7 @@
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => { window.location.href = `/pages/${data.id}`; })
|
||||
.catch(e => alert('Duplicate failed'));
|
||||
.catch(e => this.toast('Duplicate failed', 'error'));
|
||||
break;
|
||||
case 'rename':
|
||||
const newName = prompt('New name:', pageName);
|
||||
@@ -750,7 +768,7 @@
|
||||
case 'moveTo':
|
||||
const section = prompt('Move to section (Private, Recents, Favorites):', 'Private');
|
||||
if (!section) break;
|
||||
alert(`Moved to ${section}`);
|
||||
this.toast('Moved to ' + section, 'success');
|
||||
break;
|
||||
case 'delete':
|
||||
if (!confirm(`Move "${pageName}" to trash?`)) break;
|
||||
@@ -775,13 +793,13 @@
|
||||
window.location.href = wk && wk.includes('/') ? `/board/${wk}` : '/';
|
||||
}
|
||||
})
|
||||
.catch(e => alert('Delete failed: ' + e.message));
|
||||
.catch(e => this.toast('Delete failed: ' + e.message, 'error'));
|
||||
break;
|
||||
case 'openTab':
|
||||
window.open('/' + pageId, '_blank');
|
||||
break;
|
||||
case 'openPeek':
|
||||
alert('Side peek not available in browser');
|
||||
this.toast('Side peek not available in browser', 'info');
|
||||
break;
|
||||
}
|
||||
this.contextMenu.visible = false;
|
||||
@@ -806,7 +824,7 @@
|
||||
})
|
||||
.then(r => { if (!r.ok) throw new Error('CSRF or API error'); return r.json(); })
|
||||
.then(data => { window.location.href = `/pages/${data.id}`; })
|
||||
.catch(err => { alert('Failed to create page: ' + err.message); });
|
||||
.catch(err => { this.toast('Failed to create page: ' + err.message, 'error'); });
|
||||
},
|
||||
newSubPage(parentId) {
|
||||
const project = this.workspaceKey || '';
|
||||
@@ -817,7 +835,7 @@
|
||||
})
|
||||
.then(r => { if (!r.ok) throw new Error('CSRF or API error'); return r.json(); })
|
||||
.then(data => { window.location.href = `/pages/${data.id}`; })
|
||||
.catch(err => { alert('Failed to create sub-page: ' + err.message); });
|
||||
.catch(err => { this.toast('Failed to create sub-page: ' + err.message, 'error'); });
|
||||
},
|
||||
newPageInWorkspace() {
|
||||
const name = prompt('File name:');
|
||||
@@ -829,7 +847,7 @@
|
||||
})
|
||||
.then(r => { if (!r.ok) throw new Error('Failed'); return r.json(); })
|
||||
.then(data => { window.location.href = `/pages/${data.id}`; })
|
||||
.catch(err => { alert('Failed: ' + err.message); });
|
||||
.catch(err => { this.toast('Failed: ' + err.message, 'error'); });
|
||||
},
|
||||
newFolderInWorkspace() {
|
||||
const name = prompt('Folder name:');
|
||||
@@ -841,7 +859,7 @@
|
||||
})
|
||||
.then(r => { if (!r.ok) throw new Error('Failed'); return r.json(); })
|
||||
.then(() => { window.location.reload(); })
|
||||
.catch(err => { alert('Failed: ' + err.message); });
|
||||
.catch(err => { this.toast('Failed: ' + err.message, 'error'); });
|
||||
},
|
||||
deleteWorkspacePage(pageId) {
|
||||
if (!confirm('Delete this page?')) return;
|
||||
@@ -851,7 +869,7 @@
|
||||
})
|
||||
.then(r => { if (!r.ok) throw new Error('Failed'); return r.json(); })
|
||||
.then(() => { window.location.reload(); })
|
||||
.catch(err => { alert('Failed: ' + err.message); });
|
||||
.catch(err => { this.toast('Failed: ' + err.message, 'error'); });
|
||||
},
|
||||
|
||||
// ── Drag & Drop ──
|
||||
@@ -880,7 +898,7 @@
|
||||
body: JSON.stringify({ parent_id: parentId })
|
||||
})
|
||||
.then(r => { if (!r.ok) throw new Error('Failed'); window.location.reload(); })
|
||||
.catch(err => { alert('Move failed: ' + err.message); });
|
||||
.catch(err => { this.toast('Move failed: ' + err.message, 'error'); });
|
||||
},
|
||||
|
||||
// ── Sub-item creation in folders ──
|
||||
@@ -894,7 +912,7 @@
|
||||
})
|
||||
.then(r => { if (!r.ok) throw new Error('Failed'); return r.json(); })
|
||||
.then(data => { window.location.href = `/pages/${data.id}`; })
|
||||
.catch(err => { alert('Failed: ' + err.message); });
|
||||
.catch(err => { this.toast('Failed: ' + err.message, 'error'); });
|
||||
},
|
||||
newFolderInFolder(parentId) {
|
||||
const name = prompt('Sub-folder name:');
|
||||
@@ -905,7 +923,7 @@
|
||||
body: JSON.stringify({ name: name.trim(), type: 'folder', parent_id: parentId })
|
||||
})
|
||||
.then(r => { if (!r.ok) throw new Error('Failed'); window.location.reload(); })
|
||||
.catch(err => { alert('Failed: ' + err.message); });
|
||||
.catch(err => { this.toast('Failed: ' + err.message, 'error'); });
|
||||
},
|
||||
openWorkspacePage(pageId) {
|
||||
window.location = `/pages/${pageId}`;
|
||||
@@ -937,7 +955,7 @@
|
||||
},
|
||||
createDatabase() {
|
||||
this.showNewPageMenu = false;
|
||||
alert('Database creation: use a Gitea repo as a database. Navigate to a project board from the dashboard.');
|
||||
this.toast('Database creation: use a Gitea repo as a database. Navigate to a project board from the dashboard.', 'info');
|
||||
},
|
||||
createBoard() {
|
||||
this.showNewPageMenu = false;
|
||||
@@ -1030,6 +1048,14 @@
|
||||
if (e.persisted) applySavedTheme();
|
||||
});
|
||||
|
||||
// -- Global toast helper (accessible from any component) --
|
||||
window.showToast = function(msg, type) {
|
||||
var el = document.querySelector('[x-data]');
|
||||
if (el && el.__x && el.__x.$data && el.__x.$data.toast) {
|
||||
el.__x.$data.toast(msg, type || 'info');
|
||||
}
|
||||
};
|
||||
|
||||
// ═══════════ GLOBAL KEYBOARD SHORTCUTS ═══════════
|
||||
document.addEventListener('keydown', function(e) {
|
||||
var tag = (document.activeElement && document.activeElement.tagName) || '';
|
||||
|
||||
@@ -42,17 +42,6 @@
|
||||
.gw-commit-bar{display:flex;align-items:center;gap:8px;padding:12px 24px;background:var(--bg-secondary);border-top:1px solid var(--border);flex-wrap:wrap;}
|
||||
.gw-commit-bar input{flex:1;min-width:180px;padding:8px 12px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:13px;outline:none;}
|
||||
.gw-commit-bar input:focus{border-color:var(--accent);}
|
||||
.btn-primary{padding:8px 16px;border-radius:8px;font-size:13px;cursor:pointer;border:none;font-weight:500;background:var(--accent);color:#fff;}
|
||||
.btn-primary:hover{background:var(--accent-hover);}
|
||||
.btn-danger{background:rgba(255,80,80,.15);color:var(--danger);border:none;padding:8px 16px;border-radius:8px;font-size:13px;cursor:pointer;font-weight:500;}
|
||||
.btn-danger:hover{background:rgba(255,80,80,.25);}
|
||||
.btn{border:none;padding:8px 16px;border-radius:8px;font-size:13px;cursor:pointer;font-weight:500;border:1px solid var(--border);background:transparent;color:var(--text-dim);}
|
||||
.btn:hover{background:var(--bg-tertiary);color:var(--text);}
|
||||
|
||||
/* ── Skeleton loading animation ── */
|
||||
.skeleton{background:var(--bg-tertiary);animation:pulse 1.5s ease-in-out infinite;border-radius:4px;}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
|
||||
.file-skeleton{padding:16px;border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary);max-height:70vh;overflow:hidden;}
|
||||
|
||||
/* Prism theme tweaks for dark UI */
|
||||
.gw-content pre[class*="language-"]{background:var(--bg-tertiary)!important;border:1px solid var(--border)!important;border-radius:8px!important;padding:16px!important;}
|
||||
@@ -70,7 +59,7 @@
|
||||
</div>
|
||||
<div class="gw-content">
|
||||
<!-- Skeleton loading -->
|
||||
<div x-show="fileLoading" class="file-skeleton">
|
||||
<div x-show="fileLoading" class="skeleton-file">
|
||||
<div class="skeleton" style="height:15px;width:60%;margin-bottom:12px;"></div>
|
||||
<div class="skeleton" style="height:15px;width:80%;margin-bottom:12px;"></div>
|
||||
<div class="skeleton" style="height:15px;width:40%;margin-bottom:12px;"></div>
|
||||
@@ -450,9 +439,9 @@ function giteaWorkspace() {
|
||||
}
|
||||
} else {
|
||||
var d = await r.json();
|
||||
alert('Failed to save: ' + (d.error || 'Unknown error'));
|
||||
window.showToast('Failed to save: ' + (d.error || 'Unknown error'), 'error');
|
||||
}
|
||||
} catch(e) { alert('Network error'); }
|
||||
} catch(e) { window.showToast('Network error', 'error'); }
|
||||
},
|
||||
|
||||
async deleteCurrentFile() {
|
||||
@@ -551,9 +540,9 @@ function giteaWorkspace() {
|
||||
await this.loadSidebarTree();
|
||||
} else {
|
||||
var d = await r.json();
|
||||
alert('Failed: ' + (d.error || d.detail || 'Unknown error'));
|
||||
window.showToast('Failed: ' + (d.error || d.detail || 'Unknown error'), 'error');
|
||||
}
|
||||
} catch(e) { alert('Error: ' + e.message); }
|
||||
} catch(e) { window.showToast('Error: ' + e.message, 'error'); }
|
||||
},
|
||||
|
||||
// Upload files
|
||||
@@ -720,7 +709,7 @@ function giteaWorkspace() {
|
||||
span.className = 'page-name';
|
||||
span.textContent = originalName;
|
||||
input.replaceWith(span);
|
||||
alert('Rename failed');
|
||||
window.showToast('Rename failed', 'error');
|
||||
}
|
||||
}).catch(function() {
|
||||
var span = document.createElement('span');
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
})
|
||||
.then(r => { if (!r.ok) throw new Error('CSRF error'); return r.json(); })
|
||||
.then(data => { window.location.href = `/pages/${data.id}`; })
|
||||
.catch(err => { alert('Failed: ' + err.message); });
|
||||
.catch(err => { window.showToast('Failed: ' + err.message, 'error'); });
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -152,10 +152,6 @@
|
||||
/* Inline rename */
|
||||
.inline-rename-input{background:var(--bg);border:1px solid var(--accent);border-radius:4px;color:var(--text);font-size:13px;padding:2px 6px;outline:none;min-width:100px;}
|
||||
|
||||
/* Loading skeleton */
|
||||
.skeleton{background:linear-gradient(90deg,var(--bg-tertiary) 25%,var(--bg-hover) 50%,var(--bg-tertiary) 75%);background-size:200% 100%;animation:shimmer 1.5s infinite;border-radius:4px;height:16px;margin:4px 0;}
|
||||
@keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
|
||||
|
||||
/* Drag count badge */
|
||||
.drag-badge{position:fixed;pointer-events:none;z-index:999;background:var(--accent);color:#fff;font-size:12px;font-weight:700;padding:2px 8px;border-radius:10px;transform:translate(-50%,-50%);}
|
||||
|
||||
@@ -1267,7 +1263,7 @@ window._wsInitData = (function() {
|
||||
if (targetId) {
|
||||
window.location = '/local-workspace?folder=' + targetId;
|
||||
} else {
|
||||
alert('Path not found: ' + parts.join('/'));
|
||||
window.showToast('Path not found: ' + parts.join('/'), 'error');
|
||||
}
|
||||
},
|
||||
_findFolderByPath(nodes, parts, idx) {
|
||||
@@ -1807,10 +1803,10 @@ window._wsInitData = (function() {
|
||||
if (r.ok) {
|
||||
this._reloadAfterAction();
|
||||
} else {
|
||||
alert('Move failed');
|
||||
window.showToast('Move failed', 'error');
|
||||
}
|
||||
} catch(e) {
|
||||
alert('Move failed: ' + e.message);
|
||||
window.showToast('Move failed: ' + e.message, 'error');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -660,9 +660,9 @@ page_title %}{{ page.title }}{% endblock %} {% block topbar %}
|
||||
invitePerson(){if(!this.inviteEmail.trim())return;this.accessList.push({email:this.inviteEmail,permission:this.invitePermission,permOpen:false});this.inviteEmail='';},
|
||||
removeAccess(email){this.accessList=this.accessList.filter(a=>a.email!==email);},
|
||||
exportPage(){this.moreOpen=false;},
|
||||
duplicatePage(){this.moreOpen=false;const csrf=document.cookie.match(/csrf_token=([^;]+)/);fetch(`/board/api/pages?title=${encodeURIComponent(this.pageTitle+' copy')}§ion=Private&project=${encodeURIComponent('{{ workspace_key }}')}`,{method:'POST',headers:{'X-CSRF-Token':csrf?csrf[1]:''}}).then(r=>r.json()).then(d=>{window.location.href=`/pages/${d.id}`;}).catch(()=>{alert('Duplicate failed');});},
|
||||
duplicatePage(){this.moreOpen=false;const csrf=document.cookie.match(/csrf_token=([^;]+)/);fetch(`/board/api/pages?title=${encodeURIComponent(this.pageTitle+' copy')}§ion=Private&project=${encodeURIComponent('{{ workspace_key }}')}`,{method:'POST',headers:{'X-CSRF-Token':csrf?csrf[1]:''}}).then(r=>r.json()).then(d=>{window.location.href=`/pages/${d.id}`;}).catch(()=>{window.showToast('Duplicate failed','error');});},
|
||||
movePage(){this.moreOpen=false;},
|
||||
deletePage(){this.moreOpen=false;if(!confirm('Move to Trash?'))return;const csrf=document.cookie.match(/csrf_token=([^;]+)/);fetch(`/board/api/pages/${this.pid}/trash`,{method:'POST',headers:{'X-CSRF-Token':csrf?csrf[1]:''}}).then(()=>{window.location.href='/';}).catch(()=>{alert('Failed');});},
|
||||
deletePage(){this.moreOpen=false;if(!confirm('Move to Trash?'))return;const csrf=document.cookie.match(/csrf_token=([^;]+)/);fetch(`/board/api/pages/${this.pid}/trash`,{method:'POST',headers:{'X-CSRF-Token':csrf?csrf[1]:''}}).then(()=>{window.location.href='/';}).catch(()=>{window.showToast('Failed','error');});},
|
||||
|
||||
onKd(e){
|
||||
const ab=this.getActiveBlock();if(!ab)return;
|
||||
|
||||
@@ -57,15 +57,6 @@
|
||||
.color-swatch:hover{transform:scale(1.15);}
|
||||
.color-swatch.selected{border-color:var(--text);box-shadow:0 0 0 2px var(--accent);}
|
||||
|
||||
/* Modal (for tag delete/rename) */
|
||||
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.85);display:flex;align-items:center;justify-content:center;z-index:999;}
|
||||
.modal-box{background:var(--bg-primary);border:1px solid var(--border);border-radius:12px;padding:24px;min-width:320px;box-shadow:0 12px 40px rgba(0,0,0,.8);}
|
||||
.modal-box h3{font-size:16px;font-weight:600;color:#fff;}
|
||||
.settings-input{background:var(--bg-tertiary);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;padding:6px 10px;outline:none;width:100%;}
|
||||
.settings-input:focus{border-color:var(--accent,#2383E2);box-shadow:0 0 0 2px rgba(35,131,226,.25);}
|
||||
.btn-danger{background:#E03E3E;color:#fff;border:none;padding:6px 16px;border-radius:6px;cursor:pointer;font-size:13px;}
|
||||
.btn-danger:hover{background:#C93535;}
|
||||
|
||||
/* Admin tables */
|
||||
.table-wrap{overflow-x:auto;border-radius:8px;border:1px solid #333;}
|
||||
.admin-table{width:100%;border-collapse:collapse;font-size:13px;}
|
||||
@@ -73,7 +64,7 @@
|
||||
.admin-table td{padding:8px 12px;border-top:1px solid var(--border);color:var(--text);}
|
||||
.admin-table tr:hover td{background:var(--bg-secondary);}
|
||||
.stat-card{background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;padding:14px;text-align:center;}
|
||||
.stat-value{font-size:22px;font-weight:700;color:var(--accent,#2383E2);}
|
||||
.stat-value{font-size:22px;font-weight:700;color:var(--accent);}
|
||||
.stat-label{font-size:11px;color:var(--text-dim);margin-top:2px;text-transform:uppercase;letter-spacing:.5px;}
|
||||
.role-badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:500;}
|
||||
.role-admin{background:rgba(35,131,226,.2);color:#4DA3F5;}
|
||||
@@ -81,15 +72,10 @@
|
||||
.status-dot{display:inline-block;width:6px;height:6px;border-radius:50%;margin-right:5px;vertical-align:middle;}
|
||||
.status-dot.active{background:#0F7B6C;}
|
||||
.status-dot.inactive{background:#E03E3E;}
|
||||
.btn-sm{background:transparent;border:1px solid var(--border);color:var(--text-dim);padding:2px 6px;border-radius:4px;cursor:pointer;font-size:12px;}
|
||||
.btn-sm:hover{background:var(--bg-tertiary);color:var(--text);}
|
||||
.btn-sm-danger{color:#E03E3E;border-color:#542;}
|
||||
|
||||
/* Close button */
|
||||
.settings-close{position:absolute;top:12px;right:12px;width:32px;height:32px;background:none;border:none;color:var(--text-dim);font-size:20px;cursor:pointer;border-radius:6px;display:flex;align-items:center;justify-content:center;}
|
||||
.settings-close:hover{background:var(--bg-hover);color:var(--text);}
|
||||
.settings-select{background:var(--bg-tertiary);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px 10px;font-size:13px;outline:none;cursor:pointer;min-width:140px;}
|
||||
.settings-select:focus{border-color:var(--accent);}
|
||||
</style>
|
||||
|
||||
<div class="settings-overlay" x-data="settingsInit()" @keydown.escape="window.history.back()">
|
||||
@@ -662,9 +648,9 @@ document.addEventListener('alpine:init', function() {
|
||||
await this.loadAdminUsers();
|
||||
} else {
|
||||
var d = await r.json();
|
||||
alert(d.error || 'Failed to create user');
|
||||
window.showToast(d.error || 'Failed to create user', 'error');
|
||||
}
|
||||
} catch(e) { alert('Network error'); }
|
||||
} catch(e) { window.showToast('Network error', 'error'); }
|
||||
},
|
||||
|
||||
async adminUpdateUser() {
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
.ws-card:hover{background:var(--bg-tertiary);border-color:var(--border-strong);}
|
||||
.ws-card.active{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent);}
|
||||
.ws-card-name{font-size:15px;font-weight:500;margin-bottom:2px;}
|
||||
.ws-card-sub{font-size:12px;color:var(--text-tertiary);margin-bottom:4px;}
|
||||
.ws-card-count{font-size:12px;color:var(--text-tertiary);}
|
||||
.ws-card-sub{font-size:12px;color:var(--text-dim);margin-bottom:4px;}
|
||||
.ws-card-count{font-size:12px;color:var(--text-dim);}
|
||||
.ws-card-actions{display:flex;gap:4px;margin-top:8px;opacity:0;transition:opacity 150ms;}
|
||||
.ws-card:hover .ws-card-actions{opacity:1;}
|
||||
|
||||
@@ -46,33 +46,12 @@
|
||||
.gitea-connect:hover{text-decoration:underline;}
|
||||
.gitea-loading{color:var(--text-dim);font-size:13px;padding:12px 0;}
|
||||
|
||||
.dialog-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;z-index:500;}
|
||||
.dialog-box{background:#1F1F1F;border:1px solid var(--border);border-radius:18px;padding:24px;width:400px;max-width:90vw;}
|
||||
.dialog-box h3{margin-bottom:16px;}
|
||||
.dialog-input{width:100%;padding:10px 12px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:14px;margin-bottom:12px;outline:none;}
|
||||
.dialog-input:focus{border-color:var(--accent);}
|
||||
.dialog-actions{display:flex;gap:8px;justify-content:flex-end;}
|
||||
.btn{padding:8px 16px;border-radius:8px;font-size:13px;cursor:pointer;border:none;font-weight:500;}
|
||||
.btn-primary{background:var(--accent);color:#fff;}
|
||||
.btn-primary:hover{background:var(--accent-hover);}
|
||||
.btn-secondary{background:var(--bg-tertiary);color:var(--text);}
|
||||
.btn-secondary:hover{background:var(--bg-hover);}
|
||||
.btn-danger{background:rgba(255,80,80,.15);color:var(--danger);}
|
||||
.btn-danger:hover{background:rgba(255,80,80,.25);}
|
||||
.btn-new{border:1px dashed var(--border-strong);background:transparent;color:var(--text-secondary);display:flex;align-items:center;justify-content:center;min-height:120px;}
|
||||
.btn-new:hover{background:var(--bg-tertiary);color:var(--text);}
|
||||
.btn-sm{padding:4px 8px;font-size:11px;border-radius:6px;border:1px solid var(--border);background:transparent;color:var(--text-dim);cursor:pointer;}
|
||||
.btn-sm:hover{background:var(--bg-tertiary);color:var(--text);}
|
||||
/* Org tabs */
|
||||
.org-tab{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:6px;font-size:13px;color:var(--text-dim);background:transparent;border:1px solid var(--border);cursor:pointer;white-space:nowrap;transition:all 100ms;}
|
||||
.org-tab:hover{background:var(--bg-tertiary);color:var(--text);}
|
||||
.org-tab.active{background:var(--accent);color:#fff;border-color:var(--accent);}
|
||||
.org-tab-badge{font-size:10px;padding:0 5px;border-radius:8px;background:rgba(255,255,255,.15);}
|
||||
.search-input:focus{border-color:var(--accent);}
|
||||
|
||||
/* ── Skeleton loading animation ── */
|
||||
.skeleton{background:var(--bg-tertiary);animation:pulse 1.5s ease-in-out infinite;border-radius:4px;}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
|
||||
</style>
|
||||
|
||||
<div class="ws-page" x-data="workspacesPage()">
|
||||
|
||||
Reference in New Issue
Block a user