diff --git a/backend/api/invoices/delete.py b/backend/api/invoices/delete.py index d16b417fc..0074dbcf1 100644 --- a/backend/api/invoices/delete.py +++ b/backend/api/invoices/delete.py @@ -1,6 +1,8 @@ from django.contrib import messages -from django.http import HttpRequest, JsonResponse, QueryDict +from django.http import HttpRequest, JsonResponse, QueryDict, HttpResponse from django.shortcuts import render +from django.urls import resolve +from django.urls.exceptions import Resolver404 from django.views.decorators.http import require_http_methods from backend.models import Invoice @@ -11,6 +13,7 @@ def delete_invoice(request: HttpRequest): delete_items = QueryDict(request.body) invoice = delete_items.get("invoice") + redirect = delete_items.get("redirect", None) try: invoice = Invoice.objects.get(id=invoice) @@ -26,7 +29,17 @@ def delete_invoice(request: HttpRequest): invoice.delete() if request.htmx: - messages.success(request, "Invoice deleted") - return render(request, "base/toasts.html") + print("should send msg") + if not redirect: + messages.success(request, "Invoice deleted") + return render(request, "base/toasts.html") + + try: + resolve(redirect) + response = HttpResponse(request, status=200) + response["HX-Location"] = redirect + return response + except Resolver404: + ... return JsonResponse({"message": "Invoice successfully deleted"}, status=200) diff --git a/backend/api/invoices/manage.py b/backend/api/invoices/manage.py new file mode 100644 index 000000000..c1e6e9a24 --- /dev/null +++ b/backend/api/invoices/manage.py @@ -0,0 +1,74 @@ +from dataclasses import dataclass +from typing import Literal, TypedDict + +from django.contrib import messages +from django.http import HttpRequest +from django.shortcuts import render, redirect +from django.views.decorators.http import require_http_methods + +from backend.models import Invoice, UserSettings + + +class PreviewContext(TypedDict): + type: Literal["preview"] + invoice: Invoice + currency_symbol: str + + +@dataclass(frozen=True) +class SuccessResponse: + context: PreviewContext + success: Literal[True] = True + + +@dataclass(frozen=True) +class ErrorResponse: + message: str + success: Literal[False] = False + + +@require_http_methods(["GET"]) +def tab_preview_invoice(request: HttpRequest, invoice_id): + # Redirect if not an HTMX request + if not request.htmx: + return redirect("invoices dashboard") # Maybe should be 404? + + prev_invoice = preview_invoice(request, invoice_id) + + if prev_invoice.success: + return render(request, "pages/invoices/view/invoice.html", prev_invoice.context) + + messages.error(request, prev_invoice.message) + + return render(request, "base/toasts.html") + + +def preview_invoice(request: HttpRequest, invoice_id) -> SuccessResponse | ErrorResponse: + context = {"type": "preview"} + + try: + invoice = Invoice.objects.prefetch_related("items").get(id=invoice_id) + + except Invoice.DoesNotExist: + return ErrorResponse("Invoice not found") + + if request.user.logged_in_as_team: + if invoice.organization != request.user.logged_in_as_team: + return ErrorResponse("You don't have access to this invoice") + else: + if invoice.user != request.user: + return ErrorResponse("You don't have access to this invoice") + try: + currency_symbol = request.user.user_profile.get_currency_symbol + except UserSettings.DoesNotExist: + currency_symbol = "$" + + context.update({"invoice": invoice, "currency_symbol": currency_symbol}) + + context_object = PreviewContext( + type="preview", + invoice=invoice, + currency_symbol=currency_symbol, + ) + + return SuccessResponse(context=context_object) diff --git a/backend/api/invoices/urls.py b/backend/api/invoices/urls.py index f5e7db46d..8bc66e9df 100644 --- a/backend/api/invoices/urls.py +++ b/backend/api/invoices/urls.py @@ -1,6 +1,6 @@ from django.urls import path -from . import fetch, delete, edit, schedule +from . import fetch, delete, edit, schedule, manage from .create import set_destination from .create.services import add @@ -36,6 +36,7 @@ path("create_schedule/", schedule.create_schedule, name="create_schedule"), path("schedules/onetime//cancel/", schedule.cancel_onetime_schedule, name="schedules onetime cancel"), path("schedules/onetime/fetch//", schedule.fetch_onetime_schedules, name="schedules onetime fetch"), + path("manage//tabs/preview/", manage.tab_preview_invoice, name="tab preview"), ] app_name = "invoices" diff --git a/backend/api/invoices/view.py b/backend/api/invoices/view.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/urls.py b/backend/urls.py index 543203048..3af88996f 100644 --- a/backend/urls.py +++ b/backend/urls.py @@ -12,6 +12,7 @@ receipts, ) from backend.views.core.currency_converter import dashboard as cc_dashboard +from backend.views.core.invoices.overview import manage_invoice from backend.views.core.other.index import index, dashboard url( @@ -26,6 +27,11 @@ path("dashboard/", dashboard, name="dashboard"), path("dashboard/settings/", settings_v.view.settings_page, name="user settings"), path("dashboard/invoices/", include("backend.views.core.invoices.urls")), + path( + "dashboard/invoice//", + manage_invoice, + name="invoice overview", + ), path("favicon.ico", RedirectView.as_view(url="favicon.ico")), path( "dashboard/settings/teams", diff --git a/backend/views/core/auth/login.py b/backend/views/core/auth/login.py index 5734d787d..8bcc63ce6 100644 --- a/backend/views/core/auth/login.py +++ b/backend/views/core/auth/login.py @@ -5,7 +5,8 @@ from django.contrib.auth.hashers import check_password from django.core.validators import validate_email from django.http import HttpRequest -from django.urls import reverse +from django.urls import reverse, resolve +from django.urls.exceptions import Resolver404 from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.http import require_GET, require_POST @@ -26,10 +27,12 @@ @require_GET @not_authenticated def login_initial_page(request: HttpRequest): + next = request.GET.get("next") + return render( request, "pages/auth/login_initial.html", - {"github_enabled": SOCIAL_AUTH_GITHUB_ENABLED, "google_enabled": SOCIAL_AUTH_GOOGLE_OAUTH2_ENABLED}, + {"github_enabled": SOCIAL_AUTH_GITHUB_ENABLED, "next": next, "google_enabled": SOCIAL_AUTH_GOOGLE_OAUTH2_ENABLED}, ) @@ -41,12 +44,13 @@ def login_manual(request: HttpRequest): # HTMX POST email = request.POST.get("email") password = request.POST.get("password") page = str(request.POST.get("page")) + next = request.POST.get("next") if not page or page == "1": return render( request, "pages/auth/login.html", - context={"email": email, "magic_links_enabled": ARE_EMAILS_ENABLED}, + context={"email": email, "next": next, "magic_links_enabled": ARE_EMAILS_ENABLED}, ) if not email: @@ -71,8 +75,16 @@ def login_manual(request: HttpRequest): # HTMX POST login(request, user) messages.success(request, "Successfully logged in") + response = HttpResponse(request, status=200) - response["HX-Refresh"] = "true" + + try: + resolve(next) + response["HX-Location"] = next + except Resolver404: + print(f"did not resolve: {next}") + ... + return response diff --git a/backend/views/core/invoices/overview.py b/backend/views/core/invoices/overview.py new file mode 100644 index 000000000..16e969fe9 --- /dev/null +++ b/backend/views/core/invoices/overview.py @@ -0,0 +1,22 @@ +from django.http import HttpRequest + +from backend.decorators import * +from backend.models import * + + +def invoices_dashboard(request: HttpRequest): + context = {} + + return render(request, "pages/invoices/dashboard/dashboard.html", context) + + +def manage_invoice(request: HttpRequest, invoice_id: str): + if not invoice_id.isnumeric(): + messages.error(request, "Invalid invoice ID") + return redirect("invoices:dashboard") + + invoice = Invoice.objects.get(id=invoice_id) + + if not invoice: + return redirect("invoices:dashboard") + return render(request, "pages/invoices/dashboard/manage.html", {"invoice": invoice}) diff --git a/backend/views/core/invoices/view.py b/backend/views/core/invoices/view.py index 98793a96b..1ef6ef7d8 100644 --- a/backend/views/core/invoices/view.py +++ b/backend/views/core/invoices/view.py @@ -30,7 +30,7 @@ def preview(request, invoice_id): return render( request, - "pages/invoices/view/invoice.html", + "pages/invoices/view/invoice_page.html", context, ) @@ -57,6 +57,6 @@ def view(request, uuid): return render( request, - "pages/invoices/view/invoice.html", + "pages/invoices/view/invoice_page.html", context, ) diff --git a/frontend/static/src/output.css b/frontend/static/src/output.css index 75ae1333c..14f808c76 100644 --- a/frontend/static/src/output.css +++ b/frontend/static/src/output.css @@ -1,5 +1,5 @@ /* -! tailwindcss v3.3.6 | MIT License | https://tailwindcss.com +! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com */ /* @@ -32,9 +32,11 @@ 4. Use the user's configured `sans` font-family by default. 5. Use the user's configured `sans` font-feature-settings by default. 6. Use the user's configured `sans` font-variation-settings by default. +7. Disable tap highlights on iOS */ -html { +html, +:host { line-height: 1.5; /* 1 */ -webkit-text-size-adjust: 100%; @@ -44,12 +46,14 @@ html { -o-tab-size: 4; tab-size: 4; /* 3 */ - font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */ font-feature-settings: normal; /* 5 */ font-variation-settings: normal; /* 6 */ + -webkit-tap-highlight-color: transparent; + /* 7 */ } /* @@ -898,8 +902,8 @@ html { color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); } - .menu li > *:not(ul):not(.menu-title):not(details):active, -.menu li > *:not(ul):not(.menu-title):not(details).active, + .menu li > *:not(ul, .menu-title, details, .btn):active, +.menu li > *:not(ul, .menu-title, details, .btn).active, .menu li > details > summary:active { --tw-bg-opacity: 1; background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); @@ -1075,6 +1079,70 @@ html { --tw-border-opacity: 0.2; } +.collapse:not(td):not(tr):not(colgroup) { + visibility: visible; +} + +.collapse { + position: relative; + display: grid; + overflow: hidden; + grid-template-rows: auto 0fr; + transition: grid-template-rows 0.2s; + width: 100%; + border-radius: var(--rounded-box, 1rem); +} + +.collapse-title, +.collapse > input[type="checkbox"], +.collapse > input[type="radio"], +.collapse-content { + grid-column-start: 1; + grid-row-start: 1; +} + +.collapse > input[type="checkbox"], +.collapse > input[type="radio"] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + opacity: 0; +} + +.collapse-content { + visibility: hidden; + grid-column-start: 1; + grid-row-start: 2; + min-height: 0px; + transition: visibility 0.2s; + transition: padding 0.2s ease-out, + background-color 0.2s ease-out; + padding-left: 1rem; + padding-right: 1rem; + cursor: unset; +} + +.collapse[open], +.collapse-open, +.collapse:focus:not(.collapse-close) { + grid-template-rows: auto 1fr; +} + +.collapse:not(.collapse-close):has(> input[type="checkbox"]:checked), +.collapse:not(.collapse-close):has(> input[type="radio"]:checked) { + grid-template-rows: auto 1fr; +} + +.collapse[open] > .collapse-content, +.collapse-open > .collapse-content, +.collapse:focus:not(.collapse-close) > .collapse-content, +.collapse:not(.collapse-close) > input[type="checkbox"]:checked ~ .collapse-content, +.collapse:not(.collapse-close) > input[type="radio"]:checked ~ .collapse-content { + visibility: visible; + min-height: -moz-fit-content; + min-height: fit-content; +} + .divider { display: flex; flex-direction: row; @@ -1122,7 +1190,8 @@ html { grid-template-rows: repeat(1, minmax(0, 1fr)); align-items: flex-start; justify-items: start; - overflow-y: auto; + overflow-x: hidden; + overflow-y: hidden; overscroll-behavior: contain; height: 100vh; height: 100dvh; @@ -1171,6 +1240,7 @@ html { .drawer-toggle:checked ~ .drawer-side { pointer-events: auto; visibility: visible; + overflow-y: auto; } .drawer-toggle:checked ~ .drawer-side > *:not(.drawer-overlay) { @@ -1450,14 +1520,14 @@ html { transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } - :where(.menu li:not(.menu-title):not(.disabled) > *:not(ul):not(details):not(.menu-title)):not(.active):hover, :where(.menu li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):not(.active):hover { + :where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(.active, .btn):hover, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(.active, .btn):hover { cursor: pointer; outline: 2px solid transparent; outline-offset: 2px; } @supports (color: oklch(0 0 0)) { - :where(.menu li:not(.menu-title):not(.disabled) > *:not(ul):not(details):not(.menu-title)):not(.active):hover, :where(.menu li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):not(.active):hover { + :where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(.active, .btn):hover, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(.active, .btn):hover { background-color: var(--fallback-bc,oklch(var(--bc)/0.1)); } } @@ -1587,6 +1657,13 @@ html { background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); } +.input[type="number"]::-webkit-inner-spin-button, +.input-md[type="number"]::-webkit-inner-spin-button { + margin-top: -1rem; + margin-bottom: -1rem; + margin-inline-end: -1rem; +} + .join { display: inline-flex; align-items: stretch; @@ -1708,8 +1785,7 @@ html { padding-inline-start: 0.5rem; } -.menu :where(li:not(.menu-title) > *:not(ul):not(details):not(.menu-title)), - .menu :where(li:not(.menu-title) > details > summary:not(.menu-title)) { +.menu :where(li:not(.menu-title) > *:not(ul, details, .menu-title, .btn)), .menu :where(li:not(.menu-title) > details > summary:not(.menu-title)) { display: grid; grid-auto-flow: column; align-content: flex-start; @@ -1837,7 +1913,7 @@ html { width: 100%; } -:where(.navbar > *) { +:where(.navbar > *:not(script, style)) { display: inline-flex; align-items: center; } @@ -2257,9 +2333,9 @@ input.tab:checked + .tab-content, --btn-color: var(--fallback-s); } - .btn-accent { - --btn-color: var(--fallback-a); - } + .btn-accent { + --btn-color: var(--fallback-a); + } .btn-neutral { --btn-color: var(--fallback-n); @@ -2332,9 +2408,9 @@ input.tab:checked + .tab-content, --btn-color: var(--s); } - .btn-accent { - --btn-color: var(--a); - } + .btn-accent { + --btn-color: var(--a); + } .btn-neutral { --btn-color: var(--n); @@ -2356,9 +2432,9 @@ input.tab:checked + .tab-content, } .btn-accent { - --tw-text-opacity: 1; - color: var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity))); - outline-color: var(--fallback-a,oklch(var(--a)/1)); + --tw-text-opacity: 1; + color: var(--fallback-ac, oklch(var(--ac)/var(--tw-text-opacity))); + outline-color: var(--fallback-a, oklch(var(--a)/1)); } .btn-neutral { @@ -2731,6 +2807,130 @@ input.tab:checked + .tab-content, } } +details.collapse { + width: 100%; +} + +details.collapse summary { + position: relative; + display: block; + outline: 2px solid transparent; + outline-offset: 2px; +} + +details.collapse summary::-webkit-details-marker { + display: none; +} + +.collapse:focus-visible { + outline-style: solid; + outline-width: 2px; + outline-offset: 2px; + outline-color: var(--fallback-bc,oklch(var(--bc)/1)); +} + +.collapse:has(.collapse-title:focus-visible), +.collapse:has(> input[type="checkbox"]:focus-visible), +.collapse:has(> input[type="radio"]:focus-visible) { + outline-style: solid; + outline-width: 2px; + outline-offset: 2px; + outline-color: var(--fallback-bc,oklch(var(--bc)/1)); +} + +.collapse-arrow > .collapse-title:after { + position: absolute; + display: block; + height: 0.5rem; + width: 0.5rem; + --tw-translate-y: -100%; + --tw-rotate: 45deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + transition-duration: 150ms; + transition-duration: 0.2s; + top: 1.9rem; + inset-inline-end: 1.4rem; + content: ""; + transform-origin: 75% 75%; + box-shadow: 2px 2px; + pointer-events: none; +} + +.collapse-plus > .collapse-title:after { + position: absolute; + display: block; + height: 0.5rem; + width: 0.5rem; + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + transition-duration: 300ms; + top: 0.9rem; + inset-inline-end: 1.4rem; + content: "+"; + pointer-events: none; +} + +.collapse:not(.collapse-open):not(.collapse-close) > input[type="checkbox"], +.collapse:not(.collapse-open):not(.collapse-close) > input[type="radio"]:not(:checked), +.collapse:not(.collapse-open):not(.collapse-close) > .collapse-title { + cursor: pointer; +} + +.collapse:focus:not(.collapse-open):not(.collapse-close):not(.collapse[open]) > .collapse-title { + cursor: unset; +} + +.collapse-title { + position: relative; +} + +:where(.collapse > input[type="checkbox"]), +:where(.collapse > input[type="radio"]) { + z-index: 1; +} + +.collapse-title, +:where(.collapse > input[type="checkbox"]), +:where(.collapse > input[type="radio"]) { + width: 100%; + padding: 1rem; + padding-inline-end: 3rem; + min-height: 3.75rem; + transition: background-color 0.2s ease-out; +} + +.collapse[open] > :where(.collapse-content), +.collapse-open > :where(.collapse-content), +.collapse:focus:not(.collapse-close) > :where(.collapse-content), +.collapse:not(.collapse-close) > :where(input[type="checkbox"]:checked ~ .collapse-content), +.collapse:not(.collapse-close) > :where(input[type="radio"]:checked ~ .collapse-content) { + padding-bottom: 1rem; + transition: padding 0.2s ease-out, + background-color 0.2s ease-out; +} + +.collapse[open].collapse-arrow > .collapse-title:after, +.collapse-open.collapse-arrow > .collapse-title:after, +.collapse-arrow:focus:not(.collapse-close) > .collapse-title:after, +.collapse-arrow:not(.collapse-close) > input[type="checkbox"]:checked ~ .collapse-title:after, +.collapse-arrow:not(.collapse-close) > input[type="radio"]:checked ~ .collapse-title:after { + --tw-translate-y: -50%; + --tw-rotate: 225deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.collapse[open].collapse-plus > .collapse-title:after, +.collapse-open.collapse-plus > .collapse-title:after, +.collapse-plus:focus:not(.collapse-close) > .collapse-title:after, +.collapse-plus:not(.collapse-close) > input[type="checkbox"]:checked ~ .collapse-title:after, +.collapse-plus:not(.collapse-close) > input[type="radio"]:checked ~ .collapse-title:after { + content: "−"; +} + .divider:not(:empty) { gap: 1rem; } @@ -2807,6 +3007,12 @@ input.tab:checked + .tab-content, color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); } +.input input { + --tw-bg-opacity: 1; + background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); + background-color: transparent; +} + .input input:focus { outline: 2px solid transparent; outline-offset: 2px; @@ -2994,7 +3200,7 @@ input.tab:checked + .tab-content, content: ""; } -.menu :where(li:not(.menu-title) > *:not(ul):not(details):not(.menu-title)), +.menu :where(li:not(.menu-title) > *:not(ul, details, .menu-title, .btn)), .menu :where(li:not(.menu-title) > details > summary:not(.menu-title)) { border-radius: var(--rounded-btn, 0.5rem); padding-left: 1rem; @@ -3011,12 +3217,7 @@ input.tab:checked + .tab-content, text-wrap: balance; } -:where(.menu li:not(.menu-title):not(.disabled) > *:not(ul):not(details):not(.menu-title)):not(summary):not(.active).focus, - :where(.menu li:not(.menu-title):not(.disabled) > *:not(ul):not(details):not(.menu-title)):not(summary):not(.active):focus, - :where(.menu li:not(.menu-title):not(.disabled) > *:not(ul):not(details):not(.menu-title)):is(summary):not(.active):focus-visible, - :where(.menu li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):not(summary):not(.active).focus, - :where(.menu li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):not(summary):not(.active):focus, - :where(.menu li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):is(summary):not(.active):focus-visible { +:where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(summary, .active, .btn).focus, :where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(summary, .active, .btn):focus, :where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):is(summary):not(.active, .btn):focus-visible, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(summary, .active, .btn).focus, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(summary, .active, .btn):focus, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):is(summary):not(.active, .btn):focus-visible { cursor: pointer; background-color: var(--fallback-bc,oklch(var(--bc)/0.1)); --tw-text-opacity: 1; @@ -3025,8 +3226,8 @@ input.tab:checked + .tab-content, outline-offset: 2px; } -.menu li > *:not(ul):not(.menu-title):not(details):active, -.menu li > *:not(ul):not(.menu-title):not(details).active, +.menu li > *:not(ul, .menu-title, details, .btn):active, +.menu li > *:not(ul, .menu-title, details, .btn).active, .menu li > details > summary:active { --tw-bg-opacity: 1; background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); @@ -3721,6 +3922,14 @@ input.tab:checked + .tab-content, width: 320px; } +.badge-lg { + height: 1.5rem; + font-size: 1rem; + line-height: 1.5rem; + padding-left: 0.688rem; + padding-right: 0.688rem; +} + .btm-nav-xs > *:where(.active) { border-top-width: 1px; } @@ -3753,6 +3962,14 @@ input.tab:checked + .tab-content, font-size: 0.875rem; } +.btn-md { + height: 3rem; + min-height: 3rem; + padding-left: 1rem; + padding-right: 1rem; + font-size: 0.875rem; +} + .btn-lg { height: 4rem; min-height: 4rem; @@ -3843,6 +4060,15 @@ input.tab:checked + .tab-content, visibility: visible; } +.drawer-open > .drawer-side { + overflow-y: auto; +} + +html:has(.drawer-toggle:checked) { + overflow-y: hidden; + scrollbar-gutter: stable; +} + .join.join-vertical { flex-direction: column; } @@ -4009,8 +4235,7 @@ input.tab:checked + .tab-content, padding-bottom: 0.25rem; } -.menu-sm :where(li:not(.menu-title) > *:not(ul):not(details):not(.menu-title)), - .menu-sm :where(li:not(.menu-title) > details > summary:not(.menu-title)) { +.menu-sm :where(li:not(.menu-title) > *:not(ul, details, .menu-title)), .menu-sm :where(li:not(.menu-title) > details > summary:not(.menu-title)) { border-radius: var(--rounded-btn, 0.5rem); padding-left: 0.75rem; padding-right: 0.75rem; @@ -4187,6 +4412,10 @@ input.tab:checked + .tab-content, visibility: visible; } +.collapse { + visibility: collapse; +} + .static { position: static; } @@ -4263,6 +4492,10 @@ input.tab:checked + .tab-content, float: left; } +.m-1 { + margin: 0.25rem; +} + .m-auto { margin: auto; } @@ -4353,7 +4586,7 @@ input.tab:checked + .tab-content, } .mb-8 { - margin-bottom: 2rem; + margin-bottom: 2rem; } .me-2 { @@ -4376,6 +4609,10 @@ input.tab:checked + .tab-content, margin-left: 0.5rem; } +.mr-1 { + margin-right: 0.25rem; +} + .mr-2 { margin-right: 0.5rem; } @@ -4384,6 +4621,10 @@ input.tab:checked + .tab-content, margin-right: 0.75rem; } +.mr-4 { + margin-right: 1rem; +} + .ms-2 { margin-inline-start: 0.5rem; } @@ -4569,7 +4810,7 @@ input.tab:checked + .tab-content, } .w-24 { - width: 6rem; + width: 6rem; } .w-3\/4 { @@ -4645,13 +4886,21 @@ input.tab:checked + .tab-content, } .max-w-0 { - max-width: 0rem; + max-width: 0px; +} + +.max-w-12 { + max-width: 3rem; } .max-w-3xl { max-width: 48rem; } +.max-w-44 { + max-width: 11rem; +} + .max-w-4xl { max-width: 56rem; } @@ -4720,6 +4969,10 @@ input.tab:checked + .tab-content, grid-template-columns: repeat(3, minmax(0, 1fr)); } +.grid-cols-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + .flex-row { flex-direction: row; } @@ -4783,6 +5036,16 @@ input.tab:checked + .tab-content, row-gap: 0.25rem; } +.gap-y-4 { + row-gap: 1rem; +} + +.space-x-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.5rem * var(--tw-space-x-reverse)); + margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); +} + .space-x-4 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(1rem * var(--tw-space-x-reverse)); @@ -4863,10 +5126,22 @@ input.tab:checked + .tab-content, border-width: 1px; } +.border-4 { + border-width: 4px; +} + +.border-2 { + border-width: 2px; +} + .border-t-2 { border-top-width: 2px; } +.border-s { + border-inline-start-width: 1px; +} + .border-none { border-style: none; } @@ -4905,6 +5180,16 @@ input.tab:checked + .tab-content, border-color: transparent; } +.border-error { + --tw-border-opacity: 1; + border-color: var(--fallback-er, oklch(var(--er)/var(--tw-border-opacity))); +} + +.border-success { + --tw-border-opacity: 1; + border-color: var(--fallback-su, oklch(var(--su)/var(--tw-border-opacity))); +} + .bg-base-100 { --tw-bg-opacity: 1; background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); @@ -5328,11 +5613,6 @@ input.tab:checked + .tab-content, --tw-ring-offset-color: var(--fallback-b1,oklch(var(--b1)/1)); } -.blur { - --tw-blur: blur(8px); - filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); -} - .drop-shadow { --tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow(0 1px 1px rgb(0 0 0 / 0.06)); filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); @@ -5568,7 +5848,7 @@ input.tab:checked + .tab-content, } @media (hover:hover) { - .dropdown ul.dropdown-content li > *:not(ul):not(.menu-title):not(details):active,.dropdown ul.dropdown-content li > *:not(ul):not(.menu-title):not(details).active,.dropdown ul.dropdown-content li > details > summary:active { + .dropdown ul.dropdown-content li > *:not(ul, .menu-title, details, .btn):active,.dropdown ul.dropdown-content li > *:not(ul, .menu-title, details, .btn).active,.dropdown ul.dropdown-content li > details > summary:active { --tw-bg-opacity: 1; background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); --tw-text-opacity: 1; @@ -5577,50 +5857,50 @@ input.tab:checked + .tab-content, } @media (hover: hover) { - :where(.dropdown ul.dropdown-content li:not(.menu-title):not(.disabled) > *:not(ul):not(details):not(.menu-title)):not(.active):hover, :where(.dropdown ul.dropdown-content li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):not(.active):hover { + :where(.dropdown ul.dropdown-content li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(.active, .btn):hover, :where(.dropdown ul.dropdown-content li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(.active, .btn):hover { cursor: pointer; outline: 2px solid transparent; outline-offset: 2px; } @supports (color: oklch(0 0 0)) { - :where(.dropdown ul.dropdown-content li:not(.menu-title):not(.disabled) > *:not(ul):not(details):not(.menu-title)):not(.active):hover, :where(.dropdown ul.dropdown-content li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):not(.active):hover { + :where(.dropdown ul.dropdown-content li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(.active, .btn):hover, :where(.dropdown ul.dropdown-content li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(.active, .btn):hover { background-color: var(--fallback-bc,oklch(var(--bc)/0.1)); } } - :where(.dropdown ul.dropdown-content li:not(.menu-title):not(.disabled) > *:not(ul):not(details):not(.menu-title)):not(.active):hover, :where(.dropdown ul.dropdown-content li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):not(.active):hover { + :where(.dropdown ul.dropdown-content li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(.active, .btn):hover, :where(.dropdown ul.dropdown-content li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(.active, .btn):hover { cursor: pointer; outline: 2px solid transparent; outline-offset: 2px; } @supports (color: oklch(0 0 0)) { - :where(.dropdown ul.dropdown-content li:not(.menu-title):not(.disabled) > *:not(ul):not(details):not(.menu-title)):not(.active):hover, :where(.dropdown ul.dropdown-content li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):not(.active):hover { + :where(.dropdown ul.dropdown-content li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(.active, .btn):hover, :where(.dropdown ul.dropdown-content li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(.active, .btn):hover { background-color: var(--fallback-bc,oklch(var(--bc)/0.1)); } } - :where(.dropdown ul.dropdown-content li:not(.menu-title):not(.disabled) > *:not(ul):not(details):not(.menu-title)):not(.active):hover, :where(.dropdown ul.dropdown-content li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):not(.active):hover { + :where(.dropdown ul.dropdown-content li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(.active, .btn):hover, :where(.dropdown ul.dropdown-content li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(.active, .btn):hover { cursor: pointer; outline: 2px solid transparent; outline-offset: 2px; } @supports (color: oklch(0 0 0)) { - :where(.dropdown ul.dropdown-content li:not(.menu-title):not(.disabled) > *:not(ul):not(details):not(.menu-title)):not(.active):hover, :where(.dropdown ul.dropdown-content li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):not(.active):hover { + :where(.dropdown ul.dropdown-content li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(.active, .btn):hover, :where(.dropdown ul.dropdown-content li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(.active, .btn):hover { background-color: var(--fallback-bc,oklch(var(--bc)/0.1)); } } - :where(.dropdown ul.dropdown-content li:not(.menu-title):not(.disabled) > *:not(ul):not(details):not(.menu-title)):not(.active):hover, :where(.dropdown ul.dropdown-content li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):not(.active):hover { + :where(.dropdown ul.dropdown-content li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(.active, .btn):hover, :where(.dropdown ul.dropdown-content li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(.active, .btn):hover { cursor: pointer; outline: 2px solid transparent; outline-offset: 2px; } @supports (color: oklch(0 0 0)) { - :where(.dropdown ul.dropdown-content li:not(.menu-title):not(.disabled) > *:not(ul):not(details):not(.menu-title)):not(.active):hover, :where(.dropdown ul.dropdown-content li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):not(.active):hover { + :where(.dropdown ul.dropdown-content li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(.active, .btn):hover, :where(.dropdown ul.dropdown-content li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(.active, .btn):hover { background-color: var(--fallback-bc,oklch(var(--bc)/0.1)); } } @@ -5642,7 +5922,7 @@ input.tab:checked + .tab-content, padding-inline-start: 0.5rem; } -.dropdown ul.dropdown-content :where(li:not(.menu-title) > *:not(ul):not(details):not(.menu-title)),.dropdown ul.dropdown-content :where(li:not(.menu-title) > details > summary:not(.menu-title)) { +.dropdown ul.dropdown-content :where(li:not(.menu-title) > *:not(ul, details, .menu-title, .btn)),.dropdown ul.dropdown-content :where(li:not(.menu-title) > details > summary:not(.menu-title)) { display: grid; grid-auto-flow: column; align-content: flex-start; @@ -5699,7 +5979,7 @@ input.tab:checked + .tab-content, content: ""; } -.dropdown ul.dropdown-content :where(li:not(.menu-title) > *:not(ul):not(details):not(.menu-title)),.dropdown ul.dropdown-content :where(li:not(.menu-title) > details > summary:not(.menu-title)) { +.dropdown ul.dropdown-content :where(li:not(.menu-title) > *:not(ul, details, .menu-title, .btn)),.dropdown ul.dropdown-content :where(li:not(.menu-title) > details > summary:not(.menu-title)) { transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-timing-function: cubic-bezier(0, 0, 0.2, 1); border-radius: var(--rounded-btn, 0.5rem); @@ -5717,12 +5997,7 @@ input.tab:checked + .tab-content, text-wrap: balance; } -:where(.dropdown ul.dropdown-content li:not(.menu-title):not(.disabled) > *:not(ul):not(details):not(.menu-title)):not(summary):not(.active).focus, - :where(.dropdown ul.dropdown-content li:not(.menu-title):not(.disabled) > *:not(ul):not(details):not(.menu-title)):not(summary):not(.active):focus, - :where(.dropdown ul.dropdown-content li:not(.menu-title):not(.disabled) > *:not(ul):not(details):not(.menu-title)):is(summary):not(.active):focus-visible, - :where(.dropdown ul.dropdown-content li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):not(summary):not(.active).focus, - :where(.dropdown ul.dropdown-content li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):not(summary):not(.active):focus, - :where(.dropdown ul.dropdown-content li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):is(summary):not(.active):focus-visible { +:where(.dropdown ul.dropdown-content li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(summary, .active, .btn).focus, :where(.dropdown ul.dropdown-content li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(summary, .active, .btn):focus, :where(.dropdown ul.dropdown-content li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):is(summary):not(.active, .btn):focus-visible, :where(.dropdown ul.dropdown-content li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(summary, .active, .btn).focus, :where(.dropdown ul.dropdown-content li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(summary, .active, .btn):focus, :where(.dropdown ul.dropdown-content li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):is(summary):not(.active, .btn):focus-visible { cursor: pointer; background-color: var(--fallback-bc,oklch(var(--bc)/0.1)); --tw-text-opacity: 1; @@ -5731,7 +6006,7 @@ input.tab:checked + .tab-content, outline-offset: 2px; } -.dropdown ul.dropdown-content li > *:not(ul):not(.menu-title):not(details):active,.dropdown ul.dropdown-content li > *:not(ul):not(.menu-title):not(details).active,.dropdown ul.dropdown-content li > details > summary:active { +.dropdown ul.dropdown-content li > *:not(ul, .menu-title, details, .btn):active,.dropdown ul.dropdown-content li > *:not(ul, .menu-title, details, .btn).active,.dropdown ul.dropdown-content li > details > summary:active { --tw-bg-opacity: 1; background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); --tw-text-opacity: 1; @@ -6726,76 +7001,6 @@ button.btn.loading-htmx.htmx-request { display: block; } -@media (prefers-color-scheme: dark) { - .dark\:block { - display: block; - } - - .dark\:hidden { - display: none; - } - - .dark\:divide-gray-700 > :not([hidden]) ~ :not([hidden]) { - --tw-divide-opacity: 1; - border-color: rgb(55 65 81 / var(--tw-divide-opacity)); - } - - .dark\:border-gray-600 { - --tw-border-opacity: 1; - border-color: rgb(75 85 99 / var(--tw-border-opacity)); - } - - .dark\:border-gray-700 { - --tw-border-opacity: 1; - border-color: rgb(55 65 81 / var(--tw-border-opacity)); - } - - .dark\:bg-gray-700 { - --tw-bg-opacity: 1; - background-color: rgb(55 65 81 / var(--tw-bg-opacity)); - } - - .dark\:bg-gray-800 { - --tw-bg-opacity: 1; - background-color: rgb(31 41 55 / var(--tw-bg-opacity)); - } - - .dark\:bg-gray-900 { - --tw-bg-opacity: 1; - background-color: rgb(17 24 39 / var(--tw-bg-opacity)); - } - - .dark\:text-gray-200 { - --tw-text-opacity: 1; - color: rgb(229 231 235 / var(--tw-text-opacity)); - } - - .dark\:text-gray-300 { - --tw-text-opacity: 1; - color: rgb(209 213 219 / var(--tw-text-opacity)); - } - - .dark\:text-gray-400 { - --tw-text-opacity: 1; - color: rgb(156 163 175 / var(--tw-text-opacity)); - } - - .dark\:text-purple-400 { - --tw-text-opacity: 1; - color: rgb(192 132 252 / var(--tw-text-opacity)); - } - - .dark\:text-white { - --tw-text-opacity: 1; - color: rgb(255 255 255 / var(--tw-text-opacity)); - } - - .peer:focus ~ .dark\:peer-focus\:ring-blue-800 { - --tw-ring-opacity: 1; - --tw-ring-color: rgb(30 64 175 / var(--tw-ring-opacity)); - } -} - @media (min-width: 640px) { .sm\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -6926,6 +7131,76 @@ button.btn.loading-htmx.htmx-request { } } +@media (prefers-color-scheme: dark) { + .dark\:block { + display: block; + } + + .dark\:hidden { + display: none; + } + + .dark\:divide-gray-700 > :not([hidden]) ~ :not([hidden]) { + --tw-divide-opacity: 1; + border-color: rgb(55 65 81 / var(--tw-divide-opacity)); + } + + .dark\:border-gray-600 { + --tw-border-opacity: 1; + border-color: rgb(75 85 99 / var(--tw-border-opacity)); + } + + .dark\:border-gray-700 { + --tw-border-opacity: 1; + border-color: rgb(55 65 81 / var(--tw-border-opacity)); + } + + .dark\:bg-gray-700 { + --tw-bg-opacity: 1; + background-color: rgb(55 65 81 / var(--tw-bg-opacity)); + } + + .dark\:bg-gray-800 { + --tw-bg-opacity: 1; + background-color: rgb(31 41 55 / var(--tw-bg-opacity)); + } + + .dark\:bg-gray-900 { + --tw-bg-opacity: 1; + background-color: rgb(17 24 39 / var(--tw-bg-opacity)); + } + + .dark\:text-gray-200 { + --tw-text-opacity: 1; + color: rgb(229 231 235 / var(--tw-text-opacity)); + } + + .dark\:text-gray-300 { + --tw-text-opacity: 1; + color: rgb(209 213 219 / var(--tw-text-opacity)); + } + + .dark\:text-gray-400 { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); + } + + .dark\:text-purple-400 { + --tw-text-opacity: 1; + color: rgb(192 132 252 / var(--tw-text-opacity)); + } + + .dark\:text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); + } + + .peer:focus ~ .dark\:peer-focus\:ring-blue-800 { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(30 64 175 / var(--tw-ring-opacity)); + } +} + .peer:checked~* .\[\.peer\:checked\~\*_\&\]\:decoration-gray-400 { text-decoration-color: #9ca3af; } \ No newline at end of file diff --git a/frontend/templates/pages/auth/login.html b/frontend/templates/pages/auth/login.html index ebbff352b..3f47fe592 100644 --- a/frontend/templates/pages/auth/login.html +++ b/frontend/templates/pages/auth/login.html @@ -1,4 +1,5 @@ -
+
{% if email %}
@@ -17,13 +18,14 @@ class="input input-bordered w-full"> +
OR
{% if magic_links_enabled %}
- £{{ invoice.amount | floatformat:2 | intcomma }} + £{{ invoice.amount | default_if_none:0 | floatformat:2 | intcomma }} {% component "pages:invoices:dashboard:payment_status_badge" status=invoice.payment_status inv_id=invoice.id %} @@ -37,6 +37,12 @@