Add standardized HTMX conventions, interaction patterns, and migration guide for ThrillWiki UX

This commit is contained in:
pacnpal
2025-12-22 16:56:27 -05:00
parent 2e35f8c5d9
commit ae31e889d7
144 changed files with 25792 additions and 4440 deletions

View File

@@ -1,63 +1,155 @@
{% comment %}
Button Component - Django Template Version of shadcn/ui Button
Usage: {% include 'components/ui/button.html' with variant='default' size='default' text='Click me' %}
Button Component - Unified Django Template Version of shadcn/ui Button
A versatile button component that supports multiple variants, sizes, icons, and both
button/link elements. Compatible with HTMX and Alpine.js.
Usage Examples:
Basic button:
{% include 'components/ui/button.html' with text='Click me' %}
With variant and size:
{% include 'components/ui/button.html' with text='Submit' variant='default' size='lg' %}
Link button:
{% include 'components/ui/button.html' with href='/path' text='Go' type='link' %}
With HTMX:
{% include 'components/ui/button.html' with text='Load' hx_get='/api/data' hx_target='#target' %}
With Alpine.js:
{% include 'components/ui/button.html' with text='Toggle' x_on_click='open = !open' %}
With SVG icon (preferred):
{% include 'components/ui/button.html' with icon=search_icon_svg text='Search' %}
Icon-only button:
{% include 'components/ui/button.html' with icon=icon_svg size='icon' aria_label='Close' %}
Parameters:
- variant: 'default', 'destructive', 'outline', 'secondary', 'ghost', 'link' (default: 'default')
- size: 'default', 'sm', 'lg', 'icon' (default: 'default')
- type: 'button', 'submit', 'reset', 'link' (default: 'button')
- text: Button text content
- label: Alias for text (for backwards compatibility)
- content: Alias for text (for backwards compatibility)
- href: URL for link buttons (required when type='link')
- icon: SVG icon content (will be sanitized)
- icon_left: Font Awesome class for left icon (deprecated, prefer icon)
- icon_right: Font Awesome class for right icon (deprecated)
- disabled: Boolean to disable the button
- class: Additional CSS classes
- id: Element ID
- aria_label: Accessibility label (required for icon-only buttons)
- onclick: JavaScript click handler
- hx_get, hx_post, hx_target, hx_swap, hx_trigger, hx_indicator, hx_include: HTMX attributes
- x_data, x_on_click, x_bind, x_show: Alpine.js attributes
- attrs: Additional HTML attributes as string
Security: Icon SVGs are sanitized using the sanitize_svg filter to prevent XSS attacks.
{% endcomment %}
{% load static %}
{% load static safe_html %}
{% with variant=variant|default:'default' size=size|default:'default' %}
<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
{% with variant=variant|default:'default' size=size|default:'default' btn_type=type|default:'button' btn_text=text|default:label|default:content %}
{% if btn_type == 'link' or href %}
{# Link element styled as button #}
<a
href="{{ href|default:'#' }}"
{% if id %}id="{{ id }}"{% endif %}
class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50
{% if 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
{% else %}bg-primary text-primary-foreground hover:bg-primary/90{% endif %}
{% if size == 'sm' %}h-9 rounded-md px-3
{% elif size == 'lg' %}h-11 rounded-md px-8
{% elif size == 'icon' %}h-10 w-10
{% else %}h-10 px-4 py-2{% endif %}
{{ class|default:'' }}"
{% if disabled %}aria-disabled="true" tabindex="-1"{% endif %}
{% if aria_label %}aria-label="{{ aria_label }}"{% endif %}
{% if x_data %}x-data="{{ x_data }}"{% endif %}
{% if x_on_click %}@click="{{ x_on_click }}"{% endif %}
{% if x_bind %}x-bind="{{ x_bind }}"{% endif %}
{% if x_show %}x-show="{{ x_show }}"{% 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 hx_trigger %}hx-trigger="{{ hx_trigger }}"{% endif %}
{% if hx_indicator %}hx-indicator="{{ hx_indicator }}"{% endif %}
{{ attrs|default:'' }}>
{% if icon %}
<span class="w-4 h-4 flex items-center justify-center">{{ icon|sanitize_svg }}</span>
{% if btn_text %}<span>{{ btn_text }}</span>{% endif %}
{% elif icon_left %}
<i class="{{ icon_left }} w-4 h-4" aria-hidden="true"></i>
{% if btn_text %}{{ btn_text }}{% endif %}
{% else %}
{{ btn_text }}
{% 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
{% if icon_right %}
<i class="{{ icon_right }} w-4 h-4" aria-hidden="true"></i>
{% 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|default:'' }}
>
{% if icon_left %}
<i class="{{ icon_left }} w-4 h-4"></i>
{% endif %}
{% if text %}
{{ text }}
{% else %}
{{ content|default:'' }}
{% endif %}
{% if icon_right %}
<i class="{{ icon_right }} w-4 h-4"></i>
{% endif %}
{% block link_content %}{% endblock %}
</a>
{% else %}
{# Button element #}
<button
type="{{ btn_type }}"
{% if id %}id="{{ id }}"{% endif %}
class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50
{% if 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
{% else %}bg-primary text-primary-foreground hover:bg-primary/90{% endif %}
{% if size == 'sm' %}h-9 rounded-md px-3
{% elif size == 'lg' %}h-11 rounded-md px-8
{% elif size == 'icon' %}h-10 w-10
{% else %}h-10 px-4 py-2{% endif %}
{{ class|default:'' }}"
{% if disabled %}disabled{% endif %}
{% if aria_label %}aria-label="{{ aria_label }}"{% endif %}
{% if onclick %}onclick="{{ onclick }}"{% endif %}
{% if x_data %}x-data="{{ x_data }}"{% endif %}
{% if x_on_click %}@click="{{ x_on_click }}"{% endif %}
{% if x_bind %}x-bind="{{ x_bind }}"{% endif %}
{% if x_show %}x-show="{{ x_show }}"{% 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 hx_trigger %}hx-trigger="{{ hx_trigger }}"{% endif %}
{% if hx_indicator %}hx-indicator="{{ hx_indicator }}"{% endif %}
{% if hx_include %}hx-include="{{ hx_include }}"{% endif %}
{{ attrs|default:'' }}>
{% if icon %}
<span class="w-4 h-4 flex items-center justify-center">{{ icon|sanitize_svg }}</span>
{% if btn_text %}<span>{{ btn_text }}</span>{% endif %}
{% elif icon_left %}
<i class="{{ icon_left }} w-4 h-4" aria-hidden="true"></i>
{% if btn_text %}{{ btn_text }}{% endif %}
{% else %}
{{ btn_text }}
{% endif %}
{% if icon_right %}
<i class="{{ icon_right }} w-4 h-4" aria-hidden="true"></i>
{% endif %}
{% block button_content %}{% endblock %}
</button>
{% endif %}
{% endwith %}