refactor: migrate and consolidate UI templates to compile-time Askama component macros

This commit is contained in:
2026-05-30 12:28:47 +05:00
parent f42a5f05b2
commit 110fc61fa2
16 changed files with 697 additions and 598 deletions
+383
View File
@@ -0,0 +1,383 @@
{% macro button(label, variant="primary", type="button", extra_class="", disabled=false) %}
<button
type="{{ type }}"
{% if disabled %}disabled{% endif %}
class="inline-flex items-center justify-center rounded-xl text-xs font-bold transition-all focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-background px-4 py-2.5 active:scale-95
{% if variant == "primary" %}
bg-primary text-primary-foreground hover:opacity-90 shadow-md shadow-slate-950/20
{% elif variant == "secondary" %}
bg-secondary text-secondary-foreground hover:bg-secondary/80
{% elif variant == "outline" %}
border border-border bg-transparent hover:bg-secondary text-slate-200
{% elif variant == "destructive" %}
bg-destructive text-destructive-foreground hover:opacity-90 shadow-md
{% elif variant == "indigo" %}
bg-indigo-600 hover:bg-indigo-500 text-white shadow-md
{% endif %}
{% if disabled %}opacity-50 cursor-not-allowed active:scale-100{% endif %}
{{ extra_class }}"
>
{{ label|safe }}
</button>
{% endmacro %}
{% macro modal_trigger(target_id, label, variant="primary", extra_class="") %}
<button
data-modal-target="{{ target_id }}"
class="inline-flex items-center justify-center rounded-xl text-xs font-bold transition-all focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-background px-4 py-2.5 active:scale-95
{% if variant == "primary" %}
bg-primary text-primary-foreground hover:opacity-90 shadow-md shadow-slate-950/20
{% elif variant == "secondary" %}
bg-secondary text-secondary-foreground hover:bg-secondary/80
{% elif variant == "outline" %}
border border-border bg-transparent hover:bg-secondary text-slate-200
{% elif variant == "destructive" %}
bg-destructive text-destructive-foreground hover:opacity-90 shadow-md
{% elif variant == "indigo" %}
bg-indigo-600 hover:bg-indigo-500 text-white shadow-md
{% endif %}
{{ extra_class }}"
>
{{ label|safe }}
</button>
{% endmacro %}
{% macro sheet_trigger(target_id, label, variant="primary", extra_class="") %}
<button
data-sheet-target="{{ target_id }}"
class="inline-flex items-center justify-center rounded-xl text-xs font-bold transition-all focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-background px-4 py-2.5 active:scale-95
{% if variant == "primary" %}
bg-primary text-primary-foreground hover:opacity-90 shadow-md shadow-slate-950/20
{% elif variant == "secondary" %}
bg-secondary text-secondary-foreground hover:bg-secondary/80
{% elif variant == "outline" %}
border border-border bg-transparent hover:bg-secondary text-slate-200
{% elif variant == "destructive" %}
bg-destructive text-destructive-foreground hover:opacity-90 shadow-md
{% elif variant == "indigo" %}
bg-indigo-600 hover:bg-indigo-500 text-white shadow-md
{% endif %}
{{ extra_class }}"
>
{{ label|safe }}
</button>
{% endmacro %}
{% macro modal(id, title, content, close_label="Close") %}
<div id="{{ id }}" class="modal-dialog fixed inset-0 z-50 flex items-center justify-center hidden" role="dialog" aria-modal="true">
<div class="modal-backdrop fixed inset-0 bg-[#07090e]/80 backdrop-blur-sm transition-opacity duration-300"></div>
<div class="modal-content relative z-10 w-full max-w-sm scale-95 opacity-0 transition-all duration-300 border border-border bg-popover/95 backdrop-blur-xl p-6 shadow-2xl rounded-3xl text-center">
<div class="mx-auto flex h-10 w-10 items-center justify-center rounded-full bg-indigo-500/10 border border-indigo-500/20 text-indigo-400 mb-3">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<h3 class="text-sm font-bold text-slate-100">{{ title }}</h3>
<div class="text-xs text-slate-400 mt-2 leading-relaxed">{{ content|safe }}</div>
<button class="modal-close mt-4 w-full py-2 rounded-xl bg-secondary border border-border hover:bg-secondary transition text-xs font-semibold text-slate-200">{{ close_label }}</button>
</div>
</div>
{% endmacro %}
{% macro modal_open(id, title) %}
<div id="{{ id }}" class="modal-dialog fixed inset-0 z-50 flex items-center justify-center hidden" role="dialog" aria-modal="true">
<div class="modal-backdrop fixed inset-0 bg-[#07090e]/80 backdrop-blur-sm transition-opacity duration-300"></div>
<div class="modal-content relative z-10 w-full max-w-sm scale-95 opacity-0 transition-all duration-300 border border-border bg-popover/95 backdrop-blur-xl p-6 shadow-2xl rounded-3xl text-center">
<div class="mx-auto flex h-10 w-10 items-center justify-center rounded-full bg-indigo-500/10 border border-indigo-500/20 text-indigo-400 mb-3">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<h3 class="text-sm font-bold text-slate-100 mb-2">{{ title }}</h3>
{% endmacro %}
{% macro modal_close(close_label="Dismiss Modal") %}
<button class="modal-close mt-4 w-full py-2 rounded-xl bg-secondary border border-border hover:bg-secondary transition text-xs font-semibold text-slate-200">{{ close_label }}</button>
</div>
</div>
{% endmacro %}
{% macro sheet_open(id, title, max_width_class="max-w-sm") %}
<div id="{{ id }}" class="sheet-dialog fixed inset-0 z-50 overflow-hidden hidden" role="dialog" aria-modal="true">
<div class="sheet-backdrop fixed inset-0 bg-[#07090e]/80 backdrop-blur-sm opacity-0 transition-opacity duration-300 animate-fade-in"></div>
<div class="absolute inset-y-0 right-0 max-w-full flex pl-10">
<div class="sheet-content w-screen {{ max_width_class }} translate-x-full transition-transform duration-300 ease-in-out border-l border-border bg-popover/95 backdrop-blur-xl p-6 shadow-2xl flex flex-col justify-between">
<div class="space-y-4">
<div class="flex items-center justify-between pb-3 border-b border-border">
<h3 class="text-sm font-bold text-slate-100">{{ title }}</h3>
<button class="sheet-close text-slate-500 hover:text-white rounded-lg p-1.5 hover:bg-secondary transition">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
{% endmacro %}
{% macro sheet_close(save_label="Save") %}
</div>
{% if !save_label.is_empty() %}
<button class="sheet-close w-full py-2.5 rounded-xl bg-indigo-650 hover:bg-indigo-600 transition text-xs font-bold text-white shadow-lg shadow-indigo-650/10">{{ save_label }}</button>
{% endif %}
</div>
</div>
</div>
{% endmacro %}
{% macro search_combobox(name, label, placeholder="Search...", search_url="/developers/search", input_id="combobox-search", value_id="combobox-value") %}
<div class="autocomplete-combobox relative">
{% if !label.is_empty() %}
<label class="block text-xs font-semibold text-slate-400 mb-1.5">{{ label }}</label>
{% endif %}
<!-- Hidden input holding the actual selected ID to submit in the form -->
<input type="hidden" id="{{ value_id }}" name="{{ name }}" class="combobox-value">
<div class="relative">
<input type="text"
id="{{ input_id }}"
name="q"
placeholder="{{ placeholder }}"
autocomplete="off"
hx-get="{{ search_url }}"
hx-trigger="input changed delay:250ms, search"
hx-target="next .combobox-results"
hx-indicator="next .combobox-indicator"
class="combobox-input appearance-none rounded-xl relative block w-full pl-9 pr-4 py-2.5 bg-[#0f172a]/80 border border-slate-800 placeholder-slate-500 text-white focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-sky-500 transition duration-200 text-sm">
<!-- Search Icon & Loading Indicator -->
<div class="absolute left-3 top-3 text-slate-500">
<svg class="combobox-indicator htmx-indicator animate-spin h-4 w-4 text-sky-500 hidden" 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 12h4z"></path>
</svg>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
</div>
<!-- Search Results Dropdown Popover -->
<div class="combobox-results absolute z-10 w-full mt-1.5 bg-slate-900 border border-slate-800 rounded-xl shadow-2xl overflow-hidden hidden">
</div>
</div>
{% endmacro %}
{% macro text_input(id, name, label, type="text", placeholder="", value="", required=false, extra_class="") %}
<div class="space-y-2 {{ extra_class }}">
{% if !label.is_empty() %}
<label for="{{ id }}" class="block text-xs font-semibold text-muted-foreground">{{ label }}</label>
{% endif %}
<input
id="{{ id }}"
name="{{ name }}"
type="{{ type }}"
value="{{ value }}"
placeholder="{{ placeholder }}"
{% if required %}required{% endif %}
class="block h-10 w-full rounded-xl border border-border bg-background px-4 py-2 text-sm text-white placeholder-slate-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/50 transition duration-200"
>
</div>
{% endmacro %}
{% macro textarea(id, name, label, placeholder="", value="", rows="3", required=false, extra_class="") %}
<div class="space-y-2 {{ extra_class }}">
{% if !label.is_empty() %}
<label for="{{ id }}" class="block text-xs font-semibold text-muted-foreground">{{ label }}</label>
{% endif %}
<textarea
id="{{ id }}"
name="{{ name }}"
rows="{{ rows }}"
placeholder="{{ placeholder }}"
{% if required %}required{% endif %}
class="block w-full rounded-xl border border-border bg-background px-4 py-2 text-sm text-white placeholder-slate-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/50 transition duration-200 resize-none"
>{{ value }}</textarea>
</div>
{% endmacro %}
{% macro select_open(name, label, current_value, current_text) %}
<div class="space-y-2">
{% if !label.is_empty() %}
<label class="block text-xs font-semibold text-muted-foreground">{{ label }}</label>
{% endif %}
<div class="custom-select relative inline-block w-full">
<input type="hidden" name="{{ name }}" class="select-value" value="{{ current_value }}">
<button type="button" class="select-trigger flex h-10 w-full items-center justify-between rounded-xl border border-border bg-background px-4 py-2 text-sm text-slate-200 hover:bg-secondary/50 transition focus:outline-none focus:ring-2 focus:ring-sky-500">
<span class="select-text">{{ current_text }}</span>
<svg class="h-4 w-4 text-slate-500 transition-transform duration-200 select-chevron" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
<polyline points="6 9 12 15 18 9"/>
</svg>
</button>
<div class="select-popover absolute left-0 z-20 mt-2 w-full p-1 rounded-2xl border border-border bg-popover shadow-2xl hidden animate-in fade-in slide-in-from-top-2 duration-150">
<div class="max-h-60 overflow-y-auto p-0.5 space-y-0.5">
{% endmacro %}
{% macro select_item(value, label, is_selected=false) %}
<button type="button" class="select-item flex items-center w-full h-9 px-2.5 rounded-lg text-xs {% if is_selected %}bg-accent text-accent-foreground font-semibold{% else %}hover:bg-accent hover:text-accent-foreground{% endif %} focus:bg-accent focus:text-accent-foreground focus:outline-none text-slate-200 cursor-pointer select-none text-left" data-value="{{ value }}">{{ label }}</button>
{% endmacro %}
{% macro select_close() %}
</div>
</div>
</div>
</div>
{% endmacro %}
{% macro toggle_switch(name, label, checked=false, extra_class="") %}
<div class="flex items-center justify-between {{ extra_class }}">
{% if !label.is_empty() %}
<span class="text-xs text-muted-foreground font-medium">{{ label }}</span>
{% endif %}
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" name="{{ name }}" class="sr-only peer" {% if checked %}checked{% endif %}>
<div class="w-9 h-5 bg-secondary rounded-full border border-border peer-checked:bg-indigo-600 peer-checked:border-indigo-500 transition-all duration-300 relative after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-slate-400 after:rounded-full after:h-[14px] after:w-[14px] after:transition-all peer-checked:after:translate-x-4 peer-checked:after:bg-white"></div>
</label>
</div>
{% endmacro %}
{% macro checkbox(name, label, checked=false, extra_class="") %}
<label class="flex items-center gap-3 cursor-pointer group {{ extra_class }}">
<input type="checkbox" name="{{ name }}" class="sr-only peer" {% if checked %}checked{% endif %}>
<div class="w-4 h-4 rounded bg-popover border border-border flex items-center justify-center peer-checked:bg-indigo-600 peer-checked:border-indigo-500 peer-checked:[&_svg]:opacity-100 transition">
<svg class="w-2.5 h-2.5 text-white opacity-0 transition-opacity" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="4"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
</div>
{% if !label.is_empty() %}
<span class="text-xs text-muted-foreground peer-checked:text-slate-200 select-none">{{ label }}</span>
{% endif %}
</label>
{% endmacro %}
{% macro range_slider(name, label, min="0", max="100", value="50", extra_class="") %}
<div class="space-y-2 {{ extra_class }}">
{% if !label.is_empty() %}
<label class="block text-xs font-semibold text-muted-foreground">{{ label }}</label>
{% endif %}
<div class="flex items-center gap-4">
<input type="range" name="{{ name }}" min="{{ min }}" max="{{ max }}" value="{{ value }}" class="grow h-1 bg-secondary rounded-lg appearance-none cursor-pointer accent-indigo-600" oninput="this.nextElementSibling.textContent = this.value + '%'">
<span class="text-xs font-mono font-bold text-sky-400 w-10 text-right">{{ value }}%</span>
</div>
</div>
{% endmacro %}
{% macro datepicker(id, name, label, value="2026-05-30", display_text="May 30, 2026", data_year="2026", data_month="4", extra_class="") %}
<div class="space-y-2 {{ extra_class }}">
{% if !label.is_empty() %}
<label class="block text-xs font-semibold text-slate-400">{{ label }}</label>
{% endif %}
<div class="custom-datepicker relative inline-block w-full" id="{{ id }}" data-year="{{ data_year }}" data-month="{{ data_month }}">
<input type="hidden" name="{{ name }}" class="datepicker-value" value="{{ value }}">
<button type="button" class="datepicker-trigger flex h-10 w-full items-center justify-between rounded-xl border border-border bg-background px-4 py-2 text-sm text-slate-200 hover:bg-secondary transition focus:outline-none focus:ring-2 focus:ring-sky-500">
<span class="datepicker-label flex items-center gap-2">
<svg class="h-4 w-4 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<span class="datepicker-text">{{ display_text }}</span>
</span>
<svg class="h-4 w-4 text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div class="datepicker-popover absolute left-0 z-20 mt-2 w-[270px] p-3 rounded-2xl border border-border bg-popover shadow-2xl animate-in fade-in slide-in-from-top-2 duration-200 hidden">
<div class="flex items-center justify-between mb-3.5">
<button type="button" class="datepicker-prev p-1.5 rounded-lg hover:bg-secondary text-muted-foreground/90 hover:text-white transition">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><polyline points="15 18 9 12 15 6"/></svg>
</button>
<span class="datepicker-month-year text-xs font-bold text-slate-200"></span>
<button type="button" class="datepicker-next p-1.5 rounded-lg hover:bg-secondary text-muted-foreground/90 hover:text-white transition">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><polyline points="9 18 15 12 9 6"/></svg>
</button>
</div>
<div class="grid grid-cols-7 gap-1 text-center text-[10px] font-bold text-slate-500 mb-2">
<span>Su</span><span>Mo</span><span>Tu</span><span>We</span><span>Th</span><span>Fr</span><span>Sa</span>
</div>
<div class="datepicker-days grid grid-cols-7 gap-1 text-center"></div>
</div>
</div>
</div>
{% endmacro %}
{% macro timepicker(id, name, label, value="12:00 PM", extra_class="") %}
<div class="space-y-2 {{ extra_class }}">
{% if !label.is_empty() %}
<label class="block text-xs font-semibold text-slate-405">{{ label }}</label>
{% endif %}
<div class="custom-timepicker relative inline-block w-full" id="{{ id }}">
<input type="hidden" name="{{ name }}" class="timepicker-value" value="{{ value }}">
<button type="button" class="timepicker-trigger flex h-10 w-full items-center justify-between rounded-xl border border-border bg-background px-4 py-2 text-sm text-slate-200 hover:bg-secondary transition focus:outline-none focus:ring-2 focus:ring-sky-500">
<span class="timepicker-label flex items-center gap-2">
<svg class="h-4 w-4 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span class="timepicker-text">{{ value }}</span>
</span>
<svg class="h-4 w-4 text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div class="timepicker-popover absolute right-0 sm:left-0 z-20 mt-2 w-[230px] p-3 rounded-2xl border border-border bg-popover shadow-2xl animate-in fade-in slide-in-from-top-2 duration-200 hidden">
<div class="flex gap-2 justify-center items-center">
<div class="flex flex-col items-center">
<span class="text-[9px] font-bold text-slate-500 mb-1.5 uppercase tracking-wider">Hr</span>
<div class="h-32 overflow-y-auto w-12 text-center rounded-lg border border-border bg-popover scrollbar-none timepicker-col-hours"></div>
</div>
<span class="text-slate-500 font-bold self-end mb-12">:</span>
<div class="flex flex-col items-center">
<span class="text-[9px] font-bold text-slate-500 mb-1.5 uppercase tracking-wider">Min</span>
<div class="h-32 overflow-y-auto w-12 text-center rounded-lg border border-border bg-popover scrollbar-none timepicker-col-minutes"></div>
</div>
<div class="flex flex-col items-center ml-1">
<span class="text-[9px] font-bold text-slate-500 mb-1.5 uppercase tracking-wider">Am/Pm</span>
<div class="flex flex-col gap-1 w-12">
<button type="button" class="timepicker-ampm-btn py-1.5 rounded-lg text-xs font-bold hover:bg-secondary transition text-muted-foreground">AM</button>
<button type="button" class="timepicker-ampm-btn py-1.5 rounded-lg text-xs font-bold hover:bg-secondary transition text-muted-foreground">PM</button>
</div>
</div>
</div>
</div>
</div>
</div>
{% endmacro %}
{% macro tabs_header_open() %}
<div class="flex border-b border-border">
{% endmacro %}
{% macro tab_trigger(group, target_id, label, is_active=false) %}
<button
type="button"
data-tab-group="{{ group }}"
data-tab-target="{{ target_id }}"
class="px-3 py-1.5 text-xs font-semibold border-b-2 focus:outline-none transition-all
{% if is_active %}border-sky-500 text-sky-400{% else %}border-transparent text-muted-foreground hover:text-slate-200{% endif %}"
>
{{ label }}
</button>
{% endmacro %}
{% macro tabs_header_close() %}
</div>
{% endmacro %}
{% macro tabs_content_open() %}
<div class="p-4 bg-card/50 rounded-xl border border-border text-xs text-muted-foreground min-h-[5rem]">
{% endmacro %}
{% macro tab_pane_open(group, id, is_active=true) %}
<div id="{{ id }}" data-tab-content-group="{{ group }}" {% if !is_active %}class="hidden"{% endif %}>
{% endmacro %}
{% macro tab_pane_close() %}
</div>
{% endmacro %}
{% macro tabs_content_close() %}
</div>
{% endmacro %}
{% macro accordion_open(title, is_open=false) %}
<div class="accordion-item border border-border rounded-xl overflow-hidden bg-card/30">
<button type="button" class="accordion-trigger w-full flex items-center justify-between px-4 py-3 text-xs font-bold text-slate-200 hover:bg-secondary/50 transition focus:outline-none">
<span>{{ title }}</span>
<svg class="accordion-chevron h-3 w-3 text-slate-500 transition-transform duration-200 {% if is_open %}rotate-180{% endif %}" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div class="accordion-content px-4 pb-3 pt-1 text-xs text-muted-foreground {% if !is_open %}hidden{% endif %} border-t border-border leading-relaxed">
{% endmacro %}
{% macro accordion_close() %}
</div>
</div>
{% endmacro %}