Add new component system for buttons, cards, and inputs

Integrates Django Cotton to replace existing UI components with a new templating system. Adds a test page to compare the new components against the old ones.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: dcfff319-6e91-4220-98a9-8295b87755b7
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
This commit is contained in:
pac7
2025-09-21 17:02:08 +00:00
committed by pacnpal
parent cfa7019a7c
commit 7f96e85914
9 changed files with 850 additions and 1 deletions

View File

@@ -87,6 +87,7 @@ THIRD_PARTY_APPS = [
"whitenoise",
"django_tailwind_cli",
"autocomplete", # Django HTMX Autocomplete
"django_cotton", # Django Cotton for component templates
"health_check", # Health checks
"health_check.db",
"health_check.cache",

View File

@@ -62,6 +62,7 @@ dependencies = [
"djangorestframework-simplejwt>=5.5.1",
"django-forwardemail>=1.0.0",
"django-cloudflareimages-toolkit>=1.0.6",
"django-cotton>=2.1.3",
]
[dependency-groups]

View File

@@ -0,0 +1,72 @@
{% comment %}
Button Component - Django Cotton Version of shadcn/ui Button
Usage: <c-button variant="default" size="default">Click me</c-button>
Preserves EXACT same HTML output as the original include version
{% endcomment %}
<c-vars
variant="default"
size="default"
icon_left=""
icon_right=""
type=""
onclick=""
hx_get=""
hx_post=""
hx_target=""
hx_swap=""
x_data=""
x_on=""
disabled=""
/>
<button
class="
inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium
ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2
focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50
{% if variant == 'default' %}
bg-primary text-primary-foreground hover:bg-primary/90
{% elif variant == 'destructive' %}
bg-destructive text-destructive-foreground hover:bg-destructive/90
{% elif variant == 'outline' %}
border border-input bg-background hover:bg-accent hover:text-accent-foreground
{% elif variant == 'secondary' %}
bg-secondary text-secondary-foreground hover:bg-secondary/80
{% elif variant == 'ghost' %}
hover:bg-accent hover:text-accent-foreground
{% elif variant == 'link' %}
text-primary underline-offset-4 hover:underline
{% endif %}
{% if size == 'default' %}
h-10 px-4 py-2
{% elif size == 'sm' %}
h-9 rounded-md px-3
{% elif size == 'lg' %}
h-11 rounded-md px-8
{% elif size == 'icon' %}
h-10 w-10
{% endif %}
{{ class|default:'' }}
"
{% if type %}type="{{ type }}"{% endif %}
{% if onclick %}onclick="{{ onclick }}"{% endif %}
{% if hx_get %}hx-get="{{ hx_get }}"{% endif %}
{% if hx_post %}hx-post="{{ hx_post }}"{% endif %}
{% if hx_target %}hx-target="{{ hx_target }}"{% endif %}
{% if hx_swap %}hx-swap="{{ hx_swap }}"{% endif %}
{% if x_data %}x-data="{{ x_data }}"{% endif %}
{% if x_on %}{{ x_on }}{% endif %}
{% if disabled %}disabled{% endif %}
{{ attrs }}
>
{% if icon_left %}
<i class="{{ icon_left }} w-4 h-4"></i>
{% endif %}
{{ slot }}
{% if icon_right %}
<i class="{{ icon_right }} w-4 h-4"></i>
{% endif %}
</button>

View File

@@ -0,0 +1,50 @@
{% comment %}
Card Component - Django Cotton Version of shadcn/ui Card
Usage: <c-card title="Card Title" description="Card description">Card content</c-card>
Preserves EXACT same HTML output as the original include version
{% endcomment %}
<c-vars
title=""
description=""
content=""
header_content=""
body_content=""
footer_content=""
/>
<div class="rounded-lg border bg-card text-card-foreground shadow-sm {{ class|default:'' }}">
{% if title or header_content %}
<div class="flex flex-col space-y-1.5 p-6">
{% if title %}
<h3 class="text-2xl font-semibold leading-none tracking-tight">{{ title }}</h3>
{% endif %}
{% if description %}
<p class="text-sm text-muted-foreground">{{ description }}</p>
{% endif %}
{% if header_content %}
{{ header_content|safe }}
{% endif %}
</div>
{% endif %}
{% if content or body_content or slot %}
<div class="p-6 pt-0">
{% if content %}
{{ content|safe }}
{% endif %}
{% if body_content %}
{{ body_content|safe }}
{% endif %}
{% if slot %}
{{ slot }}
{% endif %}
</div>
{% endif %}
{% if footer_content %}
<div class="flex items-center p-6 pt-0">
{{ footer_content|safe }}
</div>
{% endif %}
</div>

View File

@@ -0,0 +1,47 @@
{% comment %}
Input Component - Django Cotton Version of shadcn/ui Input
Usage: <c-input type="text" placeholder="Enter text..." name="field_name" />
Preserves EXACT same HTML output as the original include version
{% endcomment %}
<c-vars
type="text"
name=""
id=""
placeholder=""
value=""
required=""
disabled=""
readonly=""
autocomplete=""
x_model=""
x_on=""
hx_get=""
hx_post=""
hx_target=""
hx_trigger=""
hx_swap=""
hx_include=""
/>
<input
type="{{ type|default:'text' }}"
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 {{ class|default:'' }}"
{% if name %}name="{{ name }}"{% endif %}
{% if id %}id="{{ id }}"{% endif %}
{% if placeholder %}placeholder="{{ placeholder }}"{% endif %}
{% if value %}value="{{ value }}"{% endif %}
{% if required %}required{% endif %}
{% if disabled %}disabled{% endif %}
{% if readonly %}readonly{% endif %}
{% if autocomplete %}autocomplete="{{ autocomplete }}"{% endif %}
{% if x_model %}x-model="{{ x_model }}"{% endif %}
{% if x_on %}{{ x_on }}{% endif %}
{% if hx_get %}hx-get="{{ hx_get }}"{% endif %}
{% if hx_post %}hx-post="{{ hx_post }}"{% endif %}
{% if hx_target %}hx-target="{{ hx_target }}"{% endif %}
{% if hx_trigger %}hx-trigger="{{ hx_trigger }}"{% endif %}
{% if hx_swap %}hx-swap="{{ hx_swap }}"{% endif %}
{% if hx_include %}hx-include="{{ hx_include }}"{% endif %}
{{ attrs }}
/>

View File

@@ -0,0 +1,654 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UI Component Comparison Test</title>
<link href="{% static 'css/tailwind.css' %}" rel="stylesheet">
<style>
.test-section {
margin: 2rem 0;
padding: 1rem;
border: 2px solid #e5e7eb;
border-radius: 8px;
}
.button-pair {
display: flex;
gap: 1rem;
margin: 1rem 0;
align-items: center;
flex-wrap: wrap;
}
.button-container {
border: 1px solid #d1d5db;
padding: 0.5rem;
border-radius: 4px;
position: relative;
}
.button-container::before {
content: attr(data-label);
position: absolute;
top: -10px;
left: 8px;
background: white;
padding: 0 4px;
font-size: 12px;
color: #6b7280;
}
.test-description {
font-weight: bold;
color: #374151;
margin-bottom: 0.5rem;
}
.html-output {
background: #f9fafb;
border: 1px solid #e5e7eb;
padding: 1rem;
margin: 0.5rem 0;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
}
</style>
</head>
<body class="bg-gray-50 p-8">
<div class="max-w-6xl mx-auto">
<h1 class="text-3xl font-bold mb-8 text-center">UI Component Comparison Test</h1>
<p class="text-center text-gray-600 mb-8">Comparing old include method vs new cotton component method for Button, Input, and Card components</p>
<!-- Test 1: All Variants with Default Size -->
<div class="test-section">
<h2 class="text-xl font-semibold mb-4">Test 1: All Variants (Default Size)</h2>
<div class="test-description">Default Variant</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/button.html' with variant='default' text='Default Button' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-button variant="default">Default Button</c-button>
</div>
</div>
<div class="test-description">Destructive Variant</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/button.html' with variant='destructive' text='Delete Item' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-button variant="destructive">Delete Item</c-button>
</div>
</div>
<div class="test-description">Outline Variant</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/button.html' with variant='outline' text='Outline Button' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-button variant="outline">Outline Button</c-button>
</div>
</div>
<div class="test-description">Secondary Variant</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/button.html' with variant='secondary' text='Secondary Button' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-button variant="secondary">Secondary Button</c-button>
</div>
</div>
<div class="test-description">Ghost Variant</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/button.html' with variant='ghost' text='Ghost Button' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-button variant="ghost">Ghost Button</c-button>
</div>
</div>
<div class="test-description">Link Variant</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/button.html' with variant='link' text='Link Button' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-button variant="link">Link Button</c-button>
</div>
</div>
</div>
<!-- Test 2: All Sizes with Default Variant -->
<div class="test-section">
<h2 class="text-xl font-semibold mb-4">Test 2: All Sizes (Default Variant)</h2>
<div class="test-description">Default Size</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/button.html' with size='default' text='Default Size' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-button size="default">Default Size</c-button>
</div>
</div>
<div class="test-description">Small Size</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/button.html' with size='sm' text='Small Size' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-button size="sm">Small Size</c-button>
</div>
</div>
<div class="test-description">Large Size</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/button.html' with size='lg' text='Large Size' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-button size="lg">Large Size</c-button>
</div>
</div>
<div class="test-description">Icon Size</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/button.html' with size='icon' text='🔥' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-button size="icon">🔥</c-button>
</div>
</div>
</div>
<!-- Test 3: HTMX Attributes -->
<div class="test-section">
<h2 class="text-xl font-semibold mb-4">Test 3: HTMX Attributes</h2>
<div class="test-description">hx-get Attribute</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/button.html' with text='Load Content' hx_get='/api/content/' hx_target='#content' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-button hx-get="/api/content/" hx-target="#content">Load Content</c-button>
</div>
</div>
<div class="test-description">hx-post Attribute</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/button.html' with text='Submit Form' hx_post='/api/submit/' hx_target='#result' hx_swap='innerHTML' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-button hx-post="/api/submit/" hx-target="#result" hx-swap="innerHTML">Submit Form</c-button>
</div>
</div>
</div>
<!-- Test 4: Icons -->
<div class="test-section">
<h2 class="text-xl font-semibold mb-4">Test 4: Icons</h2>
<div class="test-description">Left Icon</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/button.html' with text='Save' icon_left='fas fa-save' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-button icon_left="fas fa-save">Save</c-button>
</div>
</div>
<div class="test-description">Right Icon</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/button.html' with text='Next' icon_right='fas fa-arrow-right' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-button icon_right="fas fa-arrow-right">Next</c-button>
</div>
</div>
</div>
<!-- Test 5: Additional Classes and Attributes -->
<div class="test-section">
<h2 class="text-xl font-semibold mb-4">Test 5: Additional Classes and Attributes</h2>
<div class="test-description">Custom Classes</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/button.html' with text='Custom Button' class='border-2 border-red-500 shadow-lg' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-button class="border-2 border-red-500 shadow-lg">Custom Button</c-button>
</div>
</div>
<div class="test-description">Type Attribute</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/button.html' with text='Submit' type='submit' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-button type="submit">Submit</c-button>
</div>
</div>
<div class="test-description">Disabled State</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/button.html' with text='Disabled' disabled=True %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-button disabled>Disabled</c-button>
</div>
</div>
</div>
<!-- Test 6: Complex Combinations -->
<div class="test-section">
<h2 class="text-xl font-semibold mb-4">Test 6: Complex Combinations</h2>
<div class="test-description">Destructive + Large + Icon + HTMX</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/button.html' with variant='destructive' size='lg' text='Delete All' icon_left='fas fa-trash' hx_post='/api/delete-all/' hx_target='#main' class='font-bold' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-button variant="destructive" size="lg" icon_left="fas fa-trash" hx-post="/api/delete-all/" hx-target="#main" class="font-bold">Delete All</c-button>
</div>
</div>
<div class="test-description">Outline + Small + Right Icon + Custom Attributes</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/button.html' with variant='outline' size='sm' text='Export' icon_right='fas fa-download' attrs='data-testid="export-btn" title="Export data"' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-button variant="outline" size="sm" icon_right="fas fa-download" data-testid="export-btn" title="Export data">Export</c-button>
</div>
</div>
</div>
<!-- Test 7: Legacy Underscore Props vs Modern Hyphenated Attributes -->
<div class="test-section">
<h2 class="text-xl font-semibold mb-4">Test 7: Legacy Underscore Props vs Modern Hyphenated Attributes</h2>
<p class="text-sm text-gray-600 mb-4">This test verifies backward compatibility - cotton components should accept both legacy underscore props (hx_get, x_data) and modern hyphenated attributes (hx-get, x-data).</p>
<div class="test-description">Legacy hx_get vs Modern hx-get</div>
<div class="button-pair">
<div class="button-container" data-label="Include with hx_get">
{% include 'components/ui/button.html' with text='Load Content' hx_get='/api/content/' hx_target='#content' %}
</div>
<div class="button-container" data-label="Cotton with hx_get">
<c-button hx_get="/api/content/" hx_target="#content">Load Content</c-button>
</div>
<div class="button-container" data-label="Cotton with hx-get">
<c-button hx-get="/api/content/" hx-target="#content">Load Content</c-button>
</div>
</div>
<div class="test-description">Legacy hx_post vs Modern hx-post</div>
<div class="button-pair">
<div class="button-container" data-label="Include with hx_post">
{% include 'components/ui/button.html' with text='Submit Form' hx_post='/api/submit/' hx_target='#result' hx_swap='innerHTML' %}
</div>
<div class="button-container" data-label="Cotton with hx_post">
<c-button hx_post="/api/submit/" hx_target="#result" hx_swap="innerHTML">Submit Form</c-button>
</div>
<div class="button-container" data-label="Cotton with hx-post">
<c-button hx-post="/api/submit/" hx-target="#result" hx-swap="innerHTML">Submit Form</c-button>
</div>
</div>
<div class="test-description">Legacy x_data vs Modern x-data</div>
<div class="button-pair">
<div class="button-container" data-label="Include with x_data">
{% include 'components/ui/button.html' with text='Alpine Button' x_data='{ clicked: false }' %}
</div>
<div class="button-container" data-label="Cotton with x_data">
<c-button x_data="{ clicked: false }">Alpine Button</c-button>
</div>
<div class="button-container" data-label="Cotton with x-data">
<c-button x-data="{ clicked: false }">Alpine Button</c-button>
</div>
</div>
<div class="test-description">Legacy onclick vs Modern onclick</div>
<div class="button-pair">
<div class="button-container" data-label="Include with onclick">
{% include 'components/ui/button.html' with text='Click Handler' onclick="alert('Clicked!')" %}
</div>
<div class="button-container" data-label="Cotton with onclick">
<c-button onclick="alert('Clicked!')">Click Handler</c-button>
</div>
</div>
<div class="test-description">Legacy type vs Modern type</div>
<div class="button-pair">
<div class="button-container" data-label="Include with type">
{% include 'components/ui/button.html' with text='Submit Form' type='submit' %}
</div>
<div class="button-container" data-label="Cotton with type">
<c-button type="submit">Submit Form</c-button>
</div>
</div>
<div class="test-description">Legacy disabled vs Modern disabled</div>
<div class="button-pair">
<div class="button-container" data-label="Include with disabled">
{% include 'components/ui/button.html' with text='Disabled Button' disabled=True %}
</div>
<div class="button-container" data-label="Cotton with disabled">
<c-button disabled>Disabled Button</c-button>
</div>
</div>
<div class="test-description">Complex Legacy Props Combination</div>
<div class="button-pair">
<div class="button-container" data-label="Include with Legacy Props">
{% include 'components/ui/button.html' with variant='destructive' size='lg' text='Legacy Complex' hx_post='/api/complex/' hx_target='#result' x_data='{ processing: false }' type='submit' %}
</div>
<div class="button-container" data-label="Cotton with Legacy Props">
<c-button variant="destructive" size="lg" hx_post="/api/complex/" hx_target="#result" x_data="{ processing: false }" type="submit">Legacy Complex</c-button>
</div>
<div class="button-container" data-label="Cotton with Modern Props">
<c-button variant="destructive" size="lg" hx-post="/api/complex/" hx-target="#result" x-data="{ processing: false }" type="submit">Modern Complex</c-button>
</div>
</div>
<div class="test-description">Alpine.js x_on Special Case</div>
<div class="button-pair">
<div class="button-container" data-label="Include with x_on">
{% include 'components/ui/button.html' with text='Alpine Click' x_on='@click="clicked = true"' %}
</div>
<div class="button-container" data-label="Cotton with x_on">
<c-button x_on="@click='clicked = true'">Alpine Click</c-button>
</div>
<div class="button-container" data-label="Cotton with @click directly">
<c-button @click="clicked = true">Alpine Click</c-button>
</div>
</div>
</div>
<!-- Test 8: Input Component Tests -->
<div class="test-section">
<h2 class="text-xl font-semibold mb-4">Test 8: Input Component Comparison</h2>
<div class="test-description">Basic Text Input</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/input.html' with type='text' placeholder='Enter your name' name='name' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-input type="text" placeholder="Enter your name" name="name" />
</div>
</div>
<div class="test-description">Email Input with Value</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/input.html' with type='email' placeholder='email@example.com' name='email' value='test@example.com' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-input type="email" placeholder="email@example.com" name="email" value="test@example.com" />
</div>
</div>
<div class="test-description">Password Input</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/input.html' with type='password' placeholder='Password' name='password' required=True %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-input type="password" placeholder="Password" name="password" required />
</div>
</div>
<div class="test-description">Number Input with Min/Max</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/input.html' with type='number' placeholder='Age' name='age' attrs='min="18" max="100"' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-input type="number" placeholder="Age" name="age" min="18" max="100" />
</div>
</div>
<div class="test-description">Disabled Input</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/input.html' with type='text' placeholder='Disabled input' name='disabled' disabled=True value='Cannot edit' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-input type="text" placeholder="Disabled input" name="disabled" disabled value="Cannot edit" />
</div>
</div>
<div class="test-description">Readonly Input</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/input.html' with type='text' name='readonly' readonly=True value='Read only value' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-input type="text" name="readonly" readonly value="Read only value" />
</div>
</div>
<div class="test-description">Input with Custom Class</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/input.html' with type='text' placeholder='Custom styled' name='custom' class='border-blue-500 bg-blue-50' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-input type="text" placeholder="Custom styled" name="custom" class="border-blue-500 bg-blue-50" />
</div>
</div>
<div class="test-description">Input with HTMX Attributes</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/input.html' with type='text' placeholder='Search...' name='search' hx_get='/api/search/' hx_target='#results' hx_trigger='keyup changed delay:300ms' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-input type="text" placeholder="Search..." name="search" hx-get="/api/search/" hx-target="#results" hx-trigger="keyup changed delay:300ms" />
</div>
</div>
<div class="test-description">Input with Alpine.js</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/input.html' with type='text' placeholder='Alpine input' name='alpine' x_model='inputValue' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-input type="text" placeholder="Alpine input" name="alpine" x-model="inputValue" />
</div>
</div>
</div>
<!-- Test 8: Card Component Tests -->
<div class="test-section">
<h2 class="text-xl font-semibold mb-4">Test 8: Card Component Comparison</h2>
<div class="test-description">Basic Card with Title</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/card.html' with title='Basic Card' content='This is a simple card with just a title and content.' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-card title="Basic Card">This is a simple card with just a title and content.</c-card>
</div>
</div>
<div class="test-description">Card with Title and Description</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/card.html' with title='Card with Description' description='This card has both a title and a description' content='Here is the main content of the card.' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-card title="Card with Description" description="This card has both a title and a description">Here is the main content of the card.</c-card>
</div>
</div>
<div class="test-description">Card with Custom Header Content</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/card.html' with header_content='<div class="flex items-center gap-2"><h3 class="text-lg font-bold">Custom Header</h3><span class="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded">New</span></div>' content='This card has custom header content instead of a simple title.' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-card header_content='<div class="flex items-center gap-2"><h3 class="text-lg font-bold">Custom Header</h3><span class="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded">New</span></div>'>This card has custom header content instead of a simple title.</c-card>
</div>
</div>
<div class="test-description">Card with Body Content (instead of slot)</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/card.html' with title='Body Content Card' body_content='<p class="text-gray-600">This content is passed via body_content parameter.</p><button class="mt-2 px-4 py-2 bg-blue-500 text-white rounded">Action Button</button>' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-card title="Body Content Card" body_content='<p class="text-gray-600">This content is passed via body_content parameter.</p><button class="mt-2 px-4 py-2 bg-blue-500 text-white rounded">Action Button</button>'></c-card>
</div>
</div>
<div class="test-description">Card with Footer</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/card.html' with title='Card with Footer' content='This card has footer content at the bottom.' footer_content='<button class="px-4 py-2 bg-gray-100 text-gray-700 rounded mr-2">Cancel</button><button class="px-4 py-2 bg-blue-500 text-white rounded">Save</button>' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-card title="Card with Footer" footer_content='<button class="px-4 py-2 bg-gray-100 text-gray-700 rounded mr-2">Cancel</button><button class="px-4 py-2 bg-blue-500 text-white rounded">Save</button>'>This card has footer content at the bottom.</c-card>
</div>
</div>
<div class="test-description">Complete Card (All Sections)</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/card.html' with title='Complete Card' description='This card has all possible sections' content='Main content goes here. This is the primary content area.' footer_content='<span class="text-sm text-gray-500">Last updated: Today</span>' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-card title="Complete Card" description="This card has all possible sections" footer_content='<span class="text-sm text-gray-500">Last updated: Today</span>'>Main content goes here. This is the primary content area.</c-card>
</div>
</div>
<div class="test-description">Card with Custom Classes</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/card.html' with title='Custom Styled Card' content='This card has custom styling applied.' class='border-2 border-green-200 bg-green-50 shadow-lg' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-card title="Custom Styled Card" class="border-2 border-green-200 bg-green-50 shadow-lg">This card has custom styling applied.</c-card>
</div>
</div>
<div class="test-description">Content-Only Card (No Header)</div>
<div class="button-pair">
<div class="button-container" data-label="Include Version">
{% include 'components/ui/card.html' with content='This card has only content, no header or footer sections.' %}
</div>
<div class="button-container" data-label="Cotton Version">
<c-card>This card has only content, no header or footer sections.</c-card>
</div>
</div>
</div>
<!-- HTML Output Comparison Section -->
<div class="test-section">
<h2 class="text-xl font-semibold mb-4">HTML Output Inspection</h2>
<p class="text-sm text-gray-600 mb-4">Use browser dev tools to inspect the HTML output and ensure it's identical.</p>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div>
<h3 class="font-semibold mb-2">Include Version HTML:</h3>
<div id="include-html" class="html-output">
<!-- JavaScript will populate this -->
</div>
</div>
<div>
<h3 class="font-semibold mb-2">Cotton Version HTML:</h3>
<div id="cotton-html" class="html-output">
<!-- JavaScript will populate this -->
</div>
</div>
</div>
</div>
</div>
<script>
// Function to normalize HTML for comparison
function normalizeHTML(html) {
return html
.replace(/\s+/g, ' ')
.replace(/> </g, '><')
.trim();
}
// Function to extract HTML from all component containers
function extractComponentHTML() {
const containers = document.querySelectorAll('.button-container');
const includeHTMLs = [];
const cottonHTMLs = [];
let componentIndex = 1;
containers.forEach((container, index) => {
const label = container.getAttribute('data-label');
// Look for button, input, or div (card) elements
const element = container.querySelector('button') ||
container.querySelector('input') ||
container.querySelector('div.rounded-lg');
if (element && label) {
const html = element.outerHTML;
const normalized = normalizeHTML(html);
if (label === 'Include Version') {
includeHTMLs.push(`<!-- Component ${componentIndex} -->\n${normalized}\n`);
} else if (label === 'Cotton Version') {
cottonHTMLs.push(`<!-- Component ${componentIndex} -->\n${normalized}\n`);
componentIndex++;
}
}
});
document.getElementById('include-html').textContent = includeHTMLs.join('\n');
document.getElementById('cotton-html').textContent = cottonHTMLs.join('\n');
}
// Extract HTML after page loads
document.addEventListener('DOMContentLoaded', function() {
setTimeout(extractComponentHTML, 100);
});
// Function to compare HTML outputs
function compareHTML() {
const includeHTML = document.getElementById('include-html').textContent;
const cottonHTML = document.getElementById('cotton-html').textContent;
if (includeHTML === cottonHTML) {
alert('✅ HTML outputs are identical!');
} else {
alert('❌ HTML outputs differ. Check the HTML Output section for details.');
console.log('Include HTML:', includeHTML);
console.log('Cotton HTML:', cottonHTML);
}
}
// Add compare button
document.addEventListener('DOMContentLoaded', function() {
const compareBtn = document.createElement('button');
compareBtn.textContent = 'Compare HTML Outputs';
compareBtn.className = 'fixed bottom-4 right-4 bg-blue-500 text-white px-4 py-2 rounded shadow-lg hover:bg-blue-600';
compareBtn.onclick = compareHTML;
document.body.appendChild(compareBtn);
});
</script>
</body>
</html>

View File

@@ -101,6 +101,8 @@ urlpatterns = [
views.environment_and_settings_view,
name="environment_and_settings",
),
# Button component testing
path("test-button/", views.test_button_comparison, name="test_button_comparison"),
]
# Add autocomplete URLs if available

View File

@@ -156,3 +156,11 @@ def environment_and_settings_view(request):
"environment_and_settings.html",
{"env_vars": env_vars, "settings_vars": settings_vars},
)
def test_button_comparison(request):
"""
Test view to compare cotton button component with original include version.
Renders a comprehensive test page with all button variants and combinations.
"""
return render(request, "test_button_comparison.html")

16
backend/uv.lock generated
View File

@@ -1,5 +1,5 @@
version = 1
revision = 3
revision = 2
requires-python = ">=3.13"
[[package]]
@@ -642,6 +642,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/30/d8/19ed1e47badf477d17fb177c1c19b5a21da0fd2d9f093f23be3fb86c5fab/django_cors_headers-4.9.0-py3-none-any.whl", hash = "sha256:15c7f20727f90044dcee2216a9fd7303741a864865f0c3657e28b7056f61b449", size = 12809, upload-time = "2025-09-18T10:40:50.843Z" },
]
[[package]]
name = "django-cotton"
version = "2.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c3/99/36e318ebd1ace3fc874541a02e7e4149def8e21aab85aceb7bb01e17607b/django_cotton-2.1.3.tar.gz", hash = "sha256:737f9c088549d7febbf78532856ddf1270799675a4bc9fa191a5db0e195a9c13", size = 23432, upload-time = "2025-06-30T17:31:29.29Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ad/ec/5e5318af0304962be43e3b912aef024d8ac08c0f9a9dfcc4f0cd55d0e74e/django_cotton-2.1.3-py3-none-any.whl", hash = "sha256:f33658d05a8f5ecf7448bdf1089e2ad27d2ce42e59c752216129701d7d153c89", size = 22214, upload-time = "2025-06-30T17:31:28.093Z" },
]
[[package]]
name = "django-debug-toolbar"
version = "6.0.0"
@@ -2238,6 +2250,7 @@ dependencies = [
{ name = "django-cleanup" },
{ name = "django-cloudflareimages-toolkit" },
{ name = "django-cors-headers" },
{ name = "django-cotton" },
{ name = "django-debug-toolbar" },
{ name = "django-environ" },
{ name = "django-extensions" },
@@ -2309,6 +2322,7 @@ requires-dist = [
{ name = "django-cleanup", specifier = ">=8.0.0" },
{ name = "django-cloudflareimages-toolkit", specifier = ">=1.0.6" },
{ name = "django-cors-headers", specifier = ">=4.3.1" },
{ name = "django-cotton", specifier = ">=2.1.3" },
{ name = "django-debug-toolbar", specifier = ">=4.0.0" },
{ name = "django-environ", specifier = ">=0.12.0" },
{ name = "django-extensions", specifier = ">=4.1" },