1 Commits

Author SHA1 Message Date
Mondo Diaz
deb329f4cf Add pagination and search to projects API
- Add PaginatedResponse and PaginationMeta schemas
- Update GET /api/v1/projects to support:
  - page param (default: 1)
  - limit param (default: 20, max: 100)
  - search param (case-insensitive name search)
- Response includes pagination metadata (page, limit, total, total_pages)
- Add Python cache files to gitignore
2025-12-11 13:39:54 -06:00
8 changed files with 228 additions and 603 deletions

8
.gitignore vendored
View File

@@ -1,3 +1,11 @@
# Python
__pycache__/
*.py[cod]
*.pyo
.Python
*.egg-info/
.eggs/
# Binaries
/bin/
*.exe

View File

@@ -1,8 +1,9 @@
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Request
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Request, Query
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from sqlalchemy import or_
from sqlalchemy import or_, func
from typing import List, Optional
import math
import re
from .database import get_db
@@ -16,6 +17,7 @@ from .schemas import (
UploadResponse,
ConsumerResponse,
HealthResponse,
PaginatedResponse, PaginationMeta,
)
router = APIRouter()
@@ -39,13 +41,44 @@ def health_check():
# Project routes
@router.get("/api/v1/projects", response_model=List[ProjectResponse])
def list_projects(request: Request, db: Session = Depends(get_db)):
@router.get("/api/v1/projects", response_model=PaginatedResponse[ProjectResponse])
def list_projects(
request: Request,
page: int = Query(default=1, ge=1, description="Page number"),
limit: int = Query(default=20, ge=1, le=100, description="Items per page"),
search: Optional[str] = Query(default=None, description="Search by project name"),
db: Session = Depends(get_db),
):
user_id = get_user_id(request)
projects = db.query(Project).filter(
# Base query - filter by access
query = db.query(Project).filter(
or_(Project.is_public == True, Project.created_by == user_id)
).order_by(Project.name).all()
return projects
)
# Apply search filter (case-insensitive)
if search:
query = query.filter(func.lower(Project.name).contains(search.lower()))
# Get total count before pagination
total = query.count()
# Apply pagination
offset = (page - 1) * limit
projects = query.order_by(Project.name).offset(offset).limit(limit).all()
# Calculate total pages
total_pages = math.ceil(total / limit) if total > 0 else 1
return PaginatedResponse(
items=projects,
pagination=PaginationMeta(
page=page,
limit=limit,
total=total,
total_pages=total_pages,
),
)
@router.post("/api/v1/projects", response_model=ProjectResponse)

View File

@@ -1,8 +1,23 @@
from datetime import datetime
from typing import Optional, List
from typing import Optional, List, Generic, TypeVar
from pydantic import BaseModel
from uuid import UUID
T = TypeVar("T")
# Pagination schemas
class PaginationMeta(BaseModel):
page: int
limit: int
total: int
total_pages: int
class PaginatedResponse(BaseModel, Generic[T]):
items: List[T]
pagination: PaginationMeta
# Project schemas
class ProjectCreate(BaseModel):

View File

@@ -2,174 +2,64 @@
min-height: 100vh;
display: flex;
flex-direction: column;
background: var(--bg-primary);
}
/* Header */
.header {
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-primary);
padding: 0;
position: sticky;
top: 0;
z-index: 100;
backdrop-filter: blur(12px);
background: rgba(17, 17, 19, 0.85);
background-color: var(--primary);
color: white;
padding: 1rem 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
height: 64px;
}
/* Logo */
.logo {
display: flex;
align-items: center;
gap: 12px;
color: var(--text-primary);
font-size: 1.25rem;
font-weight: 600;
gap: 0.5rem;
color: white;
font-size: 1.5rem;
font-weight: bold;
text-decoration: none;
transition: opacity var(--transition-fast);
}
.logo:hover {
opacity: 0.9;
color: var(--text-primary);
text-decoration: none;
}
.logo-icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: var(--accent-gradient);
border-radius: var(--radius-md);
color: white;
box-shadow: var(--shadow-glow);
font-size: 2rem;
}
.logo-text {
letter-spacing: -0.02em;
}
/* Navigation */
.nav {
display: flex;
gap: 8px;
gap: 1.5rem;
}
.nav a {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
color: var(--text-primary);
font-size: 0.875rem;
font-weight: 500;
border-radius: var(--radius-md);
transition: all var(--transition-fast);
color: white;
opacity: 0.9;
}
.nav a:hover {
color: var(--text-primary);
background: var(--bg-hover);
}
.nav a.active {
color: var(--accent-primary);
background: rgba(16, 185, 129, 0.1);
}
.nav a svg {
opacity: 0.8;
}
.nav a:hover svg,
.nav a.active svg {
opacity: 1;
text-decoration: none;
}
.nav-link-muted {
opacity: 0.7;
}
/* Main content */
.main {
flex: 1;
padding: 32px 0 64px;
padding: 2rem 0;
}
/* Footer */
.footer {
background: var(--bg-secondary);
border-top: 1px solid var(--border-primary);
padding: 24px 0;
}
.footer-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.footer-brand {
display: flex;
align-items: center;
gap: 12px;
}
.footer-logo {
font-weight: 600;
color: var(--text-primary);
}
.footer-tagline {
color: var(--text-secondary);
font-size: 0.875rem;
}
.footer-links {
display: flex;
gap: 24px;
}
.footer-links a {
color: var(--text-secondary);
font-size: 0.875rem;
transition: color var(--transition-fast);
}
.footer-links a:hover {
color: var(--text-primary);
}
/* Responsive */
@media (max-width: 640px) {
.header-content {
height: 56px;
}
.logo-text {
display: none;
}
.nav a span {
display: none;
}
.footer-content {
flex-direction: column;
gap: 16px;
background-color: var(--primary-dark);
color: white;
padding: 1rem 0;
text-align: center;
}
.footer-brand {
flex-direction: column;
gap: 4px;
}
opacity: 0.9;
font-size: 0.875rem;
}

View File

@@ -1,5 +1,5 @@
import { ReactNode } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Link } from 'react-router-dom';
import './Layout.css';
interface LayoutProps {
@@ -7,48 +7,16 @@ interface LayoutProps {
}
function Layout({ children }: LayoutProps) {
const location = useLocation();
return (
<div className="layout">
<header className="header">
<div className="container header-content">
<Link to="/" className="logo">
<div className="logo-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
{/* Three trees representing an orchard */}
{/* Left tree */}
<ellipse cx="6" cy="9" rx="4" ry="5" fill="currentColor" opacity="0.7"/>
<rect x="5" y="13" width="2" height="5" fill="currentColor"/>
{/* Center tree (larger) */}
<ellipse cx="12" cy="7" rx="5" ry="6" fill="currentColor"/>
<rect x="11" y="12" width="2" height="6" fill="currentColor"/>
{/* Right tree */}
<ellipse cx="18" cy="9" rx="4" ry="5" fill="currentColor" opacity="0.7"/>
<rect x="17" y="13" width="2" height="5" fill="currentColor"/>
{/* Ground line */}
<line x1="2" y1="18" x2="22" y2="18" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" opacity="0.5"/>
</svg>
</div>
<span className="logo-icon">🌳</span>
<span className="logo-text">Orchard</span>
</Link>
<nav className="nav">
<Link to="/" className={location.pathname === '/' ? 'active' : ''}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9,22 9,12 15,12 15,22"/>
</svg>
Projects
</Link>
<a href="/docs" className="nav-link-muted">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14,2 14,8 20,8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
</svg>
Docs
</a>
<Link to="/">Groves</Link>
</nav>
</div>
</header>
@@ -58,15 +26,8 @@ function Layout({ children }: LayoutProps) {
</div>
</main>
<footer className="footer">
<div className="container footer-content">
<div className="footer-brand">
<span className="footer-logo">Orchard</span>
<span className="footer-tagline">Content-Addressable Storage</span>
</div>
<div className="footer-links">
<a href="/docs">Documentation</a>
<a href="/api/v1">API</a>
</div>
<div className="container">
<p>Orchard - Content-Addressable Storage System</p>
</div>
</footer>
</div>

View File

@@ -5,97 +5,34 @@
}
:root {
/* Dark mode color palette */
--bg-primary: #0a0a0b;
--bg-secondary: #111113;
--bg-tertiary: #1a1a1d;
--bg-elevated: #222225;
--bg-hover: #2a2a2e;
/* Accent colors - Green/Emerald theme */
--accent-primary: #10b981;
--accent-primary-hover: #34d399;
--accent-secondary: #059669;
--accent-gradient: linear-gradient(135deg, #10b981 0%, #059669 100%);
/* Text colors - improved contrast */
--text-primary: #f9fafb;
--text-secondary: #d1d5db;
--text-tertiary: #9ca3af;
--text-muted: #6b7280;
/* Border colors */
--border-primary: #27272a;
--border-secondary: #3f3f46;
--border-accent: #10b981;
/* Status colors */
--success: #22c55e;
--success-bg: rgba(34, 197, 94, 0.1);
--error: #ef4444;
--error-bg: rgba(239, 68, 68, 0.1);
--warning: #f59e0b;
--warning-bg: rgba(245, 158, 11, 0.1);
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -4px rgba(0, 0, 0, 0.4);
--shadow-glow: 0 0 20px rgba(16, 185, 129, 0.3);
/* Transitions */
--transition-fast: 150ms ease;
--transition-normal: 250ms ease;
--transition-slow: 350ms ease;
/* Border radius */
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
}
html {
color-scheme: dark;
--primary: #2d5a27;
--primary-light: #4a8c3f;
--primary-dark: #1e3d1a;
--secondary: #8b4513;
--background: #f5f5f0;
--surface: #ffffff;
--text: #333333;
--text-light: #666666;
--border: #e0e0e0;
--success: #28a745;
--error: #dc3545;
--warning: #ffc107;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background-color: var(--background);
color: var(--text);
line-height: 1.6;
font-size: 14px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--border-secondary);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-tertiary);
}
a {
color: var(--accent-primary);
color: var(--primary);
text-decoration: none;
transition: color var(--transition-fast);
}
a:hover {
color: var(--accent-primary-hover);
text-decoration: underline;
}
button {
@@ -104,13 +41,7 @@ button {
}
.container {
max-width: 1280px;
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
}
/* Selection */
::selection {
background: var(--accent-primary);
color: white;
padding: 0 20px;
}

View File

@@ -1,4 +1,3 @@
/* Page Layout */
.home {
max-width: 1000px;
margin: 0 auto;
@@ -8,92 +7,71 @@
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
margin-bottom: 2rem;
}
.page-header h1 {
font-size: 2rem;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.02em;
color: var(--primary-dark);
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
padding: 0.625rem 1.25rem;
border: none;
border-radius: var(--radius-md);
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
transition: all var(--transition-fast);
transition: all 0.2s;
}
.btn-primary {
background: var(--accent-gradient);
background-color: var(--primary);
color: white;
box-shadow: var(--shadow-sm), 0 0 20px rgba(16, 185, 129, 0.2);
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-md), 0 0 30px rgba(16, 185, 129, 0.3);
}
.btn-primary:active {
transform: translateY(0);
background-color: var(--primary-dark);
}
.btn-primary:disabled {
opacity: 0.5;
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-secondary {
background: var(--bg-tertiary);
color: var(--text-secondary);
border: 1px solid var(--border-primary);
background-color: var(--border);
color: var(--text);
}
.btn-secondary:hover {
background: var(--bg-hover);
color: var(--text-primary);
border-color: var(--border-secondary);
background-color: #d0d0d0;
}
/* Cards */
.card {
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: var(--radius-lg);
padding: 24px;
transition: all var(--transition-normal);
background-color: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1.5rem;
}
/* Forms */
.form {
margin-bottom: 32px;
margin-bottom: 2rem;
}
.form h3 {
margin-bottom: 20px;
color: var(--text-primary);
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 1rem;
color: var(--primary-dark);
}
.form-group {
margin-bottom: 16px;
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 8px;
margin-bottom: 0.375rem;
font-weight: 500;
color: var(--text-secondary);
color: var(--text-light);
font-size: 0.875rem;
}
@@ -102,138 +80,82 @@
.form-group select,
.form-group textarea {
width: 100%;
padding: 12px 16px;
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
border-radius: var(--radius-md);
padding: 0.625rem;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 0.875rem;
color: var(--text-primary);
transition: all var(--transition-fast);
}
.form-group input::placeholder {
color: var(--text-muted);
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.15);
background: var(--bg-elevated);
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(45, 90, 39, 0.1);
}
.form-group.checkbox label {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
color: var(--text-primary);
}
.form-group.checkbox input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: var(--accent-primary);
gap: 0.5rem;
cursor: pointer;
}
/* Messages */
.form-group.checkbox input {
width: auto;
}
.error-message {
background: var(--error-bg);
border: 1px solid rgba(239, 68, 68, 0.2);
background-color: #fef2f2;
border: 1px solid #fecaca;
color: var(--error);
padding: 12px 16px;
border-radius: var(--radius-md);
margin-bottom: 16px;
font-size: 0.875rem;
padding: 0.75rem 1rem;
border-radius: 6px;
margin-bottom: 1rem;
}
.success-message {
background: var(--success-bg);
border: 1px solid rgba(34, 197, 94, 0.2);
color: var(--success);
padding: 12px 16px;
border-radius: var(--radius-md);
margin-bottom: 16px;
font-size: 0.875rem;
}
/* Loading */
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 64px;
color: var(--text-tertiary);
font-size: 0.875rem;
text-align: center;
padding: 3rem;
color: var(--text-light);
}
/* Empty State */
.empty-state {
text-align: center;
padding: 64px 32px;
color: var(--text-tertiary);
background: var(--bg-secondary);
border: 1px dashed var(--border-secondary);
border-radius: var(--radius-lg);
padding: 3rem;
color: var(--text-light);
background-color: var(--surface);
border: 1px dashed var(--border);
border-radius: 8px;
}
.empty-state p {
font-size: 0.9375rem;
}
/* Project Grid */
.project-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
gap: 1rem;
}
.project-card {
display: block;
color: inherit;
position: relative;
overflow: hidden;
}
.project-card::before {
content: '';
position: absolute;
inset: 0;
background: var(--accent-gradient);
opacity: 0;
transition: opacity var(--transition-normal);
border-radius: var(--radius-lg);
transition: transform 0.2s, box-shadow 0.2s;
}
.project-card:hover {
border-color: var(--border-secondary);
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
color: inherit;
}
.project-card:hover::before {
opacity: 0.03;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
text-decoration: none;
}
.project-card h3 {
color: var(--text-primary);
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 8px;
color: var(--primary);
margin-bottom: 0.5rem;
}
.project-card p {
color: var(--text-secondary);
color: var(--text-light);
font-size: 0.875rem;
margin-bottom: 16px;
line-height: 1.5;
margin-bottom: 1rem;
}
.project-meta {
@@ -241,63 +163,24 @@
justify-content: space-between;
align-items: center;
font-size: 0.75rem;
padding-top: 16px;
border-top: 1px solid var(--border-primary);
margin-top: auto;
}
/* Badges */
.badge {
padding: 4px 10px;
border-radius: 100px;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-weight: 500;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.02em;
}
.badge-public {
background: var(--success-bg);
color: var(--success);
border: 1px solid rgba(34, 197, 94, 0.2);
background-color: #dcfce7;
color: #166534;
}
.badge-private {
background: var(--warning-bg);
color: var(--warning);
border: 1px solid rgba(245, 158, 11, 0.2);
background-color: #fef3c7;
color: #92400e;
}
.date {
color: var(--text-muted);
}
/* Breadcrumb */
.breadcrumb {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 24px;
font-size: 0.875rem;
color: var(--text-tertiary);
}
.breadcrumb a {
color: var(--text-secondary);
transition: color var(--transition-fast);
}
.breadcrumb a:hover {
color: var(--accent-primary);
}
.breadcrumb span {
color: var(--text-primary);
font-weight: 500;
}
.description {
color: var(--text-secondary);
margin-top: 4px;
font-size: 0.9375rem;
color: var(--text-light);
}

View File

@@ -1,32 +1,44 @@
/* Upload Section */
.breadcrumb {
margin-bottom: 1rem;
font-size: 0.875rem;
color: var(--text-light);
}
.breadcrumb a {
color: var(--primary);
}
.breadcrumb span {
color: var(--text);
font-weight: 500;
}
.description {
color: var(--text-light);
margin-top: 0.25rem;
}
.success-message {
background-color: #f0fdf4;
border: 1px solid #bbf7d0;
color: var(--success);
padding: 0.75rem 1rem;
border-radius: 6px;
margin-bottom: 1rem;
}
.upload-section {
margin-bottom: 32px;
background: linear-gradient(135deg, rgba(16, 185, 129, 0.05) 0%, rgba(5, 150, 105, 0.05) 100%);
border: 1px solid rgba(16, 185, 129, 0.2);
margin-bottom: 2rem;
}
.upload-section h3 {
margin-bottom: 20px;
color: var(--text-primary);
font-size: 1rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.upload-section h3::before {
content: '';
display: block;
width: 4px;
height: 20px;
background: var(--accent-gradient);
border-radius: 2px;
margin-bottom: 1rem;
color: var(--primary-dark);
}
.upload-form {
display: flex;
gap: 16px;
gap: 1rem;
align-items: flex-end;
flex-wrap: wrap;
}
@@ -37,42 +49,17 @@
min-width: 200px;
}
.upload-form .form-group input[type="file"] {
padding: 10px 16px;
background: var(--bg-tertiary);
cursor: pointer;
}
.upload-form .form-group input[type="file"]::file-selector-button {
background: var(--accent-gradient);
color: white;
border: none;
padding: 6px 12px;
border-radius: var(--radius-sm);
margin-right: 12px;
cursor: pointer;
font-weight: 500;
font-size: 0.8125rem;
}
/* Section Headers */
h2 {
margin-bottom: 16px;
color: var(--text-primary);
font-size: 1.25rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 1rem;
color: var(--primary-dark);
}
/* Tags Table */
.tags-table {
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: var(--radius-lg);
background-color: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
margin-bottom: 32px;
margin-bottom: 2rem;
}
.tags-table table {
@@ -82,146 +69,63 @@ h2 {
.tags-table th,
.tags-table td {
padding: 14px 20px;
padding: 0.875rem 1rem;
text-align: left;
border-bottom: 1px solid var(--border-primary);
border-bottom: 1px solid var(--border);
}
.tags-table th {
background: var(--bg-tertiary);
background-color: #f9f9f9;
font-weight: 600;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-tertiary);
color: var(--text-light);
}
.tags-table tr:last-child td {
border-bottom: none;
}
.tags-table tbody tr {
transition: background var(--transition-fast);
}
.tags-table tbody tr:hover {
background: var(--bg-tertiary);
}
.tags-table td strong {
color: var(--accent-primary);
font-weight: 600;
.tags-table tr:hover {
background-color: #f9f9f9;
}
.artifact-id {
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
font-size: 0.8125rem;
color: var(--text-tertiary);
background: var(--bg-tertiary);
padding: 4px 8px;
border-radius: var(--radius-sm);
font-family: monospace;
font-size: 0.875rem;
color: var(--text-light);
}
.btn-small {
padding: 6px 12px;
padding: 0.375rem 0.75rem;
font-size: 0.75rem;
}
/* Usage Section */
.usage-section {
margin-top: 32px;
background: var(--bg-secondary);
margin-top: 2rem;
}
.usage-section h3 {
margin-bottom: 12px;
color: var(--text-primary);
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--primary-dark);
}
.usage-section p {
color: var(--text-secondary);
margin-bottom: 12px;
color: var(--text-light);
margin-bottom: 0.5rem;
font-size: 0.875rem;
}
.usage-section pre {
background: #0d0d0f;
border: 1px solid var(--border-primary);
padding: 16px 20px;
border-radius: var(--radius-md);
background-color: #1e1e1e;
color: #d4d4d4;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
margin-bottom: 16px;
margin-bottom: 1rem;
}
.usage-section code {
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
font-size: 0.8125rem;
color: #e2e8f0;
}
/* Syntax highlighting for code blocks */
.usage-section pre {
position: relative;
}
.usage-section pre::before {
content: 'bash';
position: absolute;
top: 8px;
right: 12px;
font-size: 0.6875rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Copy button for code blocks (optional enhancement) */
.code-block {
position: relative;
}
.code-block .copy-btn {
position: absolute;
top: 8px;
right: 8px;
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
color: var(--text-secondary);
padding: 4px 8px;
border-radius: var(--radius-sm);
font-size: 0.6875rem;
cursor: pointer;
opacity: 0;
transition: opacity var(--transition-fast);
}
.code-block:hover .copy-btn {
opacity: 1;
}
.code-block .copy-btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.upload-form {
flex-direction: column;
align-items: stretch;
}
.upload-form .form-group {
min-width: 100%;
}
.tags-table {
overflow-x: auto;
}
.tags-table table {
min-width: 500px;
}
font-family: 'Fira Code', 'Consolas', monospace;
font-size: 0.875rem;
}