phase 1 styling pass

This commit is contained in:
MattLeo 2025-12-08 15:12:08 -06:00
parent 40838bd134
commit 1f1c6e0bbc
19 changed files with 1228 additions and 973 deletions

View File

@ -1,158 +1,95 @@
.admin-panel {
padding: 2rem;
max-width: 1400px;
padding: var(--spacing-2xl);
max-width: var(--max-width-3xl);
margin: 0 auto;
}
.admin-header {
display: flex;
align-items: center;
gap: 2rem;
margin-bottom: 2rem;
border-bottom: 2px solid #333;
padding-bottom: 1rem;
}
.admin-header h1 {
margin: 0;
}
.admin-section {
margin-bottom: 3rem;
}
.admin-section h2 {
margin-bottom: 1rem;
color: #2c3e50;
}
.users-table {
width: 100%;
border-collapse: collapse;
background: #242424;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
background: var(--color-background-alt);
box-shadow: var(--shadow-sm);
}
.users-table th,
.users-table td {
padding: 12px;
padding: var(--spacing-md);
text-align: left;
border-bottom: 1px solid #ddd;
border-bottom: 1px solid var(--color-border);
}
.users-table th {
background-color: #2c3e50;
color: white;
font-weight: bold;
background-color: var(--color-dark);
color: var(--color-text-inverse);
font-weight: var(--font-weight-bold);
}
.users-table tr:hover {
background-color: #7f8c8d;
background-color: var(--color-dark-light);
}
.auth-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 0.85rem;
font-weight: bold;
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-sm);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-bold);
}
.auth-badge.local {
background-color: #95a5a6;
color: white;
background-color: var(--color-dark-light);
color: var(--color-text-inverse);
}
.auth-badge.entra {
background-color: #0078d4;
color: white;
color: var(--color-text-inverse);
}
.role-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 0.85rem;
font-weight: bold;
padding: var(--spacing-xs) var(--spacing-md);
border-radius: var(--radius-xl);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-bold);
}
.role-badge.admin {
background-color: #e74c3c;
color: white;
background-color: var(--color-danger);
color: var(--color-text-inverse);
}
.role-badge.editor {
background-color: #3498db;
color: white;
background-color: var(--color-primary);
color: var(--color-text-inverse);
}
.role-badge.user {
background-color: #95a5a6;
color: white;
background-color: var(--color-dark-light);
color: var(--color-text-inverse);
}
.role-select {
padding: 4px 8px;
border-radius: 4px;
border: 1px solid #ddd;
}
.action-buttons {
display: flex;
gap: 8px;
}
.action-buttons button {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
}
.edit-btn {
background-color: #3498db;
color: white;
}
.edit-btn:hover {
background-color: #2980b9;
}
.save-btn {
background-color: #27ae60;
color: white;
}
.save-btn:hover {
background-color: #229954;
}
.cancel-btn {
background-color: #95a5a6;
color: white;
}
.cancel-btn:hover {
background-color: #7f8c8d;
}
.delete-btn {
background-color: #e74c3c;
color: white;
}
.delete-btn:hover {
background-color: #c0392b;
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-sm);
border: 1px solid var(--color-border);
}
.coming-soon {
color: #7f8c8d;
color: var(--color-text-muted);
font-style: italic;
}
.error-message {
background-color: #ffebee;
color: #c62828;
padding: 12px;
border-radius: 4px;
margin-bottom: 1rem;
/* Responsive */
@media (max-width: 768px) {
.admin-panel {
padding: var(--spacing-lg);
}
.users-table {
font-size: var(--font-size-sm);
}
.users-table th,
.users-table td {
padding: var(--spacing-sm);
}
}

View File

@ -96,19 +96,27 @@ function AdminPanel({ token, onBack }) {
};
if (loading) {
return <div className='admin-panel'>Loading...</div>
return (
<div className='admin-panel'>
<div className='loading-container'>
<div className='loading-spinner'></div>
<div className='loading-text'>Loading users...</div>
</div>
</div>
);
}
return (
<div className='admin-panel'>
<div className='admin-header'>
<button className='back-button' onClick={onBack}> Back to Articles</button>
<div className='page-header'>
<h1 className='page-title'>Admin Panel</h1>
<button className='btn btn-ghost' onClick={onBack}> Back to Articles</button>
</div>
{error && <div className='error-message'>{error}</div>}
<div className='admin-section'>
<h2>User Management</h2>
<div className='section'>
<h2 className='section-title'>User Management</h2>
<table className='users-table'>
<thead>
<tr>
@ -123,7 +131,7 @@ function AdminPanel({ token, onBack }) {
</thead>
<tbody>
{users.map(user => (
<tr key ={user.id}>
<tr key={user.id}>
<td>{user.username}</td>
<td>{user.email}</td>
<td>{user.display_name}</td>
@ -152,24 +160,24 @@ function AdminPanel({ token, onBack }) {
<td>{new Date(user.created_at).toLocaleDateString()}</td>
<td>
{editingUserId === user.id ? (
<div className='action-buttons'>
<div className='btn-group'>
<button
className='save-btn'
className='btn btn-success btn-sm'
onClick={() => handleSaveRole(user.id)}
>Save</button>
<button
className='cancel-btn'
className='btn btn-secondary btn-sm'
onClick={() => handleCancelEdit()}
>Cancel</button>
</div>
) : (
<div className='action-buttons'>
<div className='btn-group'>
<button
className='edit-btn'
className='btn btn-primary btn-sm'
onClick={() => handleRoleChange(user.id, user.role)}
>Edit Role</button>
<button
className='delete-btn'
className='btn btn-danger btn-sm'
onClick={() => handleDeleteUser(user.id)}
>Delete</button>
</div>
@ -180,8 +188,9 @@ function AdminPanel({ token, onBack }) {
</tbody>
</table>
</div>
<div className='admin-section'>
<h2>Audit Logs</h2>
<div className='section'>
<h2 className='section-title'>Audit Logs</h2>
<p className='coming-soon'>Coming Soon...</p>
</div>
</div>

View File

@ -1,105 +1,43 @@
.article-detail {
padding: 2rem;
max-width: 800px;
padding: var(--spacing-2xl);
max-width: var(--max-width-lg);
margin: 0 auto;
}
.back-button {
background: none;
border: 1px solid #333;
padding: 0.5rem 1rem;
cursor: pointer;
margin-bottom: 1rem;
}
.back-button:hover {
background-color: #f0f0f0;
}
.edit-button {
background: none;
border: 1px solid #333;
padding: 0.5rem 1rem;
cursor: pointer;
margin-bottom: 1rem;
}
.edit-button:hover {
background-color: #f0f0f0;
}
.delete-button {
background: none;
border: 1px solid #333;
padding: 0.5rem 1rem;
cursor: pointer;
margin-bottom: 1rem;
}
.delete-button:hover {
background-color: #f0f0f0;
}
.ka-number {
background-color: #3498db;
color: white;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.9rem;
}
.article-meta {
color: #666;
font-size: 0.9rem;
color: var(--color-text-secondary);
font-size: var(--font-size-md);
}
.article-content {
margin-top: 2rem;
line-height: 1.6;
margin-top: var(--spacing-2xl);
line-height: var(--line-height-relaxed);
}
.article-metadata {
display: flex;
align-items: center;
gap: 1rem;
margin-top: 1rem;
gap: var(--spacing-lg);
margin-top: var(--spacing-lg);
flex-wrap: wrap;
}
.metadata-label {
font-weight: bold;
color: #2c3e50;
font-size: 0.95rem;
}
.metadata-badges {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.badge {
display: inline-flex;
align-items: center;
padding: 0.4rem 0.75rem;
border-radius: 16px;
font-size: 0.85rem;
font-weight: 500;
}
.category-badge {
background-color: #3498db;
color: white;
}
.tag-badge {
background-color: #27ae60;
color: white;
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
font-size: var(--font-size-md);
}
.article-content video {
max-width: 100%;
height: auto;
display: block;
margin: 1rem 0;
margin: var(--spacing-lg) 0;
}
/* Responsive */
@media (max-width: 768px) {
.article-detail {
padding: var(--spacing-lg);
}
}

View File

@ -9,16 +9,16 @@ function ArticleDetail({ article, onBack, onEdit, onDelete, currentUser }) {
return (
<div className='article-detail'>
<div className='button-group'>
<button className='back-button' onClick={onBack}>
<div className='btn-group' style={{ marginBottom: 'var(--spacing-xl)' }}>
<button className='btn btn-ghost' onClick={onBack}>
Back
</button>
{canEdit && (
<>
<button className='edit-button' onClick={onEdit}>
<button className='btn btn-primary' onClick={onEdit}>
Edit
</button>
<button className='delete-button' onClick={onDelete}>
<button className='btn btn-danger' onClick={onDelete}>
Delete
</button>
</>
@ -26,7 +26,7 @@ function ArticleDetail({ article, onBack, onEdit, onDelete, currentUser }) {
</div>
<div className='article-header'>
<span className='ka-number'>{article.ka_number}</span>
<span className='ka-number-badge'>{article.ka_number}</span>
<h1>{article.title}</h1>
<p className='article-meta'>
Created: {new Date(article.created_at).toLocaleDateString()}
@ -36,7 +36,7 @@ function ArticleDetail({ article, onBack, onEdit, onDelete, currentUser }) {
{article.categories && article.categories.length > 0 && (
<div className='article-metadata'>
<div className='metadata-label'>Categories:</div>
<div className='metadata-badges'>
<div className='badge-group'>
{article.categories.map(category => (
<span key={category} className='badge category-badge'>
{category}
@ -49,7 +49,7 @@ function ArticleDetail({ article, onBack, onEdit, onDelete, currentUser }) {
{article.tags && article.tags.length > 0 && (
<div className='article-metadata'>
<div className='metadata-label'>Tags:</div>
<div className='metadata-badges'>
<div className='badge-group'>
{article.tags.map(tag => (
<span key={tag} className='badge tag-badge'>
#{tag}

View File

@ -1,13 +1,17 @@
.article-editor {
padding: 2rem;
max-width: 900px;
padding: var(--spacing-2xl);
max-width: var(--max-width-lg);
margin: 0 auto;
transition: padding-bottom var(--transition-slow);
}
.editor-header {
margin-bottom: 2rem;
border-bottom: 2px solid #333;
padding-bottom: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-2xl);
padding-bottom: var(--spacing-lg);
border-bottom: 2px solid var(--color-border-dark);
}
.editor-header h2 {
@ -17,166 +21,25 @@
.editor-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.editor-form label {
font-weight: bold;
margin-bottom: 0.5rem;
display: block;
}
.editor-form input[type="text"] {
width: 100%;
padding: 0.75rem;
font-size: 1rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.editor-form input[type="text"]:focus {
outline: none;
border-color: #3498db;
gap: var(--spacing-xl);
}
/* React Quill editor styling */
.quill {
background-color: white;
background-color: var(--color-background);
}
.ql-container {
min-height: 300px;
font-size: 1rem;
font-size: var(--font-size-base);
}
.ql-editor {
min-height: 300px;
}
.editor-buttons {
display: flex;
gap: 1rem;
margin-top: 1rem;
}
.editor-buttons button {
padding: 0.75rem 2rem;
font-size: 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
}
.editor-buttons button:first-child {
background-color: #27ae60;
color: white;
}
.editor-buttons button:first-child:hover {
background-color: #229954;
}
.editor-buttons button:last-child {
background-color: #95a5a6;
color: white;
}
.editor-buttons button:last-child:hover {
background-color: #7f8c8d;
}
/* Form field styling */
.form-field {
margin-bottom: 1.5rem;
}
.form-field label {
display: block;
font-weight: bold;
margin-bottom: 0.5rem;
color: #2c3e50;
}
.form-field input[type="text"] {
width: 100%;
padding: 0.75rem;
font-size: 1rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.form-field input[type="text"]:focus {
outline: none;
border-color: #3498db;
}
/* Category selector */
.category-selector {
position: relative;
}
.category-dropdown-btn {
width: 100%;
padding: 0.75rem;
background-color: white;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
text-align: left;
font-size: 1rem;
}
.category-dropdown-btn:hover {
border-color: #3498db;
}
.dropdown-arrow {
color: #666;
font-size: 0.8rem;
}
.category-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
max-height: 200px;
overflow-y: auto;
z-index: 100;
margin-top: 4px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.category-option {
display: flex;
align-items: center;
padding: 0.75rem;
cursor: pointer;
transition: background-color 0.2s;
}
.category-option:hover {
background-color: #f5f5f5;
}
.category-option input[type="checkbox"] {
margin-right: 0.75rem;
cursor: pointer;
}
.no-options {
padding: 1rem;
text-align: center;
color: #999;
}
/* Tag selector */
/* Category/Tag selector specific styles */
.category-selector,
.tag-selector {
position: relative;
}
@ -185,142 +48,29 @@
position: relative;
}
.tag-input {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.tag-input:focus {
outline: none;
border-color: #3498db;
}
.tag-suggestions {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
margin-top: 4px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
z-index: 100;
}
.tag-suggestion {
padding: 0.75rem;
cursor: pointer;
transition: background-color 0.2s;
}
.tag-suggestion:hover {
background-color: #f5f5f5;
}
/* Badges */
.selected-badges {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.75rem;
}
.badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border-radius: 16px;
font-size: 0.9rem;
font-weight: 500;
}
.category-badge {
background-color: #3498db;
color: white;
}
.tag-badge {
background-color: #27ae60;
color: white;
}
.badge-remove {
background: none;
border: none;
color: white;
font-size: 1.2rem;
cursor: pointer;
padding: 0;
line-height: 1;
opacity: 0.8;
}
.badge-remove:hover {
opacity: 1;
}
.create-new-section {
padding: 0.75rem;
border-bottom: 1px solid #ddd;
background-color: #f9f9f9;
padding: var(--spacing-md);
border-bottom: 1px solid var(--color-border);
background-color: var(--color-gray-100);
}
.create-new-section input {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 0.5rem;
padding: var(--spacing-sm);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
margin-bottom: var(--spacing-sm);
}
.create-new-buttons {
display: flex;
gap: 0.5rem;
}
.create-new-buttons button {
padding: 0.4rem 0.8rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
}
.create-new-buttons .create-btn {
background-color: #27ae60;
color: white;
}
.create-new-buttons .create-btn:hover {
background-color: #229954;
}
.create-new-buttons .cancel-btn {
background-color: #95a5a6;
color: white;
}
.create-new-buttons .cancel-btn:hover {
background-color: #7f8c8d;
}
.create-new-button-dropdown {
width: 100%;
padding: 0.75rem;
background-color: #27ae60;
color: white;
border: none;
border-bottom: 1px solid #ddd;
text-align: left;
cursor: pointer;
font-weight: bold;
}
.create-new-button-dropdown:hover {
background-color: #229954;
/* Responsive */
@media (max-width: 768px) {
.article-editor {
padding: var(--spacing-lg);
}
.editor-header {
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-md);
}
}

View File

@ -84,6 +84,34 @@ function ArticleEditor({ article, onSave, onCancel, token }) {
}
}, [article]);
// Add padding and scroll when gallery opens
useEffect(() => {
const editorElement = document.querySelector('.article-editor');
if (isGalleryOpen) {
// Add padding equal to drawer height (40vh + some extra space)
editorElement.style.paddingBottom = 'calc(40vh + 2rem)';
// Scroll down smoothly so editor is visible above drawer
setTimeout(() => {
window.scrollTo({
top: document.body.scrollHeight,
behavior: 'smooth'
});
}, 100); // Small delay to let padding apply first
} else {
// Remove padding when closed
editorElement.style.paddingBottom = '';
}
// Cleanup on unmount
return () => {
if (editorElement) {
editorElement.style.paddingBottom = '';
}
};
}, [isGalleryOpen]);
const fetchCategories = async () => {
try {
const response = await fetch('http://localhost:9000/api/categories', {
@ -209,7 +237,7 @@ function ArticleEditor({ article, onSave, onCancel, token }) {
setShowCreateCategory(false);
} else {
const data = await response.json();
alert(data.error || 'Failed to create cateogry');
alert(data.error || 'Failed to create category');
}
} catch (err) {
console.error('Failed to create new category:', err);
@ -295,7 +323,7 @@ function ArticleEditor({ article, onSave, onCancel, token }) {
{article.ka_number}
</h2>
{article.status === 'draft' && (
<span className='draft-indicator'>DRAFT</span>
<span className='badge draft-badge'>DRAFT</span>
)}
</div>
@ -304,6 +332,7 @@ function ArticleEditor({ article, onSave, onCancel, token }) {
<label>Title:</label>
<input
type='text'
className='form-input'
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
@ -314,17 +343,19 @@ function ArticleEditor({ article, onSave, onCancel, token }) {
<div className='category-selector'>
<button
type='button'
className='category-dropdown-btn'
className='dropdown-button'
onClick={() => setShowCategoryDropdown(!showCategoryDropdown)}
>
{selectedCategories.length > 0
? `${selectedCategories.length} selected`
: 'Select categories'}
<span>
{selectedCategories.length > 0
? `${selectedCategories.length} selected`
: 'Select categories'}
</span>
<span className='dropdown-arrow'>{showCategoryDropdown ? '▲' : '▼'}</span>
</button>
{showCategoryDropdown && (
<div className='category-dropdown'>
<div className='dropdown-menu'>
{showCreateCategory ? (
<div className='create-new-section'>
<input
@ -340,17 +371,17 @@ function ArticleEditor({ article, onSave, onCancel, token }) {
}}
autoFocus
/>
<div className='create-new-buttons'>
<div className='btn-group gap-sm'>
<button
type='button'
className='create-btn'
className='btn btn-success btn-sm'
onClick={handleCreateNewCategory}
>
Create
</button>
<button
type='button'
className='cancel-btn'
className='btn btn-secondary btn-sm'
onClick={() => {
setShowCreateCategory(false);
setNewCategoryInput('');
@ -363,7 +394,8 @@ function ArticleEditor({ article, onSave, onCancel, token }) {
) : (
<button
type='button'
className='create-new-button-dropdown'
className='btn btn-success'
style={{ width: '100%', borderRadius: '0', borderBottom: '1px solid var(--color-border)' }}
onClick={() => setShowCreateCategory(true)}
>
+ Create New Category
@ -371,10 +403,10 @@ function ArticleEditor({ article, onSave, onCancel, token }) {
)}
{availableCategories.length === 0 ? (
<div className='no-options'>No categories available</div>
<div className='dropdown-empty'>No categories available</div>
) : (
availableCategories.map(category => (
<label key={category} className='category-option'>
<label key={category} className='dropdown-item'>
<input
type='checkbox'
checked={selectedCategories.includes(category)}
@ -387,9 +419,9 @@ function ArticleEditor({ article, onSave, onCancel, token }) {
</div>
)}
<div className='selected-badges'>
<div className='badge-group mt-md'>
{selectedCategories.map(category => (
<span key={category} className='badge category-badge'>
<span key={category} className='badge category-badge badge-removable'>
{category}
<button
type='button'
@ -408,7 +440,7 @@ function ArticleEditor({ article, onSave, onCancel, token }) {
<div className='tag-input-wrapper'>
<input
type='text'
className='tag-input'
className='form-input'
placeholder='Type to add tags...'
value={tagInput}
onChange={(e) => {
@ -420,11 +452,11 @@ function ArticleEditor({ article, onSave, onCancel, token }) {
/>
{showTagSuggestions && filteredTagSuggestions.length > 0 && (
<div className='tag-suggestions'>
<div className='dropdown-menu'>
{filteredTagSuggestions.slice(0, 5).map(tag => (
<div
key={tag}
className='tag-suggestion'
className='dropdown-item'
onClick={() => addTag(tag)}
>
{tag}
@ -434,9 +466,9 @@ function ArticleEditor({ article, onSave, onCancel, token }) {
)}
</div>
<div className='selected-badges'>
<div className='badge-group mt-md'>
{selectedTags.map(tag => (
<span key={tag} className='badge tag-badge'>
<span key={tag} className='badge tag-badge badge-removable'>
#{tag}
<button
type='button'
@ -460,14 +492,14 @@ function ArticleEditor({ article, onSave, onCancel, token }) {
/>
</div>
<div className='editor-buttons'>
<button className='save-draft-btn' onClick={handleSaveDraft}>
<div className='btn-group'>
<button className='btn btn-secondary' onClick={handleSaveDraft}>
Save Draft
</button>
<button className='publish-btn' onClick={handlePublish}>
<button className='btn btn-success' onClick={handlePublish}>
Publish
</button>
<button className='cancel-btn' onClick={onCancel}>
<button className='btn btn-ghost' onClick={onCancel}>
Cancel
</button>
</div>

View File

@ -1,56 +1,37 @@
.article-item {
border: 1px solid #ddd;
padding: 1rem;
margin-bottom: 1rem;
border-radius: 4px;
border: 1px solid var(--color-border);
padding: var(--spacing-lg);
margin-bottom: var(--spacing-lg);
border-radius: var(--radius-sm);
cursor: pointer;
transition: all 0.2s;
transition: all var(--transition-base);
}
.article-item:hover {
background-color: #f5f5f5;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
background-color: var(--color-gray-100);
box-shadow: var(--shadow-md);
}
.article-item-header h3 {
margin: 0 0 0.5rem 0;
margin: 0 0 var(--spacing-sm) 0;
}
.article-date {
color: #666;
font-size: 0.9rem;
color: var(--color-text-secondary);
font-size: var(--font-size-md);
margin: 0;
}
.article-item-metadata {
display: flex;
gap: 0.5rem;
margin-top: 0.75rem;
gap: var(--spacing-sm);
margin-top: var(--spacing-md);
flex-wrap: wrap;
}
.metadata-badges {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
/* Badges */
.badge {
display: inline-flex;
align-items: center;
padding: 0.3rem 0.6rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 500;
}
.category-badge {
background-color: #3498db;
color: white;
}
.tag-badge {
background-color: #27ae60;
color: white;
/* Responsive */
@media (max-width: 768px) {
.article-item {
padding: var(--spacing-md);
}
}

View File

@ -1,51 +1,27 @@
.drafts-list {
padding: 2rem;
max-width: 1200px;
padding: var(--spacing-2xl);
max-width: var(--max-width-2xl);
margin: 0 auto;
}
.drafts-header {
display: flex;
align-items: center;
gap: 2rem;
margin-bottom: 2rem;
border-bottom: 2px solid #333;
padding-bottom: 1rem;
}
.drafts-header h1 {
margin: 0;
}
.no-drafts {
text-align: center;
padding: 4rem 2rem;
color: #666;
}
.no-drafts p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
.drafts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
gap: var(--spacing-xl);
}
.draft-card {
background: white;
border: 2px solid #e0e0e0;
border-radius: 8px;
padding: 1.5rem;
background: var(--color-background);
border: 2px solid var(--color-border-light);
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
cursor: pointer;
transition: all 0.2s;
transition: all var(--transition-base);
}
.draft-card:hover {
border-color: #3498db;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: var(--color-primary);
box-shadow: var(--shadow-lg);
transform: translateY(-2px);
}
@ -53,43 +29,26 @@
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.draft-badge {
background-color: #f39c12;
color: white;
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: bold;
}
.ka-number {
background-color: #3498db;
color: white;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.85rem;
margin-bottom: var(--spacing-lg);
}
.draft-title {
margin: 0 0 1rem 0;
font-size: 1.25rem;
color: #2c3e50;
margin: 0 0 var(--spacing-lg) 0;
font-size: var(--font-size-xl);
color: var(--color-text-primary);
}
.draft-title:empty::before {
content: '(Untitled)';
color: #999;
color: var(--color-text-muted);
font-style: italic;
}
.draft-preview {
color: #666;
font-size: 0.9rem;
line-height: 1.4;
margin-bottom: 1rem;
color: var(--color-text-secondary);
font-size: var(--font-size-md);
line-height: var(--line-height-snug);
margin-bottom: var(--spacing-lg);
min-height: 3rem;
}
@ -97,33 +56,22 @@
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 1rem;
border-top: 1px solid #eee;
padding-top: var(--spacing-lg);
border-top: 1px solid var(--color-border-light);
}
.draft-date {
font-size: 0.85rem;
color: #999;
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
.delete-draft-btn {
background-color: #e74c3c;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
}
.delete-draft-btn:hover {
background-color: #c0392b;
}
.error-message {
background-color: #ffebee;
color: #c62828;
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
/* Responsive */
@media (max-width: 768px) {
.drafts-list {
padding: var(--spacing-lg);
}
.drafts-grid {
grid-template-columns: 1fr;
}
}

View File

@ -57,32 +57,44 @@ function DraftsList({ token, onBack, onSelectDraft }) {
};
if (loading) {
return <div className='drafts-list'>Loading...</div>;
return (
<div className='drafts-list'>
<div className='loading-container'>
<div className='loading-spinner'></div>
<div className='loading-text'>Loading drafts...</div>
</div>
</div>
);
}
return (
<div className='drafts-list'>
<div className='drafts-header'>
<h1>My Drafts</h1>
<div className='page-header'>
<h1 className='page-title'>My Drafts</h1>
<button className='btn btn-ghost' onClick={onBack}>
Back to Articles
</button>
</div>
{error && (<div className='error-message'>{error}</div>)}
{drafts.length === 0 ? (
<div className='no-drafts'>
<p>You do not have any drafts yet.</p>
<div className='empty-state'>
<div className='empty-state-icon'>📝</div>
<h2 className='empty-state-title'>No Drafts Yet</h2>
<p className='empty-state-description'>You don't have any drafts. Click "Create New Article" to start writing.</p>
</div>
) : (
<div className='drafts-grid'>
{drafts.map(draft => (
<div
key={draft.id}
className='draft-card'
className='draft-card card-hover'
onClick={() => onSelectDraft(draft)}
>
<div className='draft-header'>
<span className='draft-badge'>DRAFT</span>
<span className='ka-number'>{draft.ka_number}</span>
<span className='badge draft-badge'>DRAFT</span>
<span className='ka-number-badge'>{draft.ka_number}</span>
</div>
<h3 className='draft-title'>{draft.title || '(Untitled)'}</h3>
<div className='draft-preview'>
@ -94,15 +106,14 @@ function DraftsList({ token, onBack, onSelectDraft }) {
<div className='draft-footer'>
<span className='draft-date'>Created: {new Date(draft.created_at).toLocaleDateString()}</span>
<button
className='delete-draft-btn'
className='btn btn-danger btn-sm'
onClick={(e) => handleDeleteDraft(draft.ka_number, e)}
>Delete</button>
</div>
</div>
))}
</div>
)
}
)}
</div>
);
}

View File

@ -1,12 +1,12 @@
.filter-bar {
padding: 1rem 2rem;
background-color: #f9f9f9;
border-bottom: 1px solid #ddd;
padding: var(--spacing-lg) var(--spacing-2xl);
background-color: var(--color-background);
border-bottom: 1px solid var(--color-border);
}
.filter-controls {
display: flex;
gap: 1rem;
gap: var(--spacing-lg);
align-items: flex-end;
flex-wrap: wrap;
}
@ -14,142 +14,48 @@
.filter-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
gap: var(--spacing-sm);
}
.filter-group label {
font-weight: bold;
font-size: 0.9rem;
color: #2c3e50;
font-weight: var(--font-weight-bold);
font-size: var(--font-size-md);
color: var(--color-text-primary);
}
.filter-dropdown-container {
position: relative;
}
.filter-dropdown-btn {
min-width: 180px;
padding: 0.5rem 1rem;
background-color: white;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.9rem;
}
.filter-dropdown-btn:hover {
border-color: #3498db;
}
.dropdown-arrow {
color: #666;
font-size: 0.7rem;
margin-left: 0.5rem;
}
.filter-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
max-height: 250px;
overflow-y: auto;
z-index: 100;
margin-top: 4px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.filter-option {
display: flex;
align-items: center;
padding: 0.75rem;
cursor: pointer;
transition: background-color 0.2s;
}
.filter-option:hover {
background-color: #f5f5f5;
}
.filter-option input[type="checkbox"] {
margin-right: 0.75rem;
cursor: pointer;
}
.no-options {
padding: 1rem;
text-align: center;
color: #999;
}
.clear-filters-btn {
padding: 0.5rem 1rem;
background-color: #e74c3c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
align-self: flex-end;
}
.clear-filters-btn:hover {
background-color: #c0392b;
}
.active-filters {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
gap: var(--spacing-sm);
align-items: center;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #ddd;
margin-top: var(--spacing-lg);
padding-top: var(--spacing-lg);
border-top: 1px solid var(--color-border);
}
.active-filters-label {
font-weight: bold;
color: #2c3e50;
font-size: 0.9rem;
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
font-size: var(--font-size-md);
}
.filter-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.75rem;
border-radius: 16px;
font-size: 0.85rem;
font-weight: 500;
}
.filter-badge.category-badge {
background-color: #3498db;
color: white;
}
.filter-badge.tag-badge {
background-color: #27ae60;
color: white;
}
.filter-badge button {
background: none;
border: none;
color: white;
font-size: 1.2rem;
cursor: pointer;
padding: 0;
line-height: 1;
opacity: 0.8;
}
.filter-badge button:hover {
opacity: 1;
/* Responsive */
@media (max-width: 768px) {
.filter-bar {
padding: var(--spacing-md) var(--spacing-lg);
}
.filter-controls {
flex-direction: column;
align-items: stretch;
}
.filter-dropdown-container {
min-width: auto;
}
}

View File

@ -1,7 +1,7 @@
import { useState, useEffect } from 'react';
import './FilterBar.css';
function FilterBar ({ token, onFilterChange }) {
function FilterBar({ token, onFilterChange }) {
const [availableCategories, setAvailableCategories] = useState([]);
const [availableTags, setAvailableTags] = useState([]);
const [selectedCategories, setSelectedCategories] = useState([]);
@ -34,7 +34,7 @@ function FilterBar ({ token, onFilterChange }) {
setAvailableCategories(data.map(category => category.name));
}
} catch(err) {
console.error('Error fetchign categories:', err);
console.error('Error fetching categories:', err);
}
};
@ -85,22 +85,24 @@ function FilterBar ({ token, onFilterChange }) {
<label>Filter by Category:</label>
<div className="filter-dropdown-container">
<button
className="filter-dropdown-btn"
className="dropdown-button"
onClick={() => setShowCategoryDropdown(!showCategoryDropdown)}
>
{selectedCategories.length > 0
? `${selectedCategories.length} selected`
: 'All Categories'}
<span>
{selectedCategories.length > 0
? `${selectedCategories.length} selected`
: 'All Categories'}
</span>
<span className="dropdown-arrow">{showCategoryDropdown ? '▲' : '▼'}</span>
</button>
{showCategoryDropdown && (
<div className="filter-dropdown">
<div className="dropdown-menu">
{availableCategories.length === 0 ? (
<div className="no-options">No categories available</div>
<div className="dropdown-empty">No categories available</div>
) : (
availableCategories.map(category => (
<label key={category} className="filter-option">
<label key={category} className="dropdown-item">
<input
type="checkbox"
checked={selectedCategories.includes(category)}
@ -119,22 +121,24 @@ function FilterBar ({ token, onFilterChange }) {
<label>Filter by Tag:</label>
<div className="filter-dropdown-container">
<button
className="filter-dropdown-btn"
className="dropdown-button"
onClick={() => setShowTagDropdown(!showTagDropdown)}
>
{selectedTags.length > 0
? `${selectedTags.length} selected`
: 'All Tags'}
<span>
{selectedTags.length > 0
? `${selectedTags.length} selected`
: 'All Tags'}
</span>
<span className="dropdown-arrow">{showTagDropdown ? '▲' : '▼'}</span>
</button>
{showTagDropdown && (
<div className="filter-dropdown">
<div className="dropdown-menu">
{availableTags.length === 0 ? (
<div className="no-options">No tags available</div>
<div className="dropdown-empty">No tags available</div>
) : (
availableTags.map(tag => (
<label key={tag} className="filter-option">
<label key={tag} className="dropdown-item">
<input
type="checkbox"
checked={selectedTags.includes(tag)}
@ -150,7 +154,7 @@ function FilterBar ({ token, onFilterChange }) {
</div>
{hasActiveFilters && (
<button className="clear-filters-btn" onClick={clearFilters}>
<button className="btn btn-danger btn-sm" onClick={clearFilters}>
Clear Filters
</button>
)}
@ -160,22 +164,21 @@ function FilterBar ({ token, onFilterChange }) {
<div className="active-filters">
<span className="active-filters-label">Active Filters:</span>
{selectedCategories.map(category => (
<span key={category} className="filter-badge category-badge">
<span key={category} className="badge category-badge badge-removable">
{category}
<button onClick={() => toggleCategory(category)}>×</button>
<button className="badge-remove" onClick={() => toggleCategory(category)}>×</button>
</span>
))}
{selectedTags.map(tag => (
<span key={tag} className="filter-badge tag-badge">
<span key={tag} className="badge tag-badge badge-removable">
#{tag}
<button onClick={() => toggleTag(tag)}>×</button>
<button className="badge-remove" onClick={() => toggleTag(tag)}>×</button>
</span>
))}
</div>
)}
</div>
);
}
export default FilterBar;

View File

@ -3,45 +3,45 @@
bottom: 0;
left: 50%;
transform: translateX(-50%);
background-color: #3498db;
color: white;
background-color: var(--color-primary);
color: var(--color-text-inverse);
border: none;
border-radius: 8px 8px 0 0;
padding: 0.75rem 2rem;
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
padding: var(--spacing-md) var(--spacing-2xl);
cursor: pointer;
z-index: 1001;
z-index: var(--z-drawer-toggle);
display: flex;
align-items: center;
gap: 0.5rem;
gap: var(--spacing-sm);
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.2);
transition: background-color 0.2s;
transition: background-color var(--transition-base);
}
.drawer-toggle:hover {
background-color: #2980b9;
background-color: var(--color-primary-hover);
}
.drawer-toggle.open {
background-color: #e74c3c;
background-color: var(--color-danger);
}
.drawer-toggle.open:hover {
background-color: #c0392b;
background-color: var(--color-danger-hover);
}
.toggle-icon {
font-size: 1.2rem;
font-size: var(--font-size-xl);
}
.toggle-text {
font-weight: bold;
font-weight: var(--font-weight-bold);
}
.media-count {
background-color: rgba(255, 255, 255, 0.3);
padding: 0.2rem 0.5rem;
border-radius: 12px;
font-size: 0.85rem;
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-xl);
font-size: var(--font-size-sm);
}
.media-drawer {
@ -50,12 +50,12 @@
left: 0;
right: 0;
height: 40vh;
background-color: white;
border-radius: 12px 12px 0 0;
background-color: var(--color-background);
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.3);
z-index: 1000;
z-index: var(--z-drawer);
transform: translateY(100%);
transition: transform 0.3s ease-in-out;
transition: transform var(--transition-slow);
display: flex;
flex-direction: column;
}
@ -65,94 +65,64 @@
}
.drawer-handle {
padding: 0.75rem;
padding: var(--spacing-md);
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
background-color: #f5f5f5;
border-radius: 12px 12px 0 0;
background-color: var(--color-gray-200);
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
}
.drawer-handle:hover {
background-color: #e0e0e0;
background-color: var(--color-gray-300);
}
.handle-bar {
width: 50px;
height: 4px;
background-color: #bbb;
border-radius: 2px;
background-color: var(--color-gray-400);
border-radius: var(--radius-sm);
}
.drawer-content {
flex: 1;
overflow-y: auto;
padding: 1rem 2rem;
padding: var(--spacing-lg) var(--spacing-2xl);
}
.gallery-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 2px solid #e0e0e0;
margin-bottom: var(--spacing-lg);
padding-bottom: var(--spacing-lg);
border-bottom: 2px solid var(--color-border-light);
}
.gallery-header h3 {
margin: 0;
color: #2c3e50;
}
.upload-button {
background-color: #27ae60;
color: white;
padding: 0.75rem 1.5rem;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s;
font-weight: bold;
}
.upload-button:hover {
background-color: #229954;
}
.loading {
text-align: center;
padding: 2rem;
color: #666;
}
.no-media {
text-align: center;
padding: 2rem;
color: #666;
}
.no-media p {
margin: 0.5rem 0;
color: var(--color-text-primary);
}
.media-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 1rem;
padding: 1rem 0;
gap: var(--spacing-lg);
padding: var(--spacing-lg) 0;
}
.media-item {
border: 2px solid #e0e0e0;
border-radius: 8px;
padding: 0.75rem;
background: white;
transition: all 0.2s;
border: 2px solid var(--color-border-light);
border-radius: var(--radius-md);
padding: var(--spacing-md);
background: var(--color-background);
transition: all var(--transition-base);
}
.media-item:hover {
border-color: #3498db;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: var(--color-primary);
box-shadow: var(--shadow-lg);
transform: translateY(-2px);
}
@ -160,22 +130,22 @@
width: 100%;
height: 120px;
object-fit: cover;
border-radius: 6px;
margin-bottom: 0.75rem;
border-radius: var(--radius-md);
margin-bottom: var(--spacing-md);
cursor: pointer;
}
.video-thumbnail {
width: 100%;
height: 120px;
background-color: #34495e;
border-radius: 6px;
background-color: var(--color-dark-hover);
border-radius: var(--radius-md);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 0.75rem;
color: white;
margin-bottom: var(--spacing-md);
color: var(--color-text-inverse);
cursor: pointer;
}
@ -184,59 +154,33 @@
}
.video-label {
margin-top: 0.5rem;
font-size: 0.85rem;
font-weight: bold;
}
.media-actions {
display: flex;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.media-actions button {
flex: 1;
padding: 0.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
font-weight: bold;
transition: all 0.2s;
}
.insert-btn {
background-color: #3498db;
color: white;
}
.insert-btn:hover {
background-color: #2980b9;
}
.delete-btn {
background-color: #e74c3c;
color: white;
}
.delete-btn:hover {
background-color: #c0392b;
margin-top: var(--spacing-sm);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-bold);
}
.media-filename {
font-size: 0.75rem;
color: #666;
font-size: var(--font-size-xs);
color: var(--color-text-secondary);
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-top: var(--spacing-sm);
}
.error-message {
background-color: #ffebee;
color: #c62828;
padding: 0.75rem;
border-radius: 4px;
margin-bottom: 1rem;
/* Responsive */
@media (max-width: 768px) {
.media-drawer {
height: 50vh;
}
.drawer-content {
padding: var(--spacing-md) var(--spacing-lg);
}
.media-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: var(--spacing-md);
}
}

View File

@ -60,7 +60,7 @@ function MediaGallery({ kaNumber, token, onInsertMedia, isOpen, onToggle }) {
setError(data.error || 'Upload failed');
}
} catch(err) {
console.error('Failed to uplaod file:', err);
console.error('Failed to upload file:', err);
setError('Failed to upload file');
} finally {
setUploading(false);
@ -106,7 +106,7 @@ function MediaGallery({ kaNumber, token, onInsertMedia, isOpen, onToggle }) {
<div className='drawer-content'>
<div className='gallery-header'>
<h3>Media Gallery - {kaNumber}</h3>
<label className='upload-button'>
<label className='btn btn-success'>
<input
type='file'
onChange={handleFileUpload}
@ -121,11 +121,17 @@ function MediaGallery({ kaNumber, token, onInsertMedia, isOpen, onToggle }) {
{error && <div className='error-message'>{error}</div>}
{loading ? (
<div className="loading">Loading media...</div>
<div className='loading-container'>
<div className='loading-spinner'></div>
<div className='loading-text'>Loading media...</div>
</div>
) : media.length === 0 ? (
<div className="no-media">
<p>No media uploaded yet</p>
<p>Click "Upload Media" to add images or videos</p>
<div className='empty-state'>
<div className='empty-state-icon'>📷</div>
<h3 className='empty-state-title'>No Media Yet</h3>
<p className='empty-state-description'>
Click "Upload Media" to add images or videos to this article.
</p>
</div>
) : (
<div className='media-grid'>
@ -144,13 +150,13 @@ function MediaGallery({ kaNumber, token, onInsertMedia, isOpen, onToggle }) {
</div>
)}
<div className='media-actions'>
<div className='btn-group gap-sm'>
<button
className='insert-btn'
className='btn btn-primary btn-sm'
onClick={() => onInsertMedia(item)}
>Insert</button>
<button
className='delete-btn'
className='btn btn-danger btn-sm'
onClick={() => handleDelete(item.filename)}
>Delete</button>
</div>

View File

@ -0,0 +1,70 @@
/* SearchBar Component Styles */
.search-bar {
padding: var(--spacing-xl) var(--spacing-2xl);
background-color: var(--color-background);
border-bottom: 1px solid var(--color-border);
}
.search-bar form {
display: flex;
gap: var(--spacing-md);
max-width: 800px;
margin: 0 auto;
}
.search-bar input[type="text"] {
flex: 1;
padding: var(--spacing-md) var(--spacing-lg);
font-size: var(--font-size-base);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background-color: var(--color-background);
transition: border-color var(--transition-base);
}
.search-bar input[type="text"]:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px var(--color-primary-light);
}
.search-bar input[type="text"]::placeholder {
color: var(--color-text-muted);
}
.search-bar button[type="submit"] {
padding: var(--spacing-md) var(--spacing-2xl);
background-color: var(--color-primary);
color: var(--color-text-inverse);
border: none;
border-radius: var(--radius-sm);
font-size: var(--font-size-base);
font-weight: var(--font-weight-medium);
cursor: pointer;
transition: background-color var(--transition-base);
white-space: nowrap;
}
.search-bar button[type="submit"]:hover {
background-color: var(--color-primary-hover);
}
.search-bar button[type="submit"]:active {
transform: translateY(1px);
}
/* Responsive */
@media (max-width: 640px) {
.search-bar {
padding: var(--spacing-lg);
}
.search-bar form {
flex-direction: column;
}
.search-bar button[type="submit"] {
width: 100%;
}
}

View File

@ -1,5 +1,6 @@
//import 'SearchBar.css'
import { useState } from 'react';
import './SearchBar.css';
function SearchBar({ onSearch }) {
const [query, setQuery] = useState('');
@ -16,15 +17,10 @@ function SearchBar({ onSearch }) {
type='text'
placeholder='Search articles by title, content, or KA number...'
value={query}
onChange={(e) => setQuery(e.target.value)}
/* USer this for instant search
onChange={(e) => {
setQuery(e.target.value);
onSearch(e.target.value);
}}
*/
/>
<button type='submit'>Search</button>
</form>

View File

@ -1,68 +1,60 @@
/* Base Reset & Typography */
* {
box-sizing: border-box;
}
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
background-color: var(--color-background);
color: var(--color-text-primary);
}
h1, h2, h3, h4, h5, h6 {
line-height: 1.2;
margin-top: 0;
}
h1 {
font-size: 3.2em;
font-size: 2rem;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
a {
color: var(--color-primary);
text-decoration: none;
transition: color var(--transition-base);
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
a:hover {
color: var(--color-primary-hover);
}
/* Remove default button styles - our .btn class will handle it */
button {
font-family: inherit;
}
/* App container */
#root {
min-height: 100vh;
}
/* Override Vite's centered layout */
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
}

View File

@ -0,0 +1,599 @@
/* Cram-A-Lot Knowledge Base - Shared Component Styles */
/* Import this in your main.jsx or index.jsx after variables.css */
/* ===== BUTTONS ===== */
/* Base button styles */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--spacing-sm);
padding: var(--spacing-md) var(--spacing-xl);
font-size: var(--font-size-base);
font-weight: var(--font-weight-medium);
line-height: 1;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
transition: all var(--transition-base);
text-decoration: none;
white-space: nowrap;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Button sizes */
.btn-sm {
padding: var(--spacing-sm) var(--spacing-lg);
font-size: var(--font-size-sm);
}
.btn-lg {
padding: var(--spacing-lg) var(--spacing-2xl);
font-size: var(--font-size-lg);
}
/* Button variants */
.btn-primary {
background-color: var(--color-primary);
color: var(--color-text-inverse);
}
.btn-primary:hover:not(:disabled) {
background-color: var(--color-primary-hover);
}
.btn-success {
background-color: var(--color-success);
color: var(--color-text-inverse);
}
.btn-success:hover:not(:disabled) {
background-color: var(--color-success-hover);
}
.btn-danger {
background-color: var(--color-danger);
color: var(--color-text-inverse);
}
.btn-danger:hover:not(:disabled) {
background-color: var(--color-danger-hover);
}
.btn-secondary {
background-color: var(--color-dark-light);
color: var(--color-text-inverse);
}
.btn-secondary:hover:not(:disabled) {
background-color: var(--color-dark-hover);
}
.btn-ghost {
background-color: transparent;
color: var(--color-text-primary);
border: 1px solid var(--color-border);
}
.btn-ghost:hover:not(:disabled) {
background-color: var(--color-gray-200);
border-color: var(--color-gray-400);
}
.btn-link {
background: none;
border: none;
color: var(--color-primary);
text-decoration: underline;
padding: 0;
}
.btn-link:hover:not(:disabled) {
color: var(--color-primary-hover);
}
/* Button groups */
.btn-group {
display: flex;
gap: var(--spacing-lg);
align-items: center;
}
/* ===== BADGES ===== */
.badge {
display: inline-flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-2xl);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
line-height: 1;
white-space: nowrap;
}
.badge-sm {
padding: var(--spacing-xs) var(--spacing-sm);
font-size: var(--font-size-xs);
}
.badge-lg {
padding: var(--spacing-md) var(--spacing-lg);
font-size: var(--font-size-base);
}
/* Badge variants */
.badge-primary {
background-color: var(--color-primary);
color: var(--color-text-inverse);
}
.badge-success {
background-color: var(--color-success);
color: var(--color-text-inverse);
}
.badge-danger {
background-color: var(--color-danger);
color: var(--color-text-inverse);
}
.badge-warning {
background-color: var(--color-warning);
color: var(--color-text-inverse);
}
.badge-secondary {
background-color: var(--color-dark-light);
color: var(--color-text-inverse);
}
.badge-gray {
background-color: var(--color-gray-400);
color: var(--color-text-inverse);
}
/* Special badge types for app */
.category-badge {
background-color: var(--color-primary);
color: var(--color-text-inverse);
}
.tag-badge {
background-color: var(--color-success);
color: var(--color-text-inverse);
}
.draft-badge {
background-color: var(--color-warning);
color: var(--color-text-inverse);
}
.ka-number-badge {
background-color: var(--color-primary);
color: var(--color-text-inverse);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-sm);
font-size: var(--font-size-sm);
}
/* Badge with remove button */
.badge-removable {
padding-right: var(--spacing-xs);
}
.badge-remove {
background: none;
border: none;
color: inherit;
font-size: 1.2rem;
cursor: pointer;
padding: 0 var(--spacing-xs);
line-height: 1;
opacity: 0.8;
transition: opacity var(--transition-fast);
}
.badge-remove:hover {
opacity: 1;
}
/* Badge container */
.badge-group {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-sm);
align-items: center;
}
/* ===== FORM ELEMENTS ===== */
.form-field {
margin-bottom: var(--spacing-xl);
}
.form-field label {
display: block;
font-weight: var(--font-weight-bold);
margin-bottom: var(--spacing-sm);
color: var(--color-text-primary);
font-size: var(--font-size-md);
}
.form-input,
.form-textarea,
.form-select {
width: 100%;
padding: var(--spacing-md);
font-size: var(--font-size-base);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background-color: var(--color-background);
transition: border-color var(--transition-base);
box-sizing: border-box;
}
.form-input:focus,
.form-textarea:focus,
.form-select:focus {
outline: none;
border-color: var(--color-primary);
}
.form-input:disabled,
.form-textarea:disabled,
.form-select:disabled {
background-color: var(--color-gray-200);
cursor: not-allowed;
opacity: 0.6;
}
.form-textarea {
min-height: 120px;
resize: vertical;
}
/* Form helper text */
.form-helper {
display: block;
margin-top: var(--spacing-xs);
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
.form-error {
color: var(--color-danger);
}
/* ===== MESSAGES & ALERTS ===== */
.message {
padding: var(--spacing-lg);
border-radius: var(--radius-sm);
margin-bottom: var(--spacing-lg);
display: flex;
align-items: flex-start;
gap: var(--spacing-md);
}
.error-message {
background-color: var(--color-danger-light);
color: var(--color-danger-hover);
border: 1px solid var(--color-danger);
}
.success-message {
background-color: var(--color-success-light);
color: var(--color-success-hover);
border: 1px solid var(--color-success);
}
.warning-message {
background-color: var(--color-warning-light);
color: var(--color-warning-hover);
border: 1px solid var(--color-warning);
}
.info-message {
background-color: var(--color-primary-light);
color: var(--color-primary-hover);
border: 1px solid var(--color-primary);
}
/* ===== LOADING STATES ===== */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--spacing-3xl) var(--spacing-2xl);
color: var(--color-text-muted);
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid var(--color-gray-300);
border-top-color: var(--color-primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.loading-spinner-sm {
width: 24px;
height: 24px;
border-width: 3px;
}
.loading-spinner-lg {
width: 56px;
height: 56px;
border-width: 5px;
}
.loading-text {
margin-top: var(--spacing-lg);
font-size: var(--font-size-base);
color: var(--color-text-secondary);
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* ===== EMPTY STATES ===== */
.empty-state {
text-align: center;
padding: var(--spacing-3xl) var(--spacing-2xl);
color: var(--color-text-muted);
}
.empty-state-icon {
font-size: var(--font-size-3xl);
margin-bottom: var(--spacing-lg);
opacity: 0.5;
}
.empty-state-title {
font-size: var(--font-size-xl);
font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
margin-bottom: var(--spacing-sm);
}
.empty-state-description {
font-size: var(--font-size-base);
color: var(--color-text-muted);
margin-bottom: var(--spacing-xl);
}
/* ===== DROPDOWNS ===== */
.dropdown-container {
position: relative;
}
.dropdown-button {
width: 100%;
padding: var(--spacing-md);
background-color: var(--color-background);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
text-align: left;
font-size: var(--font-size-base);
transition: border-color var(--transition-base);
}
.dropdown-button:hover {
border-color: var(--color-primary);
}
.dropdown-arrow {
color: var(--color-text-muted);
font-size: var(--font-size-sm);
transition: transform var(--transition-base);
}
.dropdown-arrow.open {
transform: rotate(180deg);
}
.dropdown-menu {
position: absolute;
top: calc(100% + var(--spacing-xs));
left: 0;
right: 0;
background: var(--color-background);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
max-height: 250px;
overflow-y: auto;
z-index: var(--z-dropdown);
box-shadow: var(--shadow-lg);
}
.dropdown-item {
padding: var(--spacing-md);
cursor: pointer;
transition: background-color var(--transition-base);
display: flex;
align-items: center;
gap: var(--spacing-md);
}
.dropdown-item:hover {
background-color: var(--color-gray-100);
}
.dropdown-item input[type="checkbox"] {
cursor: pointer;
}
.dropdown-divider {
height: 1px;
background-color: var(--color-border);
margin: var(--spacing-xs) 0;
}
.dropdown-header {
padding: var(--spacing-md);
font-weight: var(--font-weight-bold);
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
background-color: var(--color-gray-100);
}
.dropdown-empty {
padding: var(--spacing-lg);
text-align: center;
color: var(--color-text-muted);
font-size: var(--font-size-sm);
}
/* ===== CARDS ===== */
.card {
background: var(--color-background);
border: 1px solid var(--color-border-light);
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
transition: all var(--transition-base);
}
.card-hover:hover {
border-color: var(--color-primary);
box-shadow: var(--shadow-lg);
transform: translateY(-2px);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-lg);
}
.card-title {
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
margin: 0;
}
.card-body {
color: var(--color-text-secondary);
line-height: var(--line-height-relaxed);
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: var(--spacing-lg);
padding-top: var(--spacing-lg);
border-top: 1px solid var(--color-border-light);
}
/* ===== HEADERS & SECTIONS ===== */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--spacing-2xl);
padding-bottom: var(--spacing-lg);
border-bottom: 2px solid var(--color-border-dark);
}
.page-title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
margin: 0;
}
.section {
margin-bottom: var(--spacing-3xl);
}
.section-title {
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
margin-bottom: var(--spacing-lg);
}
/* ===== UTILITIES ===== */
.text-center {
text-align: center;
}
.text-muted {
color: var(--color-text-muted);
}
.text-secondary {
color: var(--color-text-secondary);
}
.text-sm {
font-size: var(--font-size-sm);
}
.text-lg {
font-size: var(--font-size-lg);
}
.font-bold {
font-weight: var(--font-weight-bold);
}
.mt-0 { margin-top: 0; }
.mt-sm { margin-top: var(--spacing-sm); }
.mt-md { margin-top: var(--spacing-md); }
.mt-lg { margin-top: var(--spacing-lg); }
.mt-xl { margin-top: var(--spacing-xl); }
.mt-2xl { margin-top: var(--spacing-2xl); }
.mb-0 { margin-bottom: 0; }
.mb-sm { margin-bottom: var(--spacing-sm); }
.mb-md { margin-bottom: var(--spacing-md); }
.mb-lg { margin-bottom: var(--spacing-lg); }
.mb-xl { margin-bottom: var(--spacing-xl); }
.mb-2xl { margin-bottom: var(--spacing-2xl); }
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.gap-sm { gap: var(--spacing-sm); }
.gap-md { gap: var(--spacing-md); }
.gap-lg { gap: var(--spacing-lg); }
.gap-xl { gap: var(--spacing-xl); }

View File

@ -0,0 +1,133 @@
/* Cram-A-Lot Knowledge Base - Design System Variables */
:root {
/* ===== COLORS ===== */
/* Primary Blue */
--color-primary: #3498db;
--color-primary-hover: #2980b9;
--color-primary-light: #ebf5fb;
/* Success Green */
--color-success: #27ae60;
--color-success-hover: #229954;
--color-success-light: #e8f6f0;
/* Danger Red */
--color-danger: #e74c3c;
--color-danger-hover: #c0392b;
--color-danger-light: #ffebee;
/* Warning Orange */
--color-warning: #f39c12;
--color-warning-hover: #e67e22;
--color-warning-light: #fef5e7;
/* Neutral Grays */
--color-gray-50: #fafafa;
--color-gray-100: #f9f9f9;
--color-gray-200: #f5f5f5;
--color-gray-300: #e0e0e0;
--color-gray-400: #bbb;
--color-gray-500: #999;
--color-gray-600: #666;
--color-gray-700: #333;
--color-gray-800: #242424;
/* Dark Colors */
--color-dark: #2c3e50;
--color-dark-hover: #34495e;
--color-dark-light: #7f8c8d;
/* Semantic Colors */
--color-text-primary: #2c3e50;
--color-text-secondary: #666;
--color-text-muted: #999;
--color-text-inverse: #ffffff;
--color-border: #ddd;
--color-border-light: #e0e0e0;
--color-border-dark: #333;
--color-background: #ffffff;
--color-background-alt: #f9f9f9;
--color-background-dark: #242424;
/* ===== SPACING SCALE ===== */
--spacing-xs: 0.25rem; /* 4px */
--spacing-sm: 0.5rem; /* 8px */
--spacing-md: 0.75rem; /* 12px */
--spacing-lg: 1rem; /* 16px */
--spacing-xl: 1.5rem; /* 24px */
--spacing-2xl: 2rem; /* 32px */
--spacing-3xl: 3rem; /* 48px */
--spacing-4xl: 4rem; /* 64px */
/* ===== TYPOGRAPHY ===== */
--font-size-xs: 0.75rem; /* 12px */
--font-size-sm: 0.85rem; /* 13.6px */
--font-size-base: 1rem; /* 16px */
--font-size-md: 0.95rem; /* 15.2px */
--font-size-lg: 1.1rem; /* 17.6px */
--font-size-xl: 1.25rem; /* 20px */
--font-size-2xl: 1.5rem; /* 24px */
--font-size-3xl: 2rem; /* 32px */
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--line-height-tight: 1.2;
--line-height-snug: 1.4;
--line-height-normal: 1.5;
--line-height-relaxed: 1.6;
/* ===== BORDER RADIUS ===== */
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 12px;
--radius-2xl: 16px;
--radius-full: 9999px;
/* ===== SHADOWS ===== */
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.15);
--shadow-xl: 0 8px 24px rgba(0, 0, 0, 0.2);
/* ===== TRANSITIONS ===== */
--transition-fast: 0.15s ease;
--transition-base: 0.2s ease;
--transition-slow: 0.3s ease;
--transition-slower: 0.4s ease;
/* ===== Z-INDEX SCALE ===== */
--z-base: 1;
--z-dropdown: 100;
--z-sticky: 500;
--z-fixed: 1000;
--z-drawer: 1000;
--z-drawer-toggle: 1001;
--z-modal-backdrop: 1040;
--z-modal: 1050;
--z-popover: 1060;
--z-tooltip: 1070;
/* ===== LAYOUT ===== */
--max-width-xs: 480px;
--max-width-sm: 640px;
--max-width-md: 768px;
--max-width-lg: 900px;
--max-width-xl: 1024px;
--max-width-2xl: 1280px;
--max-width-3xl: 1400px;
/* ===== BREAKPOINTS (for reference, use in media queries) ===== */
--breakpoint-sm: 640px;
--breakpoint-md: 768px;
--breakpoint-lg: 1024px;
--breakpoint-xl: 1280px;
}