feat: htmx setup

This commit is contained in:
2026-07-17 12:04:47 +02:00
parent c68a0e37d6
commit 82399f1391
6 changed files with 63 additions and 74 deletions
+2
View File
@@ -29,6 +29,7 @@ THIRD_PARTY_APPS = [
'corsheaders', 'corsheaders',
'django_extensions', 'django_extensions',
'django_filters', 'django_filters',
'django_htmx',
'watchman', 'watchman',
] ]
@@ -55,6 +56,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django_htmx.middleware.HtmxMiddleware',
] ]
if DEBUG: if DEBUG:
+18
View File
@@ -0,0 +1,18 @@
{% load static %}
<!DOCTYPE html>
<html lang="{{ lang }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Crochet{% endblock %}</title>
<link rel="stylesheet" href="{% static 'css/main.css' %}">
{% block extra_css %}{% endblock %}
</head>
<body{% block body_attrs %}{% endblock %}>
{% block content %}{% endblock %}
{% block extra_js %}{% endblock %}
</body>
</html>
+20 -45
View File
@@ -1,33 +1,28 @@
{% load static i18n %} {% extends 'crochet/base.html' %}
<!DOCTYPE html> {% load static i18n django_htmx %}
<html lang="{{ lang }}">
<head> {% block title %}Crochet{% endblock %}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> {% block body_attrs %} hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'{% endblock %}
{% block extra_css %}
<!-- Leído por storage.js para la cabecera X-CSRFToken al guardar por fetch. --> <!-- Leído por storage.js para la cabecera X-CSRFToken al guardar por fetch. -->
<meta name="csrf-token" content="{{ csrf_token }}"> <meta name="csrf-token" content="{{ csrf_token }}">
<title>Crochet</title>
<link rel="stylesheet" href="{% static 'css/main.css' %}">
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" /> <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<!-- Rellenada por js/page-settings.js con el @page (tamaño/orientación) {% htmx_script %}
elegido en "Personalización de página": un <style> aparte porque
@page no se puede cambiar como una propiedad normal desde JS. -->
<style id="page-size-style"></style>
</head>
<body> <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
<style id="page-size-style"></style>
{% endblock %}
{% block content %}
<main> <main>
<header class="flex flex-wrap items-center justify-between gap-2 p-4 no-print"> <header class="flex flex-wrap items-center justify-between gap-2 p-4 no-print">
<h1 class="text-xl font-semibold">Crochet</h1> <h1 class="text-xl font-semibold">Crochet</h1>
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<!-- Idioma del contenido (título/subtítulo/texto de cada sección), no
de la interfaz: la estructura de secciones es la misma para
todos los idiomas, solo cambian los textos que el usuario ha
escrito para el idioma seleccionado. Añadir un idioma nuevo es
solo cuestión de añadir una <option> aquí. -->
<select id="language-select" class="select select-sm select-bordered"> <select id="language-select" class="select select-sm select-bordered">
<option value="es" {% if lang == 'es' %}selected{% endif %}>Español</option> <option value="es" {% if lang == 'es' %}selected{% endif %}>Español</option>
<option value="en" {% if lang == 'en' %}selected{% endif %}>English</option> <option value="en" {% if lang == 'en' %}selected{% endif %}>English</option>
@@ -35,18 +30,13 @@
<button id="save-pattern" type="button" class="btn btn-sm btn-outline" <button id="save-pattern" type="button" class="btn btn-sm btn-outline"
data-save-url="{% url 'crochet:pattern_save' pattern.uuid %}" data-save-url="{% url 'crochet:pattern_save' pattern.uuid %}"
data-error-message="{% trans 'No se ha podido guardar. Inténtalo de nuevo.' %}">{% trans 'Guardar' %}</button> data-error-message="{% trans 'No se ha podido guardar. Inténtalo de nuevo.' %}">{% trans 'Guardar' %}</button>
<!-- Enlace normal, no un botón con JS: solo navega a la vista de
solo lectura (ver PatternDetailView), que se guarda tal cual
desde la última vez que se pulsó "Guardar". target="_blank"
para no perder el editor con sus cambios sin guardar. -->
<a href="{% url 'crochet:pattern_detail' pattern.uuid %}" target="_blank" rel="noopener" <a href="{% url 'crochet:pattern_detail' pattern.uuid %}" target="_blank" rel="noopener"
class="btn btn-sm btn-outline">{% trans 'Ver patrón' %}</a> class="btn btn-sm btn-outline">{% trans 'Ver patrón' %}</a>
<button id="export-pdf" type="button" class="btn btn-sm">{% trans 'Exportar a PDF' %}</button> <button id="export-pdf" type="button" class="btn btn-sm">{% trans 'Exportar a PDF' %}</button>
</div> </div>
</header> </header>
<!-- Aviso breve de que "Guardar" ha funcionado: sin esto, guardar en
localStorage no da ninguna señal de que ha ocurrido algo. -->
<div id="save-toast" class="toast toast-top toast-end no-print hidden"> <div id="save-toast" class="toast toast-top toast-end no-print hidden">
<div class="alert alert-success"> <div class="alert alert-success">
<span data-message="{% trans 'Guardado' %}">{% trans 'Guardado' %}</span> <span data-message="{% trans 'Guardado' %}">{% trans 'Guardado' %}</span>
@@ -55,10 +45,6 @@
<div class="p-4 pt-0"> <div class="p-4 pt-0">
<!-- Configuración única para todo el documento (no es una sección más:
no se repite, no se reordena ni se duplica), por eso vive fuera
de #panel-top-sections. <details> nativo para no necesitar JS
solo para plegarla/desplegarla. -->
<details class="border border-base-300 rounded-box p-4 mb-4 no-print"> <details class="border border-base-300 rounded-box p-4 mb-4 no-print">
<summary class="text-lg font-semibold cursor-pointer select-none">{% trans 'Personalización de página' %}</summary> <summary class="text-lg font-semibold cursor-pointer select-none">{% trans 'Personalización de página' %}</summary>
<div class="flex flex-col gap-4 mt-4"> <div class="flex flex-col gap-4 mt-4">
@@ -145,13 +131,7 @@
<button id="toggle-collapse-all" type="button" class="btn btn-xs btn-outline" <button id="toggle-collapse-all" type="button" class="btn btn-xs btn-outline"
data-collapse-all-label="{% trans 'Colapsar todo' %}" data-expand-all-label="{% trans 'Expandir todo' %}">{% trans 'Colapsar todo' %}</button> data-collapse-all-label="{% trans 'Colapsar todo' %}" data-expand-all-label="{% trans 'Expandir todo' %}">{% trans 'Colapsar todo' %}</button>
</div> </div>
<!-- Un único botón con menú (en vez de 7-8 botones sueltos que se
partían en varias filas en pantallas estrechas): menos ruido
visual antes de llegar al contenido real. daisyUI abre
.dropdown-content con :focus-within, sin JS propio; el mismo
botón (clonado) se reutiliza dentro de cada "Grupo" (ver
addGroupSection en section-types.js), así que su
data-label también viaja con él. -->
<div class="dropdown mb-4"> <div class="dropdown mb-4">
<button id="add-section-toggle" type="button" class="btn btn-sm" tabindex="0" <button id="add-section-toggle" type="button" class="btn btn-sm" tabindex="0"
data-label="{% trans 'Añadir sección' %}">+ {% trans 'Añadir sección' %}</button> data-label="{% trans 'Añadir sección' %}">+ {% trans 'Añadir sección' %}</button>
@@ -195,17 +175,12 @@
</div> </div>
<!-- Catálogo de tipos de punto arrastrables (StitchType), leído por
dnd-items.js. Antes era una lista fija en este HTML; ahora viene de
la base de datos, con sus propias traducciones por idioma. -->
{{ stitch_types|json_script:"stitch-types" }} {{ stitch_types|json_script:"stitch-types" }}
<!-- Contenido guardado de este patrón (sections/pageSettings), leído
por main.js al arrancar. json_script escapa el JSON de forma
segura, así que nunca hay que interpolarlo a mano en un <script>. -->
{{ pattern_data|json_script:"pattern-data" }} {{ pattern_data|json_script:"pattern-data" }}
</main> </main>
</body> {% endblock %}
<script type="module" src="{% static 'js/main.js' %}"></script>
</html> {% block extra_js %}
<script type="module" src="{% static 'js/main.js' %}"></script>
{% endblock %}
+7 -29
View File
@@ -1,21 +1,9 @@
{% extends 'crochet/base.html' %}
{% load static i18n %} {% load static i18n %}
<!DOCTYPE html>
<html lang="{{ lang }}">
<head> {% block title %}{{ page_settings.title|default:"Crochet" }}{% endblock %}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> {% block extra_css %}
<title>{{ page_settings.title|default:"Crochet" }}</title>
<link rel="stylesheet" href="{% static 'css/main.css' %}">
<!-- CSS de Tailwind precompilado (ver .tools/build/ y css/README-pattern-detail.md),
no el CDN con compilador JIT en JS que usa pattern.html: WeasyPrint
(ver PatternPdfView) no ejecuta JavaScript, así que ese CDN no
generaría ningún estilo al convertir esta misma página a PDF. Ninguna
<script> propia tampoco: esta página es puramente de lectura,
generada del todo en el servidor (ver pattern_render.py). No hay
ningún <input>/<button> de edición que "revelar" quitando una clase
desde las herramientas de desarrollo, porque ese marcado no existe
aquí (a diferencia de la vista de edición, que sí lo necesita). -->
<link rel="stylesheet" href="{% static 'css/pattern-detail.css' %}"> <link rel="stylesheet" href="{% static 'css/pattern-detail.css' %}">
<style> <style>
:root { :root {
@@ -24,19 +12,11 @@
} }
@page { size: {{ page_settings.pageSize }} {{ page_settings.orientation }}; } @page { size: {{ page_settings.pageSize }} {{ page_settings.orientation }}; }
</style> </style>
</head> {% endblock %}
<body> {% block content %}
<main class="p-4"> <main class="p-4">
<!-- Enlaces normales (sin JS): el tag "language" de i18n activa
temporalmente ese idioma para que "url" resuelva la ruta ya
traducida (ver crochet/urls.py), igual que hace LocaleMiddleware
por petición. no-print para que no aparezca en el PDF exportado
(impresión nativa del navegador sobre esta misma página). -->
<div class="flex justify-end gap-2 mb-4 no-print"> <div class="flex justify-end gap-2 mb-4 no-print">
<!-- Genera el PDF en el servidor con WeasyPrint (ver PatternPdfView):
renderiza esta misma plantilla (ya con el CSS precompilado, sin
depender de ningún JS) directamente a PDF. -->
<a href="{% url 'crochet:pattern_pdf' pattern.uuid %}" class="detail-btn">{% trans 'Descargar PDF' %}</a> <a href="{% url 'crochet:pattern_pdf' pattern.uuid %}" class="detail-btn">{% trans 'Descargar PDF' %}</a>
{% if lang != 'es' %} {% if lang != 'es' %}
{% language 'es' %} {% language 'es' %}
@@ -57,6 +37,4 @@
</div> </div>
</div> </div>
</main> </main>
</body> {% endblock %}
</html>
+1
View File
@@ -10,6 +10,7 @@ dependencies = [
"django-environ>=0.14.0", "django-environ>=0.14.0",
"django-extensions>=4.1", "django-extensions>=4.1",
"django-filter>=26.1", "django-filter>=26.1",
"django-htmx>=1.28.0",
"django-silk>=5.5.0", "django-silk>=5.5.0",
"django-storages>=1.14.6", "django-storages>=1.14.6",
"ipython>=9.15.0", "ipython>=9.15.0",
Generated
+15
View File
@@ -239,6 +239,7 @@ dependencies = [
{ name = "django-environ" }, { name = "django-environ" },
{ name = "django-extensions" }, { name = "django-extensions" },
{ name = "django-filter" }, { name = "django-filter" },
{ name = "django-htmx" },
{ name = "django-silk" }, { name = "django-silk" },
{ name = "django-storages" }, { name = "django-storages" },
{ name = "ipython" }, { name = "ipython" },
@@ -268,6 +269,7 @@ requires-dist = [
{ name = "django-environ", specifier = ">=0.14.0" }, { name = "django-environ", specifier = ">=0.14.0" },
{ name = "django-extensions", specifier = ">=4.1" }, { name = "django-extensions", specifier = ">=4.1" },
{ name = "django-filter", specifier = ">=26.1" }, { name = "django-filter", specifier = ">=26.1" },
{ name = "django-htmx", specifier = ">=1.28.0" },
{ name = "django-silk", specifier = ">=5.5.0" }, { name = "django-silk", specifier = ">=5.5.0" },
{ name = "django-storages", specifier = ">=1.14.6" }, { name = "django-storages", specifier = ">=1.14.6" },
{ name = "ipython", specifier = ">=9.15.0" }, { name = "ipython", specifier = ">=9.15.0" },
@@ -398,6 +400,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/01/afffed1e3c4540fb75bf550a18b6176a9f6371b5f3e52b69a28995b6480c/django_filter-26.1-py3-none-any.whl", hash = "sha256:7d98ef2899218e6242619b532cb1b95af14e09dfcf74844aecb550ad27b59ff2", size = 94069, upload-time = "2026-07-11T09:27:01.012Z" }, { url = "https://files.pythonhosted.org/packages/1f/01/afffed1e3c4540fb75bf550a18b6176a9f6371b5f3e52b69a28995b6480c/django_filter-26.1-py3-none-any.whl", hash = "sha256:7d98ef2899218e6242619b532cb1b95af14e09dfcf74844aecb550ad27b59ff2", size = 94069, upload-time = "2026-07-11T09:27:01.012Z" },
] ]
[[package]]
name = "django-htmx"
version = "1.28.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/47/d9/b31339fae8ede6a7cb0b9facaefe2ee4fc01d9f37a31a3dc03759765a2c3/django_htmx-1.28.0.tar.gz", hash = "sha256:699e229de0bbf19b3ee1a5d419702f5c154fa956e6921fec89faa7401b99e536", size = 100946, upload-time = "2026-07-12T22:19:19.589Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/d5/71b3a206f7a2135e2ab4a789be143a00222b52d22729c84f480915049e94/django_htmx-1.28.0-py3-none-any.whl", hash = "sha256:e6911acccf3e7905d511c4fd6ab86fe06572205e80a0f32ca7aa94a115ae0619", size = 98588, upload-time = "2026-07-12T22:19:18.225Z" },
]
[[package]] [[package]]
name = "django-silk" name = "django-silk"
version = "5.5.0" version = "5.5.0"