Initial artdag-common shared library
Shared components for L1 and L2 servers: - Jinja2 template system with base template and components - Middleware for auth and content negotiation - Pydantic models for requests/responses - Utility functions for pagination, media, formatting - Constants for Tailwind/HTMX/Cytoscape CDNs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
71
artdag_common/templates/base.html
Normal file
71
artdag_common/templates/base.html
Normal file
@@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Art-DAG{% endblock %}</title>
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="{{ TAILWIND_CDN }}"></script>
|
||||
{{ TAILWIND_CONFIG | safe }}
|
||||
|
||||
<!-- HTMX -->
|
||||
<script src="{{ HTMX_CDN }}"></script>
|
||||
|
||||
{% block head %}{% endblock %}
|
||||
|
||||
<style>
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: #1f2937;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #4b5563;
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #6b7280;
|
||||
}
|
||||
|
||||
/* HTMX loading indicator */
|
||||
.htmx-request .htmx-indicator {
|
||||
opacity: 1;
|
||||
}
|
||||
.htmx-indicator {
|
||||
opacity: 0;
|
||||
transition: opacity 200ms ease-in;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-dark-800 text-gray-100 min-h-screen">
|
||||
{% block nav %}
|
||||
<nav class="bg-dark-700 border-b border-dark-600">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<div class="flex items-center space-x-8">
|
||||
<a href="/" class="text-xl font-bold text-white">
|
||||
{% block brand %}Art-DAG{% endblock %}
|
||||
</a>
|
||||
{% block nav_items %}{% endblock %}
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
{% block nav_right %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
<main class="container mx-auto px-4 py-8">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
{% block footer %}{% endblock %}
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
64
artdag_common/templates/components/badge.html
Normal file
64
artdag_common/templates/components/badge.html
Normal file
@@ -0,0 +1,64 @@
|
||||
{#
|
||||
Badge component for status and type indicators.
|
||||
|
||||
Usage:
|
||||
{% from "components/badge.html" import badge, status_badge, type_badge %}
|
||||
|
||||
{{ badge("Active", "green") }}
|
||||
{{ status_badge("completed") }}
|
||||
{{ type_badge("EFFECT") }}
|
||||
#}
|
||||
|
||||
{% macro badge(text, color="gray", class="") %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-{{ color }}-600/20 text-{{ color }}-400 {{ class }}">
|
||||
{{ text }}
|
||||
</span>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro status_badge(status, class="") %}
|
||||
{% set colors = {
|
||||
"completed": "green",
|
||||
"cached": "blue",
|
||||
"running": "yellow",
|
||||
"pending": "gray",
|
||||
"failed": "red",
|
||||
"active": "green",
|
||||
"inactive": "gray",
|
||||
} %}
|
||||
{% set color = colors.get(status, "gray") %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-{{ color }}-600/20 text-{{ color }}-400 {{ class }}">
|
||||
{% if status == "running" %}
|
||||
<svg class="animate-spin -ml-0.5 mr-1.5 h-3 w-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
{% endif %}
|
||||
{{ status | capitalize }}
|
||||
</span>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro type_badge(node_type, class="") %}
|
||||
{% set colors = {
|
||||
"SOURCE": "blue",
|
||||
"EFFECT": "green",
|
||||
"OUTPUT": "purple",
|
||||
"ANALYSIS": "amber",
|
||||
"_LIST": "indigo",
|
||||
} %}
|
||||
{% set color = colors.get(node_type, "gray") %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-{{ color }}-600/20 text-{{ color }}-400 {{ class }}">
|
||||
{{ node_type }}
|
||||
</span>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro role_badge(role, class="") %}
|
||||
{% set colors = {
|
||||
"input": "blue",
|
||||
"output": "purple",
|
||||
"intermediate": "gray",
|
||||
} %}
|
||||
{% set color = colors.get(role, "gray") %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-{{ color }}-600/20 text-{{ color }}-400 {{ class }}">
|
||||
{{ role | capitalize }}
|
||||
</span>
|
||||
{% endmacro %}
|
||||
45
artdag_common/templates/components/card.html
Normal file
45
artdag_common/templates/components/card.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{#
|
||||
Card component for displaying information.
|
||||
|
||||
Usage:
|
||||
{% include "components/card.html" with title="Status", content="Active", class="col-span-2" %}
|
||||
|
||||
Or as a block:
|
||||
{% call card(title="Details") %}
|
||||
<p>Card content here</p>
|
||||
{% endcall %}
|
||||
#}
|
||||
|
||||
{% macro card(title=None, class="") %}
|
||||
<div class="bg-dark-600 rounded-lg p-4 {{ class }}">
|
||||
{% if title %}
|
||||
<h3 class="text-sm font-medium text-gray-400 mb-2">{{ title }}</h3>
|
||||
{% endif %}
|
||||
<div class="text-white">
|
||||
{{ caller() if caller else "" }}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro stat_card(title, value, color="white", class="") %}
|
||||
<div class="bg-dark-600 rounded-lg p-4 text-center {{ class }}">
|
||||
<div class="text-2xl font-bold text-{{ color }}-400">{{ value }}</div>
|
||||
<div class="text-sm text-gray-400">{{ title }}</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro info_card(title, items, class="") %}
|
||||
<div class="bg-dark-600 rounded-lg p-4 {{ class }}">
|
||||
{% if title %}
|
||||
<h3 class="text-sm font-medium text-gray-400 mb-3">{{ title }}</h3>
|
||||
{% endif %}
|
||||
<dl class="space-y-2">
|
||||
{% for label, value in items %}
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-400">{{ label }}</dt>
|
||||
<dd class="text-white font-mono text-sm">{{ value }}</dd>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
176
artdag_common/templates/components/dag.html
Normal file
176
artdag_common/templates/components/dag.html
Normal file
@@ -0,0 +1,176 @@
|
||||
{#
|
||||
Cytoscape.js DAG visualization component.
|
||||
|
||||
Usage:
|
||||
{% from "components/dag.html" import dag_container, dag_scripts, dag_legend %}
|
||||
|
||||
{# In head block #}
|
||||
{{ dag_scripts() }}
|
||||
|
||||
{# In content #}
|
||||
{{ dag_container(id="plan-dag", height="400px") }}
|
||||
{{ dag_legend() }}
|
||||
|
||||
{# In scripts block #}
|
||||
<script>
|
||||
initDag('plan-dag', {{ nodes | tojson }}, {{ edges | tojson }});
|
||||
</script>
|
||||
#}
|
||||
|
||||
{% macro dag_scripts() %}
|
||||
<script src="{{ CYTOSCAPE_CDN }}"></script>
|
||||
<script src="{{ DAGRE_CDN }}"></script>
|
||||
<script src="{{ CYTOSCAPE_DAGRE_CDN }}"></script>
|
||||
<script>
|
||||
// Global Cytoscape instance for WebSocket updates
|
||||
window.artdagCy = null;
|
||||
|
||||
function initDag(containerId, nodes, edges) {
|
||||
const nodeColors = {{ NODE_COLORS | tojson }};
|
||||
|
||||
window.artdagCy = cytoscape({
|
||||
container: document.getElementById(containerId),
|
||||
elements: {
|
||||
nodes: nodes,
|
||||
edges: edges
|
||||
},
|
||||
style: [
|
||||
{
|
||||
selector: 'node',
|
||||
style: {
|
||||
'label': 'data(label)',
|
||||
'text-valign': 'center',
|
||||
'text-halign': 'center',
|
||||
'background-color': function(ele) {
|
||||
return nodeColors[ele.data('nodeType')] || nodeColors['default'];
|
||||
},
|
||||
'color': '#fff',
|
||||
'font-size': '10px',
|
||||
'width': 80,
|
||||
'height': 40,
|
||||
'shape': 'round-rectangle',
|
||||
'text-wrap': 'wrap',
|
||||
'text-max-width': '70px',
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node[status="cached"], node[status="completed"]',
|
||||
style: {
|
||||
'border-width': 3,
|
||||
'border-color': '#22c55e'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node[status="running"]',
|
||||
style: {
|
||||
'border-width': 3,
|
||||
'border-color': '#eab308',
|
||||
'border-style': 'dashed'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node:selected',
|
||||
style: {
|
||||
'border-width': 3,
|
||||
'border-color': '#3b82f6'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'width': 2,
|
||||
'line-color': '#6b7280',
|
||||
'target-arrow-color': '#6b7280',
|
||||
'target-arrow-shape': 'triangle',
|
||||
'curve-style': 'bezier'
|
||||
}
|
||||
}
|
||||
],
|
||||
layout: {
|
||||
name: 'dagre',
|
||||
rankDir: 'TB',
|
||||
nodeSep: 50,
|
||||
rankSep: 80,
|
||||
padding: 20
|
||||
},
|
||||
userZoomingEnabled: true,
|
||||
userPanningEnabled: true,
|
||||
boxSelectionEnabled: false
|
||||
});
|
||||
|
||||
// Click handler for node details
|
||||
window.artdagCy.on('tap', 'node', function(evt) {
|
||||
const node = evt.target;
|
||||
const data = node.data();
|
||||
showNodeDetails(data);
|
||||
});
|
||||
|
||||
return window.artdagCy;
|
||||
}
|
||||
|
||||
function showNodeDetails(data) {
|
||||
const panel = document.getElementById('node-details');
|
||||
if (!panel) return;
|
||||
|
||||
panel.innerHTML = `
|
||||
<h4 class="font-medium text-white mb-2">${data.label || data.id}</h4>
|
||||
<dl class="space-y-1 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-400">Type</dt>
|
||||
<dd class="text-white">${data.nodeType || 'Unknown'}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-400">Status</dt>
|
||||
<dd class="text-white">${data.status || 'pending'}</dd>
|
||||
</div>
|
||||
${data.cacheId ? `
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-400">Cache ID</dt>
|
||||
<dd class="text-white font-mono text-xs">${data.cacheId.substring(0, 16)}...</dd>
|
||||
</div>
|
||||
` : ''}
|
||||
${data.level !== undefined ? `
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-400">Level</dt>
|
||||
<dd class="text-white">${data.level}</dd>
|
||||
</div>
|
||||
` : ''}
|
||||
</dl>
|
||||
`;
|
||||
panel.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Future WebSocket support: update node status in real-time
|
||||
function updateNodeStatus(stepId, status, cacheId) {
|
||||
if (!window.artdagCy) return;
|
||||
const node = window.artdagCy.getElementById(stepId);
|
||||
if (node && node.length > 0) {
|
||||
node.data('status', status);
|
||||
if (cacheId) {
|
||||
node.data('cacheId', cacheId);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro dag_container(id="dag-container", height="400px", class="") %}
|
||||
<div id="{{ id }}" class="w-full bg-dark-700 rounded-lg {{ class }}" style="height: {{ height }};"></div>
|
||||
<div id="node-details" class="hidden mt-4 p-4 bg-dark-600 rounded-lg"></div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro dag_legend(node_types=None) %}
|
||||
{% set types = node_types or ["SOURCE", "EFFECT", "_LIST"] %}
|
||||
<div class="flex gap-4 text-sm flex-wrap mt-4">
|
||||
{% for type in types %}
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="w-4 h-4 rounded" style="background-color: {{ NODE_COLORS.get(type, NODE_COLORS.default) }}"></span>
|
||||
{{ type }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="w-4 h-4 rounded border-2 border-green-500 bg-dark-600"></span>
|
||||
Cached
|
||||
</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
98
artdag_common/templates/components/media_preview.html
Normal file
98
artdag_common/templates/components/media_preview.html
Normal file
@@ -0,0 +1,98 @@
|
||||
{#
|
||||
Media preview component for videos, images, and audio.
|
||||
|
||||
Usage:
|
||||
{% from "components/media_preview.html" import media_preview, video_player, image_preview, audio_player %}
|
||||
|
||||
{{ media_preview(content_hash, media_type, title="Preview") }}
|
||||
{{ video_player(src="/cache/abc123/mp4", poster="/cache/abc123/thumb") }}
|
||||
#}
|
||||
|
||||
{% macro media_preview(content_hash, media_type, title=None, class="", show_download=True) %}
|
||||
<div class="bg-dark-600 rounded-lg overflow-hidden {{ class }}">
|
||||
{% if title %}
|
||||
<div class="px-4 py-2 border-b border-dark-500">
|
||||
<h3 class="text-sm font-medium text-gray-400">{{ title }}</h3>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="aspect-video bg-dark-700 flex items-center justify-center">
|
||||
{% if media_type == "video" %}
|
||||
{{ video_player("/cache/" + content_hash + "/mp4") }}
|
||||
{% elif media_type == "image" %}
|
||||
{{ image_preview("/cache/" + content_hash + "/raw") }}
|
||||
{% elif media_type == "audio" %}
|
||||
{{ audio_player("/cache/" + content_hash + "/raw") }}
|
||||
{% else %}
|
||||
<div class="text-gray-400 text-center p-4">
|
||||
<svg class="w-12 h-12 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
<p>Preview not available</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if show_download %}
|
||||
<div class="px-4 py-2 border-t border-dark-500">
|
||||
<a href="/cache/{{ content_hash }}/raw" download
|
||||
class="text-blue-400 hover:text-blue-300 text-sm">
|
||||
Download original
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro video_player(src, poster=None, autoplay=False, muted=True, loop=False, class="") %}
|
||||
<video
|
||||
class="w-full h-full object-contain {{ class }}"
|
||||
controls
|
||||
playsinline
|
||||
{% if poster %}poster="{{ poster }}"{% endif %}
|
||||
{% if autoplay %}autoplay{% endif %}
|
||||
{% if muted %}muted{% endif %}
|
||||
{% if loop %}loop{% endif %}
|
||||
>
|
||||
<source src="{{ src }}" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro image_preview(src, alt="", class="") %}
|
||||
<img
|
||||
src="{{ src }}"
|
||||
alt="{{ alt }}"
|
||||
class="w-full h-full object-contain {{ class }}"
|
||||
loading="lazy"
|
||||
>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro audio_player(src, class="") %}
|
||||
<div class="w-full px-4 {{ class }}">
|
||||
<audio controls class="w-full">
|
||||
<source src="{{ src }}">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro thumbnail(content_hash, media_type, size="w-24 h-24", class="") %}
|
||||
<div class="bg-dark-700 rounded {{ size }} flex items-center justify-center overflow-hidden {{ class }}">
|
||||
{% if media_type == "image" %}
|
||||
<img src="/cache/{{ content_hash }}/raw" alt="" class="w-full h-full object-cover" loading="lazy">
|
||||
{% elif media_type == "video" %}
|
||||
<svg class="w-8 h-8 text-gray-400" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
</svg>
|
||||
{% elif media_type == "audio" %}
|
||||
<svg class="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"/>
|
||||
</svg>
|
||||
{% else %}
|
||||
<svg class="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
82
artdag_common/templates/components/pagination.html
Normal file
82
artdag_common/templates/components/pagination.html
Normal file
@@ -0,0 +1,82 @@
|
||||
{#
|
||||
Pagination component with HTMX infinite scroll support.
|
||||
|
||||
Usage:
|
||||
{% from "components/pagination.html" import infinite_scroll_trigger, page_links %}
|
||||
|
||||
{# Infinite scroll (HTMX) #}
|
||||
{{ infinite_scroll_trigger(url="/items?page=2", colspan=3, has_more=True) }}
|
||||
|
||||
{# Traditional pagination #}
|
||||
{{ page_links(current_page=1, total_pages=5, base_url="/items") }}
|
||||
#}
|
||||
|
||||
{% macro infinite_scroll_trigger(url, colspan=1, has_more=True, target=None) %}
|
||||
{% if has_more %}
|
||||
<tr hx-get="{{ url }}"
|
||||
hx-trigger="revealed"
|
||||
hx-swap="afterend"
|
||||
{% if target %}hx-target="{{ target }}"{% endif %}
|
||||
class="htmx-indicator-row">
|
||||
<td colspan="{{ colspan }}" class="text-center py-4">
|
||||
<span class="text-gray-400 htmx-indicator">
|
||||
<svg class="animate-spin h-5 w-5 inline mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Loading more...
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro page_links(current_page, total_pages, base_url, class="") %}
|
||||
<nav class="flex items-center justify-center space-x-2 {{ class }}">
|
||||
{# Previous button #}
|
||||
{% if current_page > 1 %}
|
||||
<a href="{{ base_url }}?page={{ current_page - 1 }}"
|
||||
class="px-3 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors">
|
||||
← Previous
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="px-3 py-2 rounded-lg bg-dark-700 text-gray-500 cursor-not-allowed">
|
||||
← Previous
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{# Page numbers #}
|
||||
<div class="flex items-center space-x-1">
|
||||
{% for page in range(1, total_pages + 1) %}
|
||||
{% if page == current_page %}
|
||||
<span class="px-3 py-2 rounded-lg bg-blue-600 text-white">{{ page }}</span>
|
||||
{% elif page == 1 or page == total_pages or (page >= current_page - 2 and page <= current_page + 2) %}
|
||||
<a href="{{ base_url }}?page={{ page }}"
|
||||
class="px-3 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors">
|
||||
{{ page }}
|
||||
</a>
|
||||
{% elif page == current_page - 3 or page == current_page + 3 %}
|
||||
<span class="px-2 text-gray-500">...</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Next button #}
|
||||
{% if current_page < total_pages %}
|
||||
<a href="{{ base_url }}?page={{ current_page + 1 }}"
|
||||
class="px-3 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors">
|
||||
Next →
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="px-3 py-2 rounded-lg bg-dark-700 text-gray-500 cursor-not-allowed">
|
||||
Next →
|
||||
</span>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro page_info(page, limit, total) %}
|
||||
<div class="text-sm text-gray-400">
|
||||
Showing {{ (page - 1) * limit + 1 }}-{{ [page * limit, total] | min }} of {{ total }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
51
artdag_common/templates/components/table.html
Normal file
51
artdag_common/templates/components/table.html
Normal file
@@ -0,0 +1,51 @@
|
||||
{#
|
||||
Table component with dark theme styling.
|
||||
|
||||
Usage:
|
||||
{% from "components/table.html" import table, table_row %}
|
||||
|
||||
{% call table(columns=["Name", "Status", "Actions"]) %}
|
||||
{% for item in items %}
|
||||
{{ table_row([item.name, item.status, actions_html]) }}
|
||||
{% endfor %}
|
||||
{% endcall %}
|
||||
#}
|
||||
|
||||
{% macro table(columns, class="", id="") %}
|
||||
<div class="overflow-x-auto {{ class }}" {% if id %}id="{{ id }}"{% endif %}>
|
||||
<table class="w-full text-sm">
|
||||
<thead class="text-gray-400 border-b border-dark-600">
|
||||
<tr>
|
||||
{% for col in columns %}
|
||||
<th class="text-left py-3 px-4 font-medium">{{ col }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-dark-600">
|
||||
{{ caller() }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro table_row(cells, class="", href=None) %}
|
||||
<tr class="hover:bg-dark-600/50 transition-colors {{ class }}">
|
||||
{% for cell in cells %}
|
||||
<td class="py-3 px-4">
|
||||
{% if href and loop.first %}
|
||||
<a href="{{ href }}" class="text-blue-400 hover:text-blue-300">{{ cell }}</a>
|
||||
{% else %}
|
||||
{{ cell | safe }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro empty_row(colspan, message="No items found") %}
|
||||
<tr>
|
||||
<td colspan="{{ colspan }}" class="py-8 text-center text-gray-400">
|
||||
{{ message }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endmacro %}
|
||||
Reference in New Issue
Block a user