diff --git a/README.md b/README.md index c4941827..2976730d 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ Each example runs on a different port. Run multiple examples simultaneously and ### Chapter 10 - [Our First JSON Endpoint: Listing All Contacts](./chapter-10/1-listing-all-contacts) +- [Adding Contacts](./chapter-10/2-adding-contacts) ## Support diff --git a/chapter-10/2-adding-contacts/app.py b/chapter-10/2-adding-contacts/app.py new file mode 100644 index 00000000..98a24076 --- /dev/null +++ b/chapter-10/2-adding-contacts/app.py @@ -0,0 +1,164 @@ +from contacts_model import Contact, Archiver +from flask import Flask, flash, redirect, render_template, request, send_file +from typing import Any +from werkzeug.wrappers import response + +Contact.load_db() + +app: Flask = Flask(__name__) + +app.secret_key = b"it is over" + + +@app.route("/") +def index() -> response.Response: + return redirect("/contacts") + + +@app.route("/contacts") +def contacts() -> str: + search: str | None = request.args.get("q") + page: int = int(request.args.get("page", 1)) + if search is not None: + contacts_set: list = Contact.search(search) + if request.headers.get("HX-Trigger") == "search": + return render_template("rows.html", contacts=contacts_set) + else: + contacts_set = Contact.all(page) + return render_template( + "index.html", contacts=contacts_set, page=page, archiver=Archiver.get() + ) + + +@app.route("/contacts/count") +def contacts_count() -> str: + count: int = Contact.count() + return f"({str(count)} total Contacts)" + + +@app.route("/contacts/new", methods=["GET"]) +def contacts_new_get() -> str: + return render_template("new.html", contact=Contact()) + + +@app.route("/contacts/new", methods=["POST"]) +def contacts_new() -> response.Response | str: + c: Contact = Contact( + None, + request.form["first_name"], + request.form["last_name"], + request.form["phone"], + request.form["email"], + ) + if c.save(): + flash("Created New Contact!") + return redirect("/contacts") + else: + return render_template("new.html", contact=c) + + +@app.route("/contacts/") +def contacts_view(contact_id: int = 0) -> str: + contact: Any | None = Contact.find(contact_id) + return render_template("show.html", contact=contact) + + +@app.route("/contacts//edit", methods=["GET"]) +def contacts_edit_get(contact_id: int = 0) -> str: + contact: Any | None = Contact.find(contact_id) + return render_template("edit.html", contact=contact) + + +@app.route("/contacts//edit", methods=["POST"]) +def contacts_edit_post(contact_id: int = 0) -> response.Response | str: + c: Any = Contact.find(contact_id) + c.update( + request.form["first_name"], + request.form["last_name"], + request.form["phone"], + request.form["email"], + ) + if c.save(): + flash("Updated Contact!") + return redirect("/contacts/" + str(contact_id)) + else: + return render_template("edit.html", contact=c) + + +@app.route("/contacts//email", methods=["GET"]) +def contacts_email_get(contact_id=0) -> response.Response | str: + c: Any = Contact.find(contact_id) + c.email = request.args.get("email") + c.validate() + return c.errors.get("email") or "" + + +@app.route("/contacts/", methods=["DELETE"]) +def contacts_delete(contact_id: int = 0) -> response.Response | str: + contact: Any | None = Contact.find(contact_id) + if contact is not None: + contact.delete() + if request.headers.get("HX-Trigger") == "delete-btn": + flash("Deleted Contact!") + return redirect("/contacts", 303) + else: + return "" + + +@app.route("/contacts/", methods=["DELETE"]) +def contacts_delete_all() -> str: + contact_ids: list = list(map(int, request.form.getlist("selected_contact_ids"))) + for contact_id in contact_ids: + contact: Any | None = Contact.find(contact_id) + if contact is not None: + contact.delete() + flash("Deleted Contacts!") + contacts_set: list = Contact.all() + return render_template("index.html", contacts=contacts_set, page=1) + + +@app.route("/contacts/archive", methods=["POST"]) +def start_archive() -> str: + archiver = Archiver.get() + archiver.run() + return render_template("archive_ui.html", archiver=archiver) + + +@app.route("/contacts/archive", methods=["GET"]) +def archive_status() -> str: + archiver = Archiver.get() + return render_template("archive_ui.html", archiver=archiver) + + +@app.route("/contacts/archive/file", methods=["GET"]) +def archive_content() -> response.Response: + manager = Archiver.get() + return send_file(manager.archive_file(), "archive.json", as_attachment=True) + + +@app.route("/contacts/archive", methods=["DELETE"]) +def reset_archive() -> str: + archiver = Archiver.get() + archiver.reset() + return render_template("archive_ui.html", archiver=archiver) + + +@app.route("/api/v1/contacts", methods=["GET"]) +def json_contacts() -> dict: + contacts_set: list = Contact.all() + contacts_dicts: list = [c.__dict__ for c in contacts_set] + return {"contacts": contacts_dicts} + + +@app.route("/api/v1/contacts", methods=["POST"]) +def json_contacts_new() -> tuple[dict, int]: + c: Contact = Contact(None, request.form.get('first_name'), request.form.get('last_name'), request.form.get('phone'), + request.form.get('email')) + if c.save(): + return c.__dict__, 200 + else: + return {"errors": c.errors}, 400 + + +if __name__ == "__main__": + app.run(port=5062) diff --git a/chapter-10/2-adding-contacts/contacts.json b/chapter-10/2-adding-contacts/contacts.json new file mode 100644 index 00000000..3b5e0192 --- /dev/null +++ b/chapter-10/2-adding-contacts/contacts.json @@ -0,0 +1,162 @@ +[ + { + "id": 2, + "first": "Carson", + "last": "Gross", + "phone": "123-456-7890", + "email": "carson@example.comz", + "errors": {} + }, + { + "id": 3, + "first": "", + "last": "", + "phone": "", + "email": "joe@example2.com", + "errors": {} + }, + { + "id": 5, + "first": "Joe", + "last": "Blow", + "phone": "123-456-7890", + "email": "joe@example.com", + "errors": {} + }, + { + "id": 6, + "first": "Joe", + "last": "Blow", + "phone": "123-456-7890", + "email": "joe1@example.com", + "errors": {} + }, + { + "id": 7, + "first": "Joe", + "last": "Blow", + "phone": "123-456-7890", + "email": "joe2@example.com", + "errors": {} + }, + { + "id": 8, + "first": "Joe", + "last": "Blow", + "phone": "123-456-7890", + "email": "joe3@example.com", + "errors": {} + }, + { + "id": 9, + "first": "Joe", + "last": "Blow", + "phone": "123-456-7890", + "email": "joe4@example.com", + "errors": {} + }, + { + "id": 10, + "first": "Joe", + "last": "Blow", + "phone": "123-456-7890", + "email": "joe5@example.com", + "errors": {} + }, + { + "id": 11, + "first": "Joe", + "last": "Blow", + "phone": "123-456-7890", + "email": "joe6@example.com", + "errors": {} + }, + { + "id": 12, + "first": "Joe", + "last": "Blow", + "phone": "123-456-7890", + "email": "joe7@example.com", + "errors": {} + }, + { + "id": 13, + "first": "Joe", + "last": "Blow", + "phone": "123-456-7890", + "email": "joe8@example.com", + "errors": {} + }, + { + "id": 14, + "first": "Joe", + "last": "Blow", + "phone": "123-456-7890", + "email": "joe9@example.com", + "errors": {} + }, + { + "id": 15, + "first": "Joe", + "last": "Blow", + "phone": "123-456-7890", + "email": "joe10@example.com", + "errors": {} + }, + { + "id": 16, + "first": "Joe", + "last": "Blow", + "phone": "123-456-7890", + "email": "joe11@example.com", + "errors": {} + }, + { + "id": 17, + "first": "Joe", + "last": "Blow", + "phone": "123-456-7890", + "email": "joe12@example.com", + "errors": {} + }, + { + "id": 18, + "first": null, + "last": null, + "phone": null, + "email": "restexample1@example.com", + "errors": {} + }, + { + "id": 19, + "first": null, + "last": null, + "phone": null, + "email": "restexample2@example.com", + "errors": {} + }, + { + "id": 20, + "first": null, + "last": null, + "phone": null, + "email": "restexample3@example.com", + "errors": {} + }, + { + "id": 21, + "first": null, + "last": null, + "phone": null, + "email": "restexample4@example.com", + "errors": {} + }, + { + "id": 22, + "first": null, + "last": null, + "phone": null, + "email": "restexample5@example.com", + "errors": {} + } +] \ No newline at end of file diff --git a/chapter-10/2-adding-contacts/contacts_model.py b/chapter-10/2-adding-contacts/contacts_model.py new file mode 100644 index 00000000..7d157b6f --- /dev/null +++ b/chapter-10/2-adding-contacts/contacts_model.py @@ -0,0 +1,156 @@ +import json +from operator import attrgetter +from random import random +from threading import Thread +import time +from typing import Any + + +PAGE_SIZE: int = 10 + + +class Contact: + db: dict = {} + + def __init__( + self, + id_=None, + first=None, + last=None, + phone=None, + email=None, + ) -> None: + self.id = id_ + self.first = first + self.last = last + self.phone = phone + self.email = email + self.errors: dict = {} + + def __str__(self) -> str: + return json.dumps(self.__dict__, ensure_ascii=False) + + def update(self, first: str, last: str, phone: str, email: str) -> None: + self.first = first + self.last = last + self.phone = phone + self.email = email + + def validate(self) -> bool: + if not self.email: + self.errors["email"] = "Email Required" + existing_contact = next( + filter( + lambda c: c.id != self.id and c.email == self.email, Contact.db.values() + ), + None, + ) + if existing_contact: + self.errors["email"] = "Email Must Be Unique" + return len(self.errors) == 0 + + def save(self) -> bool: + if not self.validate(): + return False + if self.id is None: + if len(Contact.db) == 0: + max_id: int = 1 + else: + max_id = max(contact.id for contact in Contact.db.values()) + self.id = max_id + 1 + Contact.db[self.id] = self + Contact.save_db() + return True + + def delete(self) -> bool: + del Contact.db[self.id] + Contact.save_db() + return True + + @classmethod + def count(cls) -> int: + time.sleep(2) + return len(cls.db) + + @classmethod + def all(cls, page: int = 1) -> list: + start: int = (page - 1) * PAGE_SIZE + end: int = start + PAGE_SIZE + return list(cls.db.values())[start:end] + + @classmethod + def search(cls, text: str) -> list: + result: list = [] + for c in cls.db.values(): + match_first: bool = c.first is not None and text in c.first + match_last: bool = c.last is not None and text in c.last + match_email: bool = c.email is not None and text in c.email + match_phone: bool = c.phone is not None and text in c.phone + if match_first or match_last or match_email or match_phone: + result.append(c) + return result + + @classmethod + def load_db(cls) -> None: + with open("contacts.json", "r") as contacts_file: + contacts: Any = json.load(contacts_file) + cls.db.clear() + for c in contacts: + cls.db[c["id"]] = Contact( + c["id"], c["first"], c["last"], c["phone"], c["email"] + ) + + @staticmethod + def save_db() -> None: + out_arr: list = [c.__dict__ for c in Contact.db.values()] + with open("contacts.json", "w") as f: + json.dump(out_arr, f, indent=2) + + @classmethod + def find(cls, id_: int) -> Any | None: + id_ = int(id_) + c: Any | None = cls.db.get(id_) + if c is not None: + c.errors = {} + return c + + +class Archiver: + archive_status: str = "Waiting" + archive_progress: float = 0 + thread: Thread | None = None + + def status(self) -> str: + return Archiver.archive_status + + def progress(self) -> float: + return Archiver.archive_progress + + def run(self) -> None: + if Archiver.archive_status == "Waiting": + Archiver.archive_status = "Running" + Archiver.archive_progress = 0 + Archiver.thread = Thread(target=self.run_impl) + Archiver.thread.start() + + def run_impl(self) -> None: + for i in range(10): + time.sleep(1 * random()) + if Archiver.archive_status != "Running": + return + Archiver.archive_progress = (i + 1) / 10 + print("Here... " + str(Archiver.archive_progress)) + time.sleep(1) + if Archiver.archive_status != "Running": + return + Archiver.archive_status = "Complete" + + def archive_file(self) -> str: + return "contacts.json" + + def reset(self) -> None: + Archiver.archive_status = "Waiting" + + @classmethod + def get(cls): + return Archiver() diff --git a/chapter-10/2-adding-contacts/static/css/missing.min.css b/chapter-10/2-adding-contacts/static/css/missing.min.css new file mode 100644 index 00000000..bf62c755 --- /dev/null +++ b/chapter-10/2-adding-contacts/static/css/missing.min.css @@ -0,0 +1,127 @@ +@keyframes bg{0%{background:0 0}}*,::after,::before{box-sizing:border-box;background-repeat:no-repeat}::after,::before{text-decoration:inherit;vertical-align:inherit}:root{cursor:default;overflow-wrap:break-word;-webkit-tap-highlight-color:transparent;text-size-adjust:none;-webkit-text-size-adjust:none}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}audio,canvas,iframe,img,svg,video{vertical-align:middle}svg:not([fill]){fill:currentColor}table{border-collapse:collapse;border-color:currentColor;text-indent:0;font-variant-numeric:tabular-nums;font:inherit}body,button,input,select,textarea{margin:0}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}fieldset{border:1px solid #a0a0a0;position:relative;padding:var(--gap);margin:var(--gap) 0;width:100%;border-radius:var(--border-radius);border:1px solid var(--graphical-fg)}progress{vertical-align:baseline}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-inner-spin-button,::-webkit-outer-spin-button{block-size:auto}::-webkit-input-placeholder{color:inherit;opacity:.54}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}[hidden],datalist{display:none!important}:focus-visible{outline:.2em solid var(--accent);z-index:32}body:focus-visible,html:focus-visible,iframe:focus-visible{outline:0}:target{outline:.2em solid var(--fg);z-index:2}details>summary:first-of-type{display:list-item}[aria-busy=true]{cursor:progress}[aria-disabled=true],[disabled]{cursor:not-allowed}:root{--gray-0: #f8fafb;--gray-1: #f2f4f6;--gray-2: #ebedef;--gray-3: #e0e4e5;--gray-4: #d1d6d8;--gray-5: #b1b6b9;--gray-6: #979b9d;--gray-7: #7e8282;--gray-8: #666968;--gray-9: #50514f;--gray-10: #3a3a37;--gray-11: #252521;--gray-12: #121210;--red-0: #fff5f5;--red-1: #ffe3e3;--red-2: #ffc9c9;--red-3: #ffa8a8;--red-4: #ff8787;--red-5: #ff6b6b;--red-6: #fa5252;--red-7: #f03e3e;--red-8: #e03131;--red-9: #c92a2a;--red-10: #b02525;--red-11: #962020;--red-12: #7d1a1a;--pink-0: #fff0f6;--pink-1: #ffdeeb;--pink-2: #fcc2d7;--pink-3: #faa2c1;--pink-4: #f783ac;--pink-5: #f06595;--pink-6: #e64980;--pink-7: #d6336c;--pink-8: #c2255c;--pink-9: #a61e4d;--pink-10: #8c1941;--pink-11: #731536;--pink-12: #59102a;--purple-0: #f8f0fc;--purple-1: #f3d9fa;--purple-2: #eebefa;--purple-3: #e599f7;--purple-4: #da77f2;--purple-5: #cc5de8;--purple-6: #be4bdb;--purple-7: #ae3ec9;--purple-8: #9c36b5;--purple-9: #862e9c;--purple-10: #702682;--purple-11: #5a1e69;--purple-12: #44174f;--violet-0: #f3f0ff;--violet-1: #e5dbff;--violet-2: #d0bfff;--violet-3: #b197fc;--violet-4: #9775fa;--violet-5: #845ef7;--violet-6: #7950f2;--violet-7: #7048e8;--violet-8: #6741d9;--violet-9: #5f3dc4;--violet-10: #5235ab;--violet-11: #462d91;--violet-12: #3a2578;--indigo-0: #edf2ff;--indigo-1: #dbe4ff;--indigo-2: #bac8ff;--indigo-3: #91a7ff;--indigo-4: #748ffc;--indigo-5: #5c7cfa;--indigo-6: #4c6ef5;--indigo-7: #4263eb;--indigo-8: #3b5bdb;--indigo-9: #364fc7;--indigo-10: #2f44ad;--indigo-11: #283a94;--indigo-12: #21307a;--blue-0: #e7f5ff;--blue-1: #d0ebff;--blue-2: #a5d8ff;--blue-3: #74c0fc;--blue-4: #4dabf7;--blue-5: #339af0;--blue-6: #228be6;--blue-7: #1c7ed6;--blue-8: #1971c2;--blue-9: #1864ab;--blue-10: #145591;--blue-11: #114678;--blue-12: #0d375e;--cyan-0: #e3fafc;--cyan-1: #c5f6fa;--cyan-2: #99e9f2;--cyan-3: #66d9e8;--cyan-4: #3bc9db;--cyan-5: #22b8cf;--cyan-6: #15aabf;--cyan-7: #1098ad;--cyan-8: #0c8599;--cyan-9: #0b7285;--cyan-10: #095c6b;--cyan-11: #074652;--cyan-12: #053038;--teal-0: #e6fcf5;--teal-1: #c3fae8;--teal-2: #96f2d7;--teal-3: #63e6be;--teal-4: #38d9a9;--teal-5: #20c997;--teal-6: #12b886;--teal-7: #0ca678;--teal-8: #099268;--teal-9: #087f5b;--teal-10: #066649;--teal-11: #054d37;--teal-12: #033325;--green-0: #ebfbee;--green-1: #d3f9d8;--green-2: #b2f2bb;--green-3: #8ce99a;--green-4: #69db7c;--green-5: #51cf66;--green-6: #40c057;--green-7: #37b24d;--green-8: #2f9e44;--green-9: #2b8a3e;--green-10: #237032;--green-11: #1b5727;--green-12: #133d1b;--lime-0: #f4fce3;--lime-1: #e9fac8;--lime-2: #d8f5a2;--lime-3: #c0eb75;--lime-4: #a9e34b;--lime-5: #94d82d;--lime-6: #82c91e;--lime-7: #74b816;--lime-8: #66a80f;--lime-9: #5c940d;--lime-10: #4c7a0b;--lime-11: #3c6109;--lime-12: #2c4706;--yellow-0: #fff9db;--yellow-1: #fff3bf;--yellow-2: #ffec99;--yellow-3: #ffe066;--yellow-4: #ffd43b;--yellow-5: #fcc419;--yellow-6: #fab005;--yellow-7: #f59f00;--yellow-8: #f08c00;--yellow-9: #e67700;--yellow-10: #b35c00;--yellow-11: #804200;--yellow-12: #663500;--orange-0: #fff4e6;--orange-1: #ffe8cc;--orange-2: #ffd8a8;--orange-3: #ffc078;--orange-4: #ffa94d;--orange-5: #ff922b;--orange-6: #fd7e14;--orange-7: #f76707;--orange-8: #e8590c;--orange-9: #d9480f;--orange-10: #bf400d;--orange-11: #99330b;--orange-12: #802b09;--choco-0: #fff8dc;--choco-1: #fce1bc;--choco-2: #f7ca9e;--choco-3: #f1b280;--choco-4: #e99b62;--choco-5: #df8545;--choco-6: #d46e25;--choco-7: #bd5f1b;--choco-8: #a45117;--choco-9: #8a4513;--choco-10: #703a13;--choco-11: #572f12;--choco-12: #3d210d;--brown-0: #faf4eb;--brown-1: #ede0d1;--brown-2: #e0cab7;--brown-3: #d3b79e;--brown-4: #c5a285;--brown-5: #b78f6d;--brown-6: #a87c56;--brown-7: #956b47;--brown-8: #825b3a;--brown-9: #6f4b2d;--brown-10:#5e3a21;--brown-11:#4e2b15;--brown-12: #422412;--sand-0: #f8fafb;--sand-1: #e6e4dc;--sand-2: #d5cfbd;--sand-3: #c2b9a0;--sand-4: #aea58c;--sand-5: #9a9178;--sand-6: #867c65;--sand-7: #736a53;--sand-8: #5f5746;--sand-9: #4b4639;--sand-10:#38352d;--sand-11:#252521;--sand-12: #121210;--camo-0: #f9fbe7;--camo-1: #e8ed9c;--camo-2: #d2df4e;--camo-3: #c2ce34;--camo-4: #b5bb2e;--camo-5: #a7a827;--camo-6: #999621;--camo-7: #8c851c;--camo-8: #7e7416;--camo-9: #6d6414;--camo-10: #5d5411;--camo-11: #4d460e;--camo-12: #36300a;--jungle-0: #ecfeb0;--jungle-1: #def39a;--jungle-2: #d0e884;--jungle-3: #c2dd6e;--jungle-4: #b5d15b;--jungle-5: #a8c648;--jungle-6: #9bbb36;--jungle-7: #8fb024;--jungle-8: #84a513;--jungle-9: #7a9908;--jungle-10: #658006;--jungle-11: #516605;--jungle-12: #3d4d04}html{font-family:var(--main-font);line-height:var(--rhythm);background:var(--bg);color:var(--fg);scroll-padding-block-start:calc(4*var(--gap))}footer,header,section+section{margin-block:calc(2*var(--gap))}aside.big,nav a{color:var(--accent)}nav a{text-decoration:none}aside{font-size:.8em;line-height:calc(var(--rhythm)*2/3);--gap: calc(var(--rhythm) * var(--density) * 2 / 3) +;border-block:1px solid var(--graphical-fg);padding-block:var(--gap)}aside>*{--rhythm: calc(var(--gap)) }aside h1,aside h2,aside h3,aside h4,aside h5,aside h6{font-size:1em;text-transform:none;letter-spacing:none}aside.big{background:0 0;border:0;-webkit-border-start:1px solid var(--muted-fg);border-inline-start:1px solid var(--muted-fg);border-radius:0;padding:0;-webkit-padding-start:var(--rhythm);padding-inline-start:var(--rhythm);font-style:italic}.\,.\,.\,.\,.\,.\,h1,h2,h3,h4,h5,h6{-webkit-margin-after:var(--gap);margin-block-end:var(--gap);font-family:var(--secondary-font);-webkit-margin-before:calc(2*var(--gap));margin-block-start:calc(2*var(--gap));position:relative}.\,.\,h1,h2{font-size:2em;text-transform:none;line-height:calc(2*var(--rhythm));letter-spacing:0}.\,h2{font-size:1.6em;line-height:calc(1.5*var(--rhythm))}.\,.\,.\,.\,h3,h4,h5,h6{font-size:1.17em;line-height:calc(1*var(--rhythm))}.\,.\,.\,h4,h5,h6{font-size:1em;text-transform:none;letter-spacing:0;-webkit-margin-before:var(--gap);margin-block-start:var(--gap)}h1+h2,h1:first-child,h2+h3,h2:first-child,h3+h4,h3:first-child,h4+h5,h4:first-child,h5+h6,h5:first-child,h6:first-child{-webkit-margin-before:var(--gap);margin-block-start:var(--gap)}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{outline:0}h1:target::before,h2:target::before,h3:target::before,h4:target::before,h5:target::before,h6:target::before{content:"";display:block;position:absolute;left:-.5em;width:4px;height:100%;background:var(--accent)}header{-webkit-border-after:1px solid var(--graphical-fg);border-block-end:1px solid var(--graphical-fg)}dt,footer,header{font-family:var(--secondary-font)}footer{font-size:.8em;line-height:calc(var(--rhythm)*2/3);-webkit-border-before:1px solid var(--graphical-fg);border-block-start:1px solid var(--graphical-fg)}footer>*{--rhythm: calc(var(--rhythm) * 2 / 3) }body>footer,body>header,main+footer{padding:var(--rhythm) calc((100% - var(--eff-line-length))/2)}address{--density: 0}hr{color:inherit;margin-inline:0;flex:0 1 0px;-webkit-border-start:1px solid var(--accent);border-inline-start:1px solid var(--accent);block-size:auto;-webkit-border-before:1px solid var(--accent);border-block-start:1px solid var(--accent);-webkit-border-after:none;border-block-end:none;-webkit-border-end:none;border-inline-end:none}blockquote,pre{line-height:var(--rhythm)}pre{font-family:var(--mono-font);tab-size:2;margin:var(--gap) 0;overflow-x:auto;scrollbar-width:thin;scrollbar-color:var(--accent) transparent;font-size:.9em}blockquote,dl,hr,ol,p,ul{margin-block:var(--gap)}blockquote{margin-inline:0 var(--gap);padding-inline:var(--gap) 0;font-size:1.1em;font-style:italic;-webkit-border-start:1px solid var(--graphical-fg);border-inline-start:1px solid var(--graphical-fg);color:var(--muted-fg)}.italic address,.italic cite,.italic dfn,.italic em,.italic i,.italic var,blockquote address,blockquote cite,blockquote dfn,blockquote em,blockquote i,blockquote var,q address,q cite,q dfn,q em,q i,q var{font-style:normal}blockquote footer{text-align:right;text-align:end}ol,ul{-webkit-padding-start:var(--rhythm);padding-inline-start:var(--rhythm)}ol ol,ol ul,ul ol,ul ul{-webkit-padding-start:var(--gap);padding-inline-start:var(--gap)}ol[role=list],ol[role=listbox],ul[role=list],ul[role=listbox]{-webkit-padding-start:0;padding-inline-start:0;list-style:none}ol{list-style:decimal}dt{font-weight:700}dd{-webkit-margin-start:var(--rhythm);margin-inline-start:var(--rhythm)}li::marker{font-family:var(--secondary-font)}figure{max-width:100%;margin-inline:0}figcaption{margin-block:var(--gap);font-family:var(--secondary-font);color:var(--muted-fg)}main{max-inline-size:var(--eff-line-length);inline-size:100%;margin-inline:auto}main:first-child{padding-top:var(--gap)}.\,a{color:var(--link-fg, var(--accent));border-radius:var(--border-radius);outline-offset:1px;background:0 0;border:0;font-size:1em;-webkit-text-decoration:1px dotted underline;text-decoration:1px dotted underline}.list-of-links :is(a,.\){text-decoration:none}:is(a,.\):focus,:is(a,.\):hover{-webkit-text-decoration:2px solid underline;text-decoration:2px solid underline;cursor:pointer;outline:0}small[role=note]{display:block;float:inline-end;clear:inline-end;--sidenote-width: 20ch;max-inline-size:var(--sidenote-width);padding-inline:1.5ch 1ch;-webkit-margin-end:calc(1em - var(--sidenote-width));margin-inline-end:calc(1em - var(--sidenote-width));-webkit-margin-after:var(--rhythm);margin-block-end:var(--rhythm);font-family:var(--secondary-font);background:var(--bg);border:1px solid transparent;transition:transform .1s ease-in-out}small[role=note]:focus-within,small[role=note]:hover{border:1px solid var(--graphical-fg);border-radius:var(--border-radius);transform:translateX(calc(0px - var(--sidenote-width) + min(var(--gutter-width),var(--sidenote-width))))}.\,small{font-size:.8em;line-height:calc(var(--rhythm)*2/3)}:is(small,.\)>*{--rhythm: calc(var(--rhythm) * 2 / 3) }del,s{color:var(--bad-fg)}caption,q{font-style:italic}time{font-variant-numeric:tabular-nums}code,kbd,samp{font-family:var(--mono-font);font-style:normal}ins,samp{color:var(--ok-fg)}kbd kbd{display:inline-block;padding:0 .3em;font-size:.8em;line-height:1.1em;background:var(--interactive-bg);border:1px outset var(--graphical-fg);border-block-end-width:3px;border-radius:var(--border-radius)}sub{vertical-align:bottom}sub,sup{line-height:1}mark{background:var(--warn-bg);color:var(--warn-fg)}ins{background:var(--ok-bg)}del{background:var(--bad-bg)}audio,embed,iframe,img,object,video{max-inline-size:100%;inline-size:max-content;block-size:auto}caption{text-align:start;font-family:var(--secondary-font)}tbody{border-block:1px solid var(--faded-fg)}select[multiple],sup,td,th{vertical-align:top}td:not(:last-child),th:not(:last-child){-webkit-padding-end:var(--rhythm);padding-inline-end:var(--rhythm)}th{font-family:var(--secondary-font);text-align:start}input{display:block}label input:not([specificity-hack]){display:inline;padding-block:0}.\,button,input::file-selector-button,input[type=button],input[type=reset],input[type=submit]{display:inline-block;padding:0 calc(var(--rhythm)/4);vertical-align:middle;box-sizing:border-box;font-size:.8rem;line-height:1.125em;font-family:var(--secondary-font);min-height:var(--rhythm);background:var(--interactive-bg);border:1px solid var(--muted-fg);box-shadow:0 2px 4px -2px var(--fg);border-radius:var(--border-radius);color:var(--fg);text-decoration:none;display:inline-flex;justify-content:center;align-items:center}:is(button,input[type="submit"],input[type="reset"],input[type="button"],input::file-selector-button,.\):focus-visible,:is(button,input[type="submit"],input[type="reset"],input[type="button"],input::file-selector-button,.\):hover{filter:brightness(1.1);box-shadow:0 3px 6px -2px var(--fg);text-decoration:none}:is(button,input[type="submit"],input[type="reset"],input[type="button"],input::file-selector-button,.\):active{box-shadow:none}:is(button,input[type="submit"],input[type="reset"],input[type="button"],input::file-selector-button,.\):active:is([aria-pressed], [aria-expanded]){color:var(--accent);box-shadow:0 1px 5px -1px var(--fg) inset}:is(button,input[type="submit"],input[type="reset"],input[type="button"],input::file-selector-button,.\):where([aria-pressed="true"], [aria-expanded="true"]){box-shadow:0 2px 4px -1px var(--fg) inset;background:var(--pressed-interactive-bg);color:var(--accent)}:is(button,input[type="submit"],input[type="reset"],input[type="button"],input::file-selector-button,.\):where([aria-pressed="true"], [aria-expanded="true"]):focus-visible,:is(button,input[type="submit"],input[type="reset"],input[type="button"],input::file-selector-button,.\):where([aria-pressed="true"], [aria-expanded="true"]):hover{box-shadow:0 1px 3px -1px var(--fg) inset}[disabled]:is(button,input[type="submit"],input[type="reset"],input[type="button"],input::file-selector-button,.\){color:var(--muted-fg);box-shadow:none}strong>:is(button,input[type="submit"],input[type="reset"],input[type="button"],input::file-selector-button,.\){background:var(--accent);color:var(--bg);border:0;font-weight:700}strong>[disabled]:is(button,input[type="submit"],input[type="reset"],input[type="button"],input::file-selector-button,.\){color:var(--muted-accent)}.big:is(button,input[type="submit"],input[type="reset"],input[type="button"],input::file-selector-button,.\){min-block-size:calc(1.5*var(--rhythm));font-size:1rem;padding-inline:calc(.5*var(--rhythm));line-height:var(--rhythm)}input:not([type]),input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{padding:calc(var(--rhythm)/4);font-size:1rem;line-height:inherit;font-family:var(--main-font);background:var(--bg);color:var(--fg);border:1px solid var(--graphical-fg);border-radius:var(--border-radius);vertical-align:top}:is(input:not([type]),input[type="text"],input[type="search"],input[type="tel"],input[type="url"],input[type="email"],input[type="password"],input[type="date"],input[type="month"],input[type="week"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="number"],select,textarea):focus-visible{border:1px solid var(--accent)}:is(input:not([type]),input[type="text"],input[type="search"],input[type="tel"],input[type="url"],input[type="email"],input[type="password"],input[type="date"],input[type="month"],input[type="week"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="number"],select,textarea)::placeholder{color:var(--muted-fg);opacity:1;text-align:end}input[type=range]{width:100%;padding:calc(var(--gap)/4)}input[type=color]{padding:0;margin:0;height:calc(1.5*var(--rhythm));border:0;background:0 0}input[type=file]{padding:calc(var(--gap)/4) 0;font:inherit;line-height:calc(var(--rhythm)/2)}input[type=file]::file-selector-button{margin-block:.1em 0;-webkit-margin-end:1ch;margin-inline-end:1ch}optgroup::before{color:var(--muted-fg);font-style:normal}label[for]{display:block;padding-block:calc(var(--gap)/4)}fieldset>legend+*{-webkit-margin-before:0;margin-block-start:0}details:not(specificity-hack){-webkit-padding-before:0;padding-block-start:0}details:not(specificity-hack):not([open]){-webkit-padding-after:0;padding-block-end:0}summary{margin:calc(0px - var(--gap));margin-bottom:0;padding-inline:var(--gap);font-family:var(--secondary-font);font-weight:700;cursor:pointer}summary:active,summary:focus-visible{filter:brightness(.8);outline:0}dialog{inline-inset:0;block-size:-moz-fit-content;block-size:fit-content;inline-size:-moz-fit-content;inline-size:fit-content;margin:auto!important;background-color:var(--bg);color:var(--fg)}dialog[open]::backdrop{display:block;background:#000;opacity:.4;animation:bg 2s}dialog:not([open]){display:none}.box,.sidebar-layout>header,[role=menu],[role=tabpanel],details,dialog,figure{margin:var(--gap) 0;padding:var(--gap);overflow:clip;border-radius:var(--border-radius);background:var(--box-bg);border:1px solid var(--graphical-fg)}.titlebar{margin-inline:calc(0px - var(--gap));-webkit-margin-after:calc(0px - var(--gap));margin-block-end:calc(0px - var(--gap));padding-inline:var(--gap);font:inherit;font-family:var(--secondary-font);font-weight:700;translate:0 calc(-1px - var(--gap));background:var(--graphical-fg);color:var(--bg)}.sub-title,sub-title{display:block;font-weight:400;color:var(--muted-fg)}.tool-bar,[role=toolbar]{display:flex;flex-flow:row wrap;gap:calc(var(--gap)/2)}.tool-bar>*,[role=toolbar]>*{margin:0}.sidebar-layout header li{margin-block:calc(.5*var(--gap))}.breadcrumbs[aria-label] [aria-current=page],.sidebar-layout header a{font-weight:700}@media (min-width:75ch){.sidebar-layout{display:grid;grid-template-columns:25ch auto;inset:0}.sidebar-layout>header{border-block:none;-webkit-border-start:none;border-inline-start:none;margin:0}.sidebar-layout>:nth-child(2){overflow:auto;--full-width: calc(100vw - 25ch);margin-top:var(--gap)}}.breadcrumbs[aria-label]{font-family:var(--secondary-font)}.breadcrumbs[aria-label] ol,.breadcrumbs[aria-label] ul{list-style:none;-webkit-padding-start:0;padding-inline-start:0}.breadcrumbs[aria-label] li{display:inline}:is(.breadcrumbs[aria-label] li)+li::before{content:' / ';display:inline}.chip,.navbar,chip{font-family:var(--secondary-font);background:var(--box-bg)}.chip,chip{border:1px solid var(--accent);border-radius:calc(var(--rhythm)/2);padding-inline:calc(var(--rhythm)/2)}.navbar{padding:var(--rhythm);-webkit-border-after:1px solid var(--accent);border-block-end:1px solid var(--accent);overflow-x:auto;scrollbar-width:thin;position:sticky;z-index:5;top:0;left:0;right:0;display:flex;flex-flow:row;align-items:center;gap:var(--gap)}.navbar.expanded{flex-flow:column;align-items:start;max-height:90vh;overflow-y:auto}.navbar.expanded ul[role=list]{flex-flow:column}.navbar *{flex-shrink:0;margin-block:0}.navbar:not(.expanded) nav>:first-child,.navbar:not(.expanded)>:first-child{-webkit-margin-start:auto;margin-inline-start:auto}.navbar:not(.expanded) nav>:last-child,.navbar:not(.expanded)>:last-child{-webkit-margin-end:auto;margin-inline-end:auto}.navbar hr{align-self:stretch}.navbar nav ul[role=list]{display:flex;flex-flow:row;gap:var(--rhythm);-webkit-padding-start:0;padding-inline-start:0}.navbar nav ul[role=list] *{flex-shrink:0}.navbar a{font-weight:700;text-decoration:none;padding-inline:.2em}.navbar a:focus,.navbar a:hover{text-decoration:underline}.navbar [aria-current=page]{position:relative}.navbar [aria-current=page]::after{width:100%;height:6px;content:"";display:block;position:absolute;bottom:calc(-1*var(--gap));background:currentcolor}.navbar.expanded [aria-current=page]::after{width:6px;height:100%;position:absolute;left:calc(-1*var(--gap));top:0}.permalink-anchor{display:none}:hover>.permalink-anchor{display:initial}button.iconbutton{border:0;background:0 0;color:currentcolor;padding:0;line-height:var(--rhythm);font-size:24px;width:24px;height:24px;display:inline-block;text-align:center;border-radius:50%;transition:font-weight .2s ease-in-out}button.iconbutton:focus-visible,button.iconbutton:hover{outline:1px solid var(--accent);outline-offset:6px}button.iconbutton:active{outline-offset:3px;background:0 0}button.iconbutton[aria-pressed=true]{box-shadow:none;transform:none}[role=tablist]{display:flex;gap:.5ch;scrollbar-width:thin}[role=tab][role=tab]{all:initial;font-family:var(--secondary-font);padding:0 calc(var(--rhythm)/4);margin:0;min-height:var(--rhythm);bottom:-1px;position:relative;color:var(--fg);border:solid var(--graphical-fg);border-width:1px;background:var(--interactive-bg);border-start-start-radius:.4em;border-start-end-radius:.4em}[role=tab][role=tab]:active,[role=tab][role=tab][aria-selected=true]{background:var(--box-bg);-webkit-border-after:1px solid transparent;border-block-end:1px solid transparent}[role=tab][role=tab]:hover{background-color:var(--box-bg);box-shadow:none}[role=tab][role=tab]:focus-visible{box-shadow:none;color:var(--accent);text-decoration:underline}[role=tabpanel]{-webkit-margin-before:0;margin-block-start:0;border-start-start-radius:0;border-start-end-radius:0;z-index:1}[role=menu]{position:absolute;z-index:10;padding:calc(var(--gap)/2) 0;margin:1px 0 0;display:flex;flex-flow:column nowrap}[role=menuitem]{padding:0 calc(var(--gap)/2);display:block;text-decoration:none;border-radius:0;color:var(--fg)}[role=menuitem]:active,[role=menuitem]:focus{background:var(--accent);color:var(--bg)}[role=listbox]{list-style:none}[role=listbox] [role=option]{margin-inline:calc(-1*var(--gap));padding-inline:var(--gap)}[role=listbox] [role=option][aria-selected=true]{background:var(--interactive-bg)}[role=listbox] .active[role=option]{--temporary-bg: var(--accent);--temporary-fg: var(--bg);--temporary-accent: parent-var(--muted-accent);--temporary-muted-accent: parent-var(--box-bg);background:var(--temporary-bg);color:var(--temporary-fg)}[role=listbox] .active[role=option]>*{--bg: var(--temporary-bg);--fg: var(--temporary-fg);--accent: var(--temporary-accent);--muted-accent: var(--temporary-muted-accent)}[aria-orientation=vertical]{flex-direction:column;width:-moz-fit-content;width:fit-content;text-align:center}.plain{--box-bg: var(--plain-bg);--accent: var(--plain-fg);--graphical-fg: var(--plain-graphical-fg)}.info{--box-bg: var(--info-bg);--accent: var(--info-fg);--graphical-fg: var(--info-graphical-fg)}.ok{--box-bg: var(--ok-bg);--accent: var(--ok-fg);--graphical-fg: var(--ok-graphical-fg)}.warn{--box-bg: var(--warn-bg);--accent: var(--warn-fg);--graphical-fg: var(--warn-graphical-fg)}.bad{--box-bg: var(--bad-bg);--accent: var(--bad-fg);--graphical-fg: var(--bad-graphical-fg)}.color{color:var(--accent)}.bg{background:var(--box-bg)}.border{border-style:solid;border-color:var(--graphical-fg)}:root{--fg: var(--gray-12);--muted-fg: var(--gray-10);--faded-fg: var(--gray-6);--graphical-fg: var(--plain-graphical-fg);--plain-fg: var(--blue-10);--info-fg: var(--blue-11);--ok-fg: var(--green-11);--bad-fg: var(--red-11);--warn-fg: var(--yellow-11);--plain-graphical-fg: var(--gray-6);--info-graphical-fg: var(--blue-6);--ok-graphical-fg: var(--green-6);--bad-graphical-fg: var(--red-6);--warn-graphical-fg: var(--yellow-6);--bg: var(--gray-0);--box-bg: var(--plain-bg);--interactive-bg: var(--gray-4);--plain-bg: var(--gray-1);--info-bg: var(--blue-1);--ok-bg: var(--green-1);--bad-bg: var(--red-1);--warn-bg: var(--yellow-1);--accent: var(--blue-10);--muted-accent: var(--blue-7);--rhythm: 1.4rem;--line-length: 40rem;--border-radius: .2rem;--main-font: 'Source Sans 3', 'Source Sans Pro', -apple-system, system-ui, sans-serif;--secondary-font: var(--main-font);--mono-font: 'M Plus Code Latin', monospace, monospace;--density: 1;--full-width: 100vw;--eff-line-length: /* Effective line length for prose. */ + min( + calc( var(--full-width) - (2 * var(--rhythm)) ), + var(--line-length) + );--gutter-width: /* Width of spaces at each side of page content. */ + calc( + ( + var(--full-width) /* Viewport width */ + - var(--eff-line-length) /* minus line width */ + ) / 2);--gap: calc(var(--rhythm) * var(--density))}@media (prefers-color-scheme:dark){:root:not(.-no-dark-theme){--fg: var(--gray-0);--muted-fg: var(--gray-2);--faded-fg: var(--gray-7);--plain-bg: var(--gray-11);--info-bg: var(--blue-12);--ok-bg: var(--green-12);--bad-bg: var(--red-12);--warn-bg: var(--yellow-12);--plain-faded-fg: var(--blue-6);--info-faded-fg: var(--blue-6);--ok-faded-fg: var(--green-6);--bad-faded-fg: var(--red-6);--warn-faded-fg: var(--yellow-6);--bg: var(--gray-12);--box-bg: var(--gray-10);--interactive-bg: var(--gray-8);--plain-fg: (--blue-3);--info-fg: var(--blue-3);--ok-fg: var(--green-3);--bad-fg: var(--red-3);--warn-fg: var(--yellow-3);--accent: var(--blue-3);--muted-accent: var(--blue-5)}}*{accent-color:var(--accent)}.textcolumns{--col-width: 30ch;column-width:var(--col-width);column-gap:var(--gap);margin-block:var(--gap)}.textcolumns :first-child{-webkit-margin-before:0!important;margin-block-start:0!important}.text-align\:center{text-align:center}.center{display:grid;place-items:center}.container{max-inline-size:var(--eff-line-length);margin-inline:auto}.fullbleed,.fullscreen{position:relative;left:50%;border-radius:0;border-inline:none}.fullbleed{width:var(--full-width);transform:translateX(calc(-.5*var(--full-width)))}.fullscreen{height:100vh;width:100vw;transform:translateX(-50vw)}.width\:100\%{width:100%;max-width:100%}.height\:100\%{height:100%;max-height:100%}:is( + body, + .box, + [role=menu], + .sidebar-layout > header, + [role=tabpanel], + figure, + details, + dialog, + aside, + fieldset, + dd, + td, + th +)>:first-child:first-child:first-child:first-child,:is( + body, + .box, + [role=menu], + .sidebar-layout > header, + [role=tabpanel], + figure, + details, + dialog, + aside, + fieldset, + dd, + td, + th +)>:first-child>:first-child:first-child:first-child,:is( + body, + .box, + [role=menu], + .sidebar-layout > header, + [role=tabpanel], + figure, + details, + dialog, + aside, + fieldset, + dd, + td, + th +)>:first-child>:first-child>:first-child:first-child,:is( + body, + .box, + [role=menu], + .sidebar-layout > header, + [role=tabpanel], + figure, + details, + dialog, + aside, + fieldset, + dd, + td, + th +)>:first-child>:first-child>:first-child>:first-child{-webkit-margin-before:0;margin-block-start:0}:is( + body, + .box, + [role=menu], + .sidebar-layout > header, + [role=tabpanel], + figure, + details, + dialog, + aside, + fieldset, + dd, + td, + th +)>:last-child:last-child:last-child:last-child,:is( + body, + .box, + [role=menu], + .sidebar-layout > header, + [role=tabpanel], + figure, + details, + dialog, + aside, + fieldset, + dd, + td, + th +)>:last-child>:last-child:last-child:last-child,:is( + body, + .box, + [role=menu], + .sidebar-layout > header, + [role=tabpanel], + figure, + details, + dialog, + aside, + fieldset, + dd, + td, + th +)>:last-child>:last-child>:last-child:last-child,:is( + body, + .box, + [role=menu], + .sidebar-layout > header, + [role=tabpanel], + figure, + details, + dialog, + aside, + fieldset, + dd, + td, + th +)>:last-child>:last-child>:last-child>:last-child{-webkit-margin-after:0;margin-block-end:0}.padding{padding-inline:var(--gap)}.padding-block{padding-block:var(--gap)}.padding-block-start{-webkit-padding-before:var(--gap);padding-block-start:var(--gap)}.padding-block-end{-webkit-padding-after:var(--gap);padding-block-end:var(--gap)}.padding-inline{padding-inline:var(--gap)}.padding-inline-end,.padding-inline-start{-webkit-padding-start:var(--gap);padding-inline-start:var(--gap)}.margin{margin:var(--gap)}.margin-block{margin-block:var(--gap)}.margin-block-start{-webkit-margin-before:var(--gap);margin-block-start:var(--gap)}.margin-block-end{-webkit-margin-after:var(--gap);margin-block-end:var(--gap)}.margin-inline{margin-inline:var(--gap)}.margin-inline-start{-webkit-margin-start:var(--gap);margin-inline-start:var(--gap)}.margin-inline-end{-webkit-margin-end:var(--gap);margin-inline-end:var(--gap)}.flow-gap>:not(:last-child),.row:not(:last-child):not([specificity-hack])>*,.rows>:not(:last-child):not([specificity-hack])>*{margin-bottom:var(--gap)}.inline{display:inline}.block{display:block}.contents{display:contents}.table{display:table;width:100%;margin:0}.row,.rows>*{display:table-row}.row>:not([specificity-hack]),.rows>*>:not([specificity-hack]){display:table-cell;vertical-align:top}.row>*+:not([specificity-hack]),:is(.rows > *)>*+:not([specificity-hack]){-webkit-margin-start:var(--gap);margin-inline-start:var(--gap);display:inline-block}.big{font-size:1.4em;line-height:calc(1.5*var(--rhythm))}.fixed{position:fixed}.sticky{position:sticky}.top{top:0}.right{right:0}.bottom{bottom:0}.left{left:0}.float\:left{float:left}.float\:right{float:right}.overflow\:auto{overflow:auto}.overflow\:scroll{overflow:scroll}.airy{--density: 3}.spacious{--density: 2}.dense{--density: 1}.crowded{--density: .5}.packed{--density: 0}.autodensity{--density: 1 +}@media (min-width:768px){.autodensity{--density: 2 +}}@media (min-width:1024px){.autodensity{--density: 3 +}}.vh,v-h{clip:rect(0 0 0 0);-webkit-clip-path:inset(50%);clip-path:inset(50%);block-size:1px;inline-size:1px;overflow:hidden;white-space:nowrap}.all\:initial{all:initial}.bold{font-weight:700}.italic{font-style:italic}.allcaps{text-transform:uppercase;letter-spacing:.1rem}.primary-font{font-family:var(--primary-font)}.secondary-font{font-family:var(--secondary-font)}.display-font{font-family:var(--display-font)}.mono-font,.monospace{font-family:var(--mono-font)}.massivetext{font-size:calc(.13*var(--eff-line-length));line-height:1em;letter-spacing:0}.aestheticbreak{display:block;margin:0;padding:0;height:calc(.5*var(--gap))}.f-row{display:flex;flex-direction:row;gap:var(--gap)}.f-row>*{margin:0}.f-col{display:flex;flex-direction:column;gap:var(--gap)}.f-col>*{margin:0}.f-switch{display:flex;flex-wrap:wrap;gap:var(--gap);--f-switch-threshold: 55ch +}.f-switch>*{margin:0;flex-grow:1;flex-basis:calc((var(--f-switch-threshold) - 100%)*999)}.justify-content\:start{justify-content:start}.justify-content\:end{justify-content:end}.justify-content\:baseline{justify-content:baseline}.justify-content\:center{justify-content:center}.justify-content\:stretch{justify-content:stretch}.justify-content\:space-between{justify-content:space-between}.justify-content\:space-around{justify-content:space-around}.justify-content\:space-evenly{justify-content:space-evenly}.align-items\:start{align-items:start}.align-items\:end{align-items:end}.align-items\:baseline{align-items:baseline}.align-items\:center{align-items:center}.align-items\:stretch{align-items:stretch}.align-self\:start{align-self:start}.align-self\:end{align-self:end}.align-self\:baseline{align-self:baseline}.align-self\:center{align-self:center}.align-self\:stretch{align-self:stretch}.flex-grow\:0{flex-grow:0}.flex-grow\:1{flex-grow:1}.flex-grow\:2{flex-grow:2}.flex-grow\:3{flex-grow:3}.flex-grow\:4{flex-grow:4}.flex-grow\:5{flex-grow:5}.flex-grow\:6{flex-grow:6}.flex-grow\:7{flex-grow:7}.flex-grow\:8{flex-grow:8}.flex-grow\:9{flex-grow:9}.flex-grow\:10{flex-grow:10}.flex-grow\:11{flex-grow:11}.flex-grow\:12{flex-grow:12}.flex-wrap\:wrap{flex-wrap:wrap}.flex-wrap\:nowrap{flex-wrap:nowrap}.grid{display:grid;grid-auto-columns:var(--grid-col-width, 1fr);grid-auto-rows:var(--grid-row-width, auto);gap:var(--gap)}.grid>*{margin:0}.grid-even-rows{--grid-row-width: 1fr}.grid-variable-cols{--grid-column-width: auto}[data-cols^="1 "]{grid-column-start:1}[data-cols$=" 1"]{grid-column-end:2}[data-cols="1"]{grid-column:1}[data-cols^="2 "]{grid-column-start:2}[data-cols$=" 2"]{grid-column-end:3}[data-cols="2"]{grid-column:2}[data-cols^="3 "]{grid-column-start:3}[data-cols$=" 3"]{grid-column-end:4}[data-cols="3"]{grid-column:3}[data-cols^="4 "]{grid-column-start:4}[data-cols$=" 4"]{grid-column-end:5}[data-cols="4"]{grid-column:4}[data-cols^="5 "]{grid-column-start:5}[data-cols$=" 5"]{grid-column-end:6}[data-cols="5"]{grid-column:5}[data-cols^="6 "]{grid-column-start:6}[data-cols$=" 6"]{grid-column-end:7}[data-cols="6"]{grid-column:6}[data-cols^="7 "]{grid-column-start:7}[data-cols$=" 7"]{grid-column-end:8}[data-cols="7"]{grid-column:7}[data-cols^="8 "]{grid-column-start:8}[data-cols$=" 8"]{grid-column-end:9}[data-cols="8"]{grid-column:8}[data-cols^="9 "]{grid-column-start:9}[data-cols$=" 9"]{grid-column-end:10}[data-cols="9"]{grid-column:9}[data-cols^="10 "]{grid-column-start:10}[data-cols$=" 10"]{grid-column-end:11}[data-cols="10"]{grid-column:10}[data-cols^="11 "]{grid-column-start:11}[data-cols$=" 11"]{grid-column-end:12}[data-cols="11"]{grid-column:11}[data-cols^="12 "]{grid-column-start:12}[data-cols$=" 12"]{grid-column-end:13}[data-cols="12"]{grid-column:12}[data-rows^="1 "]{grid-row-start:1}[data-rows$=" 1"]{grid-row-end:2}[data-rows="1"]{grid-row:1}[data-rows^="2 "]{grid-row-start:2}[data-rows$=" 2"]{grid-row-end:3}[data-rows="2"]{grid-row:2}[data-rows^="3 "]{grid-row-start:3}[data-rows$=" 3"]{grid-row-end:4}[data-rows="3"]{grid-row:3}[data-rows^="4 "]{grid-row-start:4}[data-rows$=" 4"]{grid-row-end:5}[data-rows="4"]{grid-row:4}[data-rows^="5 "]{grid-row-start:5}[data-rows$=" 5"]{grid-row-end:6}[data-rows="5"]{grid-row:5}[data-rows^="6 "]{grid-row-start:6}[data-rows$=" 6"]{grid-row-end:7}[data-rows="6"]{grid-row:6}[data-rows^="7 "]{grid-row-start:7}[data-rows$=" 7"]{grid-row-end:8}[data-rows="7"]{grid-row:7}[data-rows^="8 "]{grid-row-start:8}[data-rows$=" 8"]{grid-row-end:9}[data-rows="8"]{grid-row:8}[data-rows^="9 "]{grid-row-start:9}[data-rows$=" 9"]{grid-row-end:10}[data-rows="9"]{grid-row:9}[data-rows^="10 "]{grid-row-start:10}[data-rows$=" 10"]{grid-row-end:11}[data-rows="10"]{grid-row:10}[data-rows^="11 "]{grid-row-start:11}[data-rows$=" 11"]{grid-row-end:12}[data-rows="11"]{grid-row:11}[data-rows^="12 "]{grid-row-start:12}[data-rows$=" 12"]{grid-row-end:13}[data-rows="12"]{grid-row:12}@media (max-width:768px){[data-cols\@s^="1 "]{grid-column-start:1}[data-cols\@s$=" 1"]{grid-column-end:2}[data-cols\@s="1"]{grid-column:1}[data-cols\@s^="2 "]{grid-column-start:2}[data-cols\@s$=" 2"]{grid-column-end:3}[data-cols\@s="2"]{grid-column:2}[data-cols\@s^="3 "]{grid-column-start:3}[data-cols\@s$=" 3"]{grid-column-end:4}[data-cols\@s="3"]{grid-column:3}[data-cols\@s^="4 "]{grid-column-start:4}[data-cols\@s$=" 4"]{grid-column-end:5}[data-cols\@s="4"]{grid-column:4}[data-cols\@s^="5 "]{grid-column-start:5}[data-cols\@s$=" 5"]{grid-column-end:6}[data-cols\@s="5"]{grid-column:5}[data-cols\@s^="6 "]{grid-column-start:6}[data-cols\@s$=" 6"]{grid-column-end:7}[data-cols\@s="6"]{grid-column:6}[data-cols\@s^="7 "]{grid-column-start:7}[data-cols\@s$=" 7"]{grid-column-end:8}[data-cols\@s="7"]{grid-column:7}[data-cols\@s^="8 "]{grid-column-start:8}[data-cols\@s$=" 8"]{grid-column-end:9}[data-cols\@s="8"]{grid-column:8}[data-cols\@s^="9 "]{grid-column-start:9}[data-cols\@s$=" 9"]{grid-column-end:10}[data-cols\@s="9"]{grid-column:9}[data-cols\@s^="10 "]{grid-column-start:10}[data-cols\@s$=" 10"]{grid-column-end:11}[data-cols\@s="10"]{grid-column:10}[data-cols\@s^="11 "]{grid-column-start:11}[data-cols\@s$=" 11"]{grid-column-end:12}[data-cols\@s="11"]{grid-column:11}[data-cols\@s^="12 "]{grid-column-start:12}[data-cols\@s$=" 12"]{grid-column-end:13}[data-cols\@s="12"]{grid-column:12}[data-rows\@s^="1 "]{grid-row-start:1}[data-rows\@s$=" 1"]{grid-row-end:2}[data-rows\@s="1"]{grid-row:1}[data-rows\@s^="2 "]{grid-row-start:2}[data-rows\@s$=" 2"]{grid-row-end:3}[data-rows\@s="2"]{grid-row:2}[data-rows\@s^="3 "]{grid-row-start:3}[data-rows\@s$=" 3"]{grid-row-end:4}[data-rows\@s="3"]{grid-row:3}[data-rows\@s^="4 "]{grid-row-start:4}[data-rows\@s$=" 4"]{grid-row-end:5}[data-rows\@s="4"]{grid-row:4}[data-rows\@s^="5 "]{grid-row-start:5}[data-rows\@s$=" 5"]{grid-row-end:6}[data-rows\@s="5"]{grid-row:5}[data-rows\@s^="6 "]{grid-row-start:6}[data-rows\@s$=" 6"]{grid-row-end:7}[data-rows\@s="6"]{grid-row:6}[data-rows\@s^="7 "]{grid-row-start:7}[data-rows\@s$=" 7"]{grid-row-end:8}[data-rows\@s="7"]{grid-row:7}[data-rows\@s^="8 "]{grid-row-start:8}[data-rows\@s$=" 8"]{grid-row-end:9}[data-rows\@s="8"]{grid-row:8}[data-rows\@s^="9 "]{grid-row-start:9}[data-rows\@s$=" 9"]{grid-row-end:10}[data-rows\@s="9"]{grid-row:9}[data-rows\@s^="10 "]{grid-row-start:10}[data-rows\@s$=" 10"]{grid-row-end:11}[data-rows\@s="10"]{grid-row:10}[data-rows\@s^="11 "]{grid-row-start:11}[data-rows\@s$=" 11"]{grid-row-end:12}[data-rows\@s="11"]{grid-row:11}[data-rows\@s^="12 "]{grid-row-start:12}[data-rows\@s$=" 12"]{grid-row-end:13}[data-rows\@s="12"]{grid-row:12}}@media (min-width:1024px){[data-cols\@l^="1 "]{grid-column-start:1}[data-cols\@l$=" 1"]{grid-column-end:2}[data-cols\@l="1"]{grid-column:1}[data-cols\@l^="2 "]{grid-column-start:2}[data-cols\@l$=" 2"]{grid-column-end:3}[data-cols\@l="2"]{grid-column:2}[data-cols\@l^="3 "]{grid-column-start:3}[data-cols\@l$=" 3"]{grid-column-end:4}[data-cols\@l="3"]{grid-column:3}[data-cols\@l^="4 "]{grid-column-start:4}[data-cols\@l$=" 4"]{grid-column-end:5}[data-cols\@l="4"]{grid-column:4}[data-cols\@l^="5 "]{grid-column-start:5}[data-cols\@l$=" 5"]{grid-column-end:6}[data-cols\@l="5"]{grid-column:5}[data-cols\@l^="6 "]{grid-column-start:6}[data-cols\@l$=" 6"]{grid-column-end:7}[data-cols\@l="6"]{grid-column:6}[data-cols\@l^="7 "]{grid-column-start:7}[data-cols\@l$=" 7"]{grid-column-end:8}[data-cols\@l="7"]{grid-column:7}[data-cols\@l^="8 "]{grid-column-start:8}[data-cols\@l$=" 8"]{grid-column-end:9}[data-cols\@l="8"]{grid-column:8}[data-cols\@l^="9 "]{grid-column-start:9}[data-cols\@l$=" 9"]{grid-column-end:10}[data-cols\@l="9"]{grid-column:9}[data-cols\@l^="10 "]{grid-column-start:10}[data-cols\@l$=" 10"]{grid-column-end:11}[data-cols\@l="10"]{grid-column:10}[data-cols\@l^="11 "]{grid-column-start:11}[data-cols\@l$=" 11"]{grid-column-end:12}[data-cols\@l="11"]{grid-column:11}[data-cols\@l^="12 "]{grid-column-start:12}[data-cols\@l$=" 12"]{grid-column-end:13}[data-cols\@l="12"]{grid-column:12}[data-rows\@l^="1 "]{grid-row-start:1}[data-rows\@l$=" 1"]{grid-row-end:2}[data-rows\@l="1"]{grid-row:1}[data-rows\@l^="2 "]{grid-row-start:2}[data-rows\@l$=" 2"]{grid-row-end:3}[data-rows\@l="2"]{grid-row:2}[data-rows\@l^="3 "]{grid-row-start:3}[data-rows\@l$=" 3"]{grid-row-end:4}[data-rows\@l="3"]{grid-row:3}[data-rows\@l^="4 "]{grid-row-start:4}[data-rows\@l$=" 4"]{grid-row-end:5}[data-rows\@l="4"]{grid-row:4}[data-rows\@l^="5 "]{grid-row-start:5}[data-rows\@l$=" 5"]{grid-row-end:6}[data-rows\@l="5"]{grid-row:5}[data-rows\@l^="6 "]{grid-row-start:6}[data-rows\@l$=" 6"]{grid-row-end:7}[data-rows\@l="6"]{grid-row:6}[data-rows\@l^="7 "]{grid-row-start:7}[data-rows\@l$=" 7"]{grid-row-end:8}[data-rows\@l="7"]{grid-row:7}[data-rows\@l^="8 "]{grid-row-start:8}[data-rows\@l$=" 8"]{grid-row-end:9}[data-rows\@l="8"]{grid-row:8}[data-rows\@l^="9 "]{grid-row-start:9}[data-rows\@l$=" 9"]{grid-row-end:10}[data-rows\@l="9"]{grid-row:9}[data-rows\@l^="10 "]{grid-row-start:10}[data-rows\@l$=" 10"]{grid-row-end:11}[data-rows\@l="10"]{grid-row:10}[data-rows\@l^="11 "]{grid-row-start:11}[data-rows\@l$=" 11"]{grid-row-end:12}[data-rows\@l="11"]{grid-row:11}[data-rows\@l^="12 "]{grid-row-start:12}[data-rows\@l$=" 12"]{grid-row-end:13}[data-rows\@l="12"]{grid-row:12}} \ No newline at end of file diff --git a/chapter-10/2-adding-contacts/static/css/site.css b/chapter-10/2-adding-contacts/static/css/site.css new file mode 100644 index 00000000..fb4aabd1 --- /dev/null +++ b/chapter-10/2-adding-contacts/static/css/site.css @@ -0,0 +1,65 @@ +.flash { + display: block; + background-color: #2fdc2f !important; + color: white; + font-weight: bold; + padding: 12px; + border: 1px solid black; + border-radius: 8px; + margin: 16px; +} + +table { + width: 100%; + margin-bottom: 12px; +} + +.error { + display: inline-block; + color: darkred; +} + +tr.htmx-swapping { + opacity: 0; + transition: opacity 1s ease-out; +} + +td { + vertical-align: middle; +} + +#download-ui { + margin-bottom: 16px; +} + +.progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f5f5f5; + border-radius: 4px; + box-shadow: inset 0 1px 2px rgba(0,0,0,.1); +} + +.progress-bar { + float: left; + width: 0%; + height: 100%; + font-size: 12px; + line-height: 20px; + color: #fff; + text-align: center; + background-color: #337ab7; + -webkit-box-shadow: inset 0 -1px 0 rgba(0,0,0,.15); + box-shadow: inset 0 -1px 0 rgba(0,0,0,.15); + -webkit-transition: width .6s ease; + -o-transition: width .6s ease; + transition: width .6s ease; +} + +[data-overflow-menu] { + visibility: hidden; +} + tr:is(:hover, :focus-within) [data-overflow-menu] { + visibility: visible; + } \ No newline at end of file diff --git a/chapter-10/2-adding-contacts/static/img/spinning-circles.svg b/chapter-10/2-adding-contacts/static/img/spinning-circles.svg new file mode 100644 index 00000000..21989594 --- /dev/null +++ b/chapter-10/2-adding-contacts/static/img/spinning-circles.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/chapter-10/2-adding-contacts/static/js/htmx.min.js b/chapter-10/2-adding-contacts/static/js/htmx.min.js new file mode 100644 index 00000000..25ba2a23 --- /dev/null +++ b/chapter-10/2-adding-contacts/static/js/htmx.min.js @@ -0,0 +1 @@ +(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var G={onLoad:t,process:Nt,on:le,off:ue,trigger:oe,ajax:xr,find:b,findAll:f,closest:d,values:function(e,t){var r=er(e,t||"post");return r.values},remove:U,addClass:B,removeClass:n,toggleClass:V,takeClass:j,defineExtension:Rr,removeExtension:Or,logAll:X,logNone:F,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get"],selfRequestsOnly:false},parseInterval:v,_:e,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=G.config.wsBinaryType;return t},version:"1.9.5"};var C={addTriggerHandler:bt,bodyContains:re,canAccessLocalStorage:M,findThisElement:he,filterValues:ar,hasAttribute:o,getAttributeValue:Z,getClosestAttributeValue:Y,getClosestMatch:c,getExpressionVars:gr,getHeaders:ir,getInputValues:er,getInternalData:ee,getSwapSpecification:sr,getTriggerSpecs:Ge,getTarget:de,makeFragment:l,mergeObjects:ne,makeSettleInfo:S,oobSwap:me,querySelectorExt:ie,selectAndSwap:De,settleImmediately:Wt,shouldCancel:Qe,triggerEvent:oe,triggerErrorEvent:ae,withExtensions:w};var R=["get","post","put","delete","patch"];var O=R.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function v(e){if(e==undefined){return undefined}if(e.slice(-2)=="ms"){return parseFloat(e.slice(0,-2))||undefined}if(e.slice(-1)=="s"){return parseFloat(e.slice(0,-1))*1e3||undefined}if(e.slice(-1)=="m"){return parseFloat(e.slice(0,-1))*1e3*60||undefined}return parseFloat(e)||undefined}function J(e,t){return e.getAttribute&&e.getAttribute(t)}function o(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function Z(e,t){return J(e,t)||J(e,"data-"+t)}function u(e){return e.parentElement}function K(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function T(e,t,r){var n=Z(t,r);var i=Z(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function Y(t,r){var n=null;c(t,function(e){return n=T(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function q(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function i(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=K().createDocumentFragment()}return i}function H(e){return e.match(/",0);return r.querySelector("template").content}else{var n=q(e);switch(n){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return i(""+e+"
",1);case"col":return i(""+e+"
",2);case"tr":return i(""+e+"
",2);case"td":case"th":return i(""+e+"
",3);case"script":return i("
"+e+"
",1);default:return i(e,0)}}}function Q(e){if(e){e()}}function L(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function A(e){return L(e,"Function")}function N(e){return L(e,"Object")}function ee(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function I(e){var t=[];if(e){for(var r=0;r=0}function re(e){if(e.getRootNode&&e.getRootNode()instanceof window.ShadowRoot){return K().body.contains(e.getRootNode().host)}else{return K().body.contains(e)}}function k(e){return e.trim().split(/\s+/)}function ne(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function y(e){try{return JSON.parse(e)}catch(e){x(e);return null}}function M(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function D(t){try{var e=new URL(t);if(e){t=e.pathname+e.search}if(!t.match("^/$")){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return hr(K().body,function(){return eval(e)})}function t(t){var e=G.on("htmx:load",function(e){t(e.detail.elt)});return e}function X(){G.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function F(){G.logger=null}function b(e,t){if(t){return e.querySelector(t)}else{return b(K(),e)}}function f(e,t){if(t){return e.querySelectorAll(t)}else{return f(K(),e)}}function U(e,t){e=s(e);if(t){setTimeout(function(){U(e);e=null},t)}else{e.parentElement.removeChild(e)}}function B(e,t,r){e=s(e);if(r){setTimeout(function(){B(e,t);e=null},r)}else{e.classList&&e.classList.add(t)}}function n(e,t,r){e=s(e);if(r){setTimeout(function(){n(e,t);e=null},r)}else{if(e.classList){e.classList.remove(t);if(e.classList.length===0){e.removeAttribute("class")}}}}function V(e,t){e=s(e);e.classList.toggle(t)}function j(e,t){e=s(e);te(e.parentElement.children,function(e){n(e,t)});B(e,t)}function d(e,t){e=s(e);if(e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&u(e));return null}}function r(e){var t=e.trim();if(t.startsWith("<")&&t.endsWith("/>")){return t.substring(1,t.length-2)}else{return t}}function W(e,t){if(t.indexOf("closest ")===0){return[d(e,r(t.substr(8)))]}else if(t.indexOf("find ")===0){return[b(e,r(t.substr(5)))]}else if(t.indexOf("next ")===0){return[_(e,r(t.substr(5)))]}else if(t.indexOf("previous ")===0){return[z(e,r(t.substr(9)))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else if(t==="body"){return[document.body]}else{return K().querySelectorAll(r(t))}}var _=function(e,t){var r=K().querySelectorAll(t);for(var n=0;n=0;n--){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING){return i}}};function ie(e,t){if(t){return W(e,t)[0]}else{return W(K().body,e)[0]}}function s(e){if(L(e,"String")){return b(e)}else{return e}}function $(e,t,r){if(A(t)){return{target:K().body,event:e,listener:t}}else{return{target:s(e),event:t,listener:r}}}function le(t,r,n){Hr(function(){var e=$(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=A(r);return e?r:n}function ue(t,r,n){Hr(function(){var e=$(t,r,n);e.target.removeEventListener(e.event,e.listener)});return A(r)?r:n}var fe=K().createElement("output");function ce(e,t){var r=Y(e,t);if(r){if(r==="this"){return[he(e,t)]}else{var n=W(e,r);if(n.length===0){x('The selector "'+r+'" on '+t+" returned no matches!");return[fe]}else{return n}}}}function he(e,t){return c(e,function(e){return Z(e,t)!=null})}function de(e){var t=Y(e,"hx-target");if(t){if(t==="this"){return he(e,"hx-target")}else{return ie(e,t)}}else{var r=ee(e);if(r.boosted){return K().body}else{return e}}}function ve(e){var t=G.config.attributesToSettle;for(var r=0;r0){o=e.substr(0,e.indexOf(":"));t=e.substr(e.indexOf(":")+1,e.length)}else{o=e}var r=K().querySelectorAll(t);if(r){te(r,function(e){var t;var r=i.cloneNode(true);t=K().createDocumentFragment();t.appendChild(r);if(!pe(o,e)){t=r}var n={shouldSwap:true,target:e,fragment:t};if(!oe(e,"htmx:oobBeforeSwap",n))return;e=n.target;if(n["shouldSwap"]){ke(o,e,e,t,a)}te(a.elts,function(e){oe(e,"htmx:oobAfterSwap",n)})});i.parentNode.removeChild(i)}else{i.parentNode.removeChild(i);ae(K().body,"htmx:oobErrorNoTarget",{content:i})}return e}function xe(e,t,r){var n=Y(e,"hx-select-oob");if(n){var i=n.split(",");for(let e=0;e0){var r=t.replace("'","\\'");var n=e.tagName.replace(":","\\:");var i=o.querySelector(n+"[id='"+r+"']");if(i&&i!==o){var a=e.cloneNode();ge(e,i);s.tasks.push(function(){ge(e,a)})}}})}function we(e){return function(){n(e,G.config.addedClass);Nt(e);St(e);Se(e);oe(e,"htmx:load")}}function Se(e){var t="[autofocus]";var r=h(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function a(e,t,r,n){be(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;B(i,G.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(we(i))}}}function Ee(e,t){var r=0;while(r-1){var t=e.replace(/]*>|>)([\s\S]*?)<\/svg>/gim,"");var r=t.match(/]*>|>)([\s\S]*?)<\/title>/im);if(r){return r[2]}}}function De(e,t,r,n,i,a){i.title=Me(n);var o=l(n);if(o){xe(r,o,i);o=Pe(r,o,a);ye(o);return ke(e,r,t,o,i)}}function Xe(e,t,r){var n=e.getResponseHeader(t);if(n.indexOf("{")===0){var i=y(n);for(var a in i){if(i.hasOwnProperty(a)){var o=i[a];if(!N(o)){o={value:o}}oe(r,a,o)}}}else{var s=n.split(",");for(var l=0;l0){var o=t[0];if(o==="]"){n--;if(n===0){if(a===null){i=i+"true"}t.shift();i+=")})";try{var s=hr(e,function(){return Function(i)()},function(){return true});s.source=i;return s}catch(e){ae(K().body,"htmx:syntax:error",{error:e,source:i});return null}}}else if(o==="["){n++}if(_e(o,a,r)){i+="(("+r+"."+o+") ? ("+r+"."+o+") : (window."+o+"))"}else{i=i+o}a=t.shift()}}}function m(e,t){var r="";while(e.length>0&&!e[0].match(t)){r+=e.shift()}return r}var $e="input, textarea, select";function Ge(e){var t=Z(e,"hx-trigger");var r=[];if(t){var n=We(t);do{m(n,je);var i=n.length;var a=m(n,/[,\[\s]/);if(a!==""){if(a==="every"){var o={trigger:"every"};m(n,je);o.pollInterval=v(m(n,/[,\[\s]/));m(n,je);var s=ze(e,n,"event");if(s){o.eventFilter=s}r.push(o)}else if(a.indexOf("sse:")===0){r.push({trigger:"sse",sseEvent:a.substr(4)})}else{var l={trigger:a};var s=ze(e,n,"event");if(s){l.eventFilter=s}while(n.length>0&&n[0]!==","){m(n,je);var u=n.shift();if(u==="changed"){l.changed=true}else if(u==="once"){l.once=true}else if(u==="consume"){l.consume=true}else if(u==="delay"&&n[0]===":"){n.shift();l.delay=v(m(n,p))}else if(u==="from"&&n[0]===":"){n.shift();var f=m(n,p);if(f==="closest"||f==="find"||f==="next"||f==="previous"){n.shift();f+=" "+m(n,p)}l.from=f}else if(u==="target"&&n[0]===":"){n.shift();l.target=m(n,p)}else if(u==="throttle"&&n[0]===":"){n.shift();l.throttle=v(m(n,p))}else if(u==="queue"&&n[0]===":"){n.shift();l.queue=m(n,p)}else if((u==="root"||u==="threshold")&&n[0]===":"){n.shift();l[u]=m(n,p)}else{ae(e,"htmx:syntax:error",{token:n.shift()})}}r.push(l)}}if(n.length===i){ae(e,"htmx:syntax:error",{token:n.shift()})}m(n,je)}while(n[0]===","&&n.shift())}if(r.length>0){return r}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,$e)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function Je(e){ee(e).cancelled=true}function Ze(e,t,r){var n=ee(e);n.timeout=setTimeout(function(){if(re(e)&&n.cancelled!==true){if(!tt(r,e,Pt("hx:poll:trigger",{triggerSpec:r,target:e}))){t(e)}Ze(e,t,r)}},r.pollInterval)}function Ke(e){return location.hostname===e.hostname&&J(e,"href")&&J(e,"href").indexOf("#")!==0}function Ye(t,r,e){if(t.tagName==="A"&&Ke(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"){r.boosted=true;var n,i;if(t.tagName==="A"){n="get";i=t.href}else{var a=J(t,"method");n=a?a.toLowerCase():"get";if(n==="get"){}i=J(t,"action")}e.forEach(function(e){rt(t,function(e,t){if(d(e,G.config.disableSelector)){g(e);return}se(n,i,e,t)},r,e,true)})}}function Qe(e,t){if(e.type==="submit"||e.type==="click"){if(t.tagName==="FORM"){return true}if(h(t,'input[type="submit"], button')&&d(t,"form")!==null){return true}if(t.tagName==="A"&&t.href&&(t.getAttribute("href")==="#"||t.getAttribute("href").indexOf("#")!==0)){return true}}return false}function et(e,t){return ee(e).boosted&&e.tagName==="A"&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function tt(e,t,r){var n=e.eventFilter;if(n){try{return n.call(t,r)!==true}catch(e){ae(K().body,"htmx:eventFilter:error",{error:e,source:n.source});return true}}return false}function rt(a,o,e,s,l){var u=ee(a);var t;if(s.from){t=W(a,s.from)}else{t=[a]}if(s.changed){t.forEach(function(e){var t=ee(e);t.lastValue=e.value})}te(t,function(n){var i=function(e){if(!re(a)){n.removeEventListener(s.trigger,i);return}if(et(a,e)){return}if(l||Qe(e,a)){e.preventDefault()}if(tt(s,a,e)){return}var t=ee(e);t.triggerSpec=s;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(a)<0){t.handledFor.push(a);if(s.consume){e.stopPropagation()}if(s.target&&e.target){if(!h(e.target,s.target)){return}}if(s.once){if(u.triggeredOnce){return}else{u.triggeredOnce=true}}if(s.changed){var r=ee(n);if(r.lastValue===n.value){return}r.lastValue=n.value}if(u.delayed){clearTimeout(u.delayed)}if(u.throttle){return}if(s.throttle){if(!u.throttle){o(a,e);u.throttle=setTimeout(function(){u.throttle=null},s.throttle)}}else if(s.delay){u.delayed=setTimeout(function(){o(a,e)},s.delay)}else{oe(a,"htmx:trigger");o(a,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:s.trigger,listener:i,on:n});n.addEventListener(s.trigger,i)})}var nt=false;var it=null;function at(){if(!it){it=function(){nt=true};window.addEventListener("scroll",it);setInterval(function(){if(nt){nt=false;te(K().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"),function(e){ot(e)})}},200)}}function ot(t){if(!o(t,"data-hx-revealed")&&P(t)){t.setAttribute("data-hx-revealed","true");var e=ee(t);if(e.initHash){oe(t,"revealed")}else{t.addEventListener("htmx:afterProcessNode",function(e){oe(t,"revealed")},{once:true})}}}function st(e,t,r){var n=k(r);for(var i=0;i=0){var t=ct(n);setTimeout(function(){lt(s,r,n+1)},t)}};t.onopen=function(e){n=0};ee(s).webSocket=t;t.addEventListener("message",function(e){if(ut(s)){return}var t=e.data;w(s,function(e){t=e.transformResponse(t,null,s)});var r=S(s);var n=l(t);var i=I(n.children);for(var a=0;a0){oe(u,"htmx:validation:halted",i);return}t.send(JSON.stringify(l));if(Qe(e,u)){e.preventDefault()}})}else{ae(u,"htmx:noWebSocketSourceError")}}function ct(e){var t=G.config.wsReconnectDelay;if(typeof t==="function"){return t(e)}if(t==="full-jitter"){var r=Math.min(e,6);var n=1e3*Math.pow(2,r);return n*Math.random()}x('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')}function ht(e,t,r){var n=k(r);for(var i=0;i0){var o=n.shift();var s=o.match(/^\s*([a-zA-Z:\-]+:)(.*)/);if(a===0&&s){o.split(":");i=s[1].slice(0,-1);r[i]=s[2]}else{r[i]+=o}a+=Tt(o)}for(var l in r){qt(e,l,r[l])}}}function Lt(t){Re(t);for(var e=0;eG.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){ae(K().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Ft(e){if(!M()){return null}e=D(e);var t=y(localStorage.getItem("htmx-history-cache"))||[];for(var r=0;r=200&&this.status<400){oe(K().body,"htmx:historyCacheMissLoad",o);var e=l(this.response);e=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;var t=Dt();var r=S(t);var n=Me(this.response);if(n){var i=b("title");if(i){i.innerHTML=n}else{window.document.title=n}}Ie(t,e,r);Wt(r.tasks);Mt=a;oe(K().body,"htmx:historyRestore",{path:a,cacheMiss:true,serverResponse:this.response})}else{ae(K().body,"htmx:historyCacheMissLoadError",o)}};e.send()}function zt(e){Bt();e=e||location.pathname+location.search;var t=Ft(e);if(t){var r=l(t.content);var n=Dt();var i=S(n);Ie(n,r,i);Wt(i.tasks);document.title=t.title;setTimeout(function(){window.scrollTo(0,t.scroll)},0);Mt=e;oe(K().body,"htmx:historyRestore",{path:e,item:t})}else{if(G.config.refreshOnHistoryMiss){window.location.reload(true)}else{_t(e)}}}function $t(e){var t=ce(e,"hx-indicator");if(t==null){t=[e]}te(t,function(e){var t=ee(e);t.requestCount=(t.requestCount||0)+1;e.classList["add"].call(e.classList,G.config.requestClass)});return t}function Gt(e){te(e,function(e){var t=ee(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.classList["remove"].call(e.classList,G.config.requestClass)}})}function Jt(e,t){for(var r=0;r=0}function sr(e,t){var r=t?t:Y(e,"hx-swap");var n={swapStyle:ee(e).boosted?"innerHTML":G.config.defaultSwapStyle,swapDelay:G.config.defaultSwapDelay,settleDelay:G.config.defaultSettleDelay};if(ee(e).boosted&&!or(e)){n["show"]="top"}if(r){var i=k(r);if(i.length>0){n["swapStyle"]=i[0];for(var a=1;a0?l.join(":"):null;n["scroll"]=u;n["scrollTarget"]=f}if(o.indexOf("show:")===0){var c=o.substr(5);var l=c.split(":");var h=l.pop();var f=l.length>0?l.join(":"):null;n["show"]=h;n["showTarget"]=f}if(o.indexOf("focus-scroll:")===0){var d=o.substr("focus-scroll:".length);n["focusScroll"]=d=="true"}}}}return n}function lr(e){return Y(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&J(e,"enctype")==="multipart/form-data"}function ur(t,r,n){var i=null;w(r,function(e){if(i==null){i=e.encodeParameters(t,n,r)}});if(i!=null){return i}else{if(lr(r)){return nr(n)}else{return rr(n)}}}function S(e){return{tasks:[],elts:[e]}}function fr(e,t){var r=e[0];var n=e[e.length-1];if(t.scroll){var i=null;if(t.scrollTarget){i=ie(r,t.scrollTarget)}if(t.scroll==="top"&&(r||i)){i=i||r;i.scrollTop=0}if(t.scroll==="bottom"&&(n||i)){i=i||n;i.scrollTop=i.scrollHeight}}if(t.show){var i=null;if(t.showTarget){var a=t.showTarget;if(t.showTarget==="window"){a="body"}i=ie(r,a)}if(t.show==="top"&&(r||i)){i=i||r;i.scrollIntoView({block:"start",behavior:G.config.scrollBehavior})}if(t.show==="bottom"&&(n||i)){i=i||n;i.scrollIntoView({block:"end",behavior:G.config.scrollBehavior})}}}function cr(e,t,r,n){if(n==null){n={}}if(e==null){return n}var i=Z(e,t);if(i){var a=i.trim();var o=r;if(a==="unset"){return null}if(a.indexOf("javascript:")===0){a=a.substr(11);o=true}else if(a.indexOf("js:")===0){a=a.substr(3);o=true}if(a.indexOf("{")!==0){a="{"+a+"}"}var s;if(o){s=hr(e,function(){return Function("return ("+a+")")()},{})}else{s=y(a)}for(var l in s){if(s.hasOwnProperty(l)){if(n[l]==null){n[l]=s[l]}}}}return cr(u(e),t,r,n)}function hr(e,t,r){if(G.config.allowEval){return t()}else{ae(e,"htmx:evalDisallowedError");return r}}function dr(e,t){return cr(e,"hx-vars",true,t)}function vr(e,t){return cr(e,"hx-vals",false,t)}function gr(e){return ne(dr(e),vr(e))}function pr(t,r,n){if(n!==null){try{t.setRequestHeader(r,n)}catch(e){t.setRequestHeader(r,encodeURIComponent(n));t.setRequestHeader(r+"-URI-AutoEncoded","true")}}}function mr(t){if(t.responseURL&&typeof URL!=="undefined"){try{var e=new URL(t.responseURL);return e.pathname+e.search}catch(e){ae(K().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function E(e,t){return e.getAllResponseHeaders().match(t)}function xr(e,t,r){e=e.toLowerCase();if(r){if(r instanceof Element||L(r,"String")){return se(e,t,null,null,{targetOverride:s(r),returnPromise:true})}else{return se(e,t,s(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:s(r.target),swapOverride:r.swap,returnPromise:true})}}else{return se(e,t,null,null,{returnPromise:true})}}function yr(e){var t=[];while(e){t.push(e);e=e.parentElement}return t}function br(e,t,r){var n=new URL(t,document.location.href);var i=document.location.origin;var a=i===n.origin;if(G.config.selfRequestsOnly){if(!a){return false}}return oe(e,"htmx:validateUrl",ne({url:n,sameHost:a},r))}function se(e,t,n,r,i,M){var a=null;var o=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var s=new Promise(function(e,t){a=e;o=t})}if(n==null){n=K().body}var D=i.handler||Sr;if(!re(n)){return}var l=i.targetOverride||de(n);if(l==null||l==fe){ae(n,"htmx:targetError",{target:Z(n,"hx-target")});return}if(!M){var X=function(){return se(e,t,n,r,i,true)};var F={target:l,elt:n,path:t,verb:e,triggeringEvent:r,etc:i,issueRequest:X};if(oe(n,"htmx:confirm",F)===false){return}}var u=n;var f=ee(n);var c=Y(n,"hx-sync");var h=null;var d=false;if(c){var v=c.split(":");var g=v[0].trim();if(g==="this"){u=he(n,"hx-sync")}else{u=ie(n,g)}c=(v[1]||"drop").trim();f=ee(u);if(c==="drop"&&f.xhr&&f.abortable!==true){return}else if(c==="abort"){if(f.xhr){return}else{d=true}}else if(c==="replace"){oe(u,"htmx:abort")}else if(c.indexOf("queue")===0){var U=c.split(" ");h=(U[1]||"last").trim()}}if(f.xhr){if(f.abortable){oe(u,"htmx:abort")}else{if(h==null){if(r){var p=ee(r);if(p&&p.triggerSpec&&p.triggerSpec.queue){h=p.triggerSpec.queue}}if(h==null){h="last"}}if(f.queuedRequests==null){f.queuedRequests=[]}if(h==="first"&&f.queuedRequests.length===0){f.queuedRequests.push(function(){se(e,t,n,r,i)})}else if(h==="all"){f.queuedRequests.push(function(){se(e,t,n,r,i)})}else if(h==="last"){f.queuedRequests=[];f.queuedRequests.push(function(){se(e,t,n,r,i)})}return}}var m=new XMLHttpRequest;f.xhr=m;f.abortable=d;var x=function(){f.xhr=null;f.abortable=false;if(f.queuedRequests!=null&&f.queuedRequests.length>0){var e=f.queuedRequests.shift();e()}};var y=Y(n,"hx-prompt");if(y){var b=prompt(y);if(b===null||!oe(n,"htmx:prompt",{prompt:b,target:l})){Q(a);x();return s}}var w=Y(n,"hx-confirm");if(w){if(!confirm(w)){Q(a);x();return s}}var S=ir(n,l,b);if(i.headers){S=ne(S,i.headers)}var E=er(n,e);var C=E.errors;var R=E.values;if(i.values){R=ne(R,i.values)}var B=gr(n);var O=ne(R,B);var T=ar(O,n);if(e!=="get"&&!lr(n)){S["Content-Type"]="application/x-www-form-urlencoded"}if(G.config.getCacheBusterParam&&e==="get"){T["org.htmx.cache-buster"]=J(l,"id")||"true"}if(t==null||t===""){t=K().location.href}var q=cr(n,"hx-request");var V=ee(n).boosted;var H=G.config.methodsThatUseUrlParams.indexOf(e)>=0;var L={boosted:V,useUrlParams:H,parameters:T,unfilteredParameters:O,headers:S,target:l,verb:e,errors:C,withCredentials:i.credentials||q.credentials||G.config.withCredentials,timeout:i.timeout||q.timeout||G.config.timeout,path:t,triggeringEvent:r};if(!oe(n,"htmx:configRequest",L)){Q(a);x();return s}t=L.path;e=L.verb;S=L.headers;T=L.parameters;C=L.errors;H=L.useUrlParams;if(C&&C.length>0){oe(n,"htmx:validation:halted",L);Q(a);x();return s}var j=t.split("#");var W=j[0];var A=j[1];var N=t;if(H){N=W;var _=Object.keys(T).length!==0;if(_){if(N.indexOf("?")<0){N+="?"}else{N+="&"}N+=rr(T);if(A){N+="#"+A}}}if(!br(n,N,L)){ae(n,"htmx:invalidPath",L);return}m.open(e.toUpperCase(),N,true);m.overrideMimeType("text/html");m.withCredentials=L.withCredentials;m.timeout=L.timeout;if(q.noHeaders){}else{for(var I in S){if(S.hasOwnProperty(I)){var z=S[I];pr(m,I,z)}}}var P={xhr:m,target:l,requestConfig:L,etc:i,boosted:V,pathInfo:{requestPath:t,finalRequestPath:N,anchor:A}};m.onload=function(){try{var e=yr(n);P.pathInfo.responsePath=mr(m);D(n,P);Gt(k);oe(n,"htmx:afterRequest",P);oe(n,"htmx:afterOnLoad",P);if(!re(n)){var t=null;while(e.length>0&&t==null){var r=e.shift();if(re(r)){t=r}}if(t){oe(t,"htmx:afterRequest",P);oe(t,"htmx:afterOnLoad",P)}}Q(a);x()}catch(e){ae(n,"htmx:onLoadError",ne({error:e},P));throw e}};m.onerror=function(){Gt(k);ae(n,"htmx:afterRequest",P);ae(n,"htmx:sendError",P);Q(o);x()};m.onabort=function(){Gt(k);ae(n,"htmx:afterRequest",P);ae(n,"htmx:sendAbort",P);Q(o);x()};m.ontimeout=function(){Gt(k);ae(n,"htmx:afterRequest",P);ae(n,"htmx:timeout",P);Q(o);x()};if(!oe(n,"htmx:beforeRequest",P)){Q(a);x();return s}var k=$t(n);te(["loadstart","loadend","progress","abort"],function(t){te([m,m.upload],function(e){e.addEventListener(t,function(e){oe(n,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});oe(n,"htmx:beforeSend",P);var $=H?null:ur(m,n,T);m.send($);return s}function wr(e,t){var r=t.xhr;var n=null;var i=null;if(E(r,/HX-Push:/i)){n=r.getResponseHeader("HX-Push");i="push"}else if(E(r,/HX-Push-Url:/i)){n=r.getResponseHeader("HX-Push-Url");i="push"}else if(E(r,/HX-Replace-Url:/i)){n=r.getResponseHeader("HX-Replace-Url");i="replace"}if(n){if(n==="false"){return{}}else{return{type:i,path:n}}}var a=t.pathInfo.finalRequestPath;var o=t.pathInfo.responsePath;var s=Y(e,"hx-push-url");var l=Y(e,"hx-replace-url");var u=ee(e).boosted;var f=null;var c=null;if(s){f="push";c=s}else if(l){f="replace";c=l}else if(u){f="push";c=o||a}if(c){if(c==="false"){return{}}if(c==="true"){c=o||a}if(t.pathInfo.anchor&&c.indexOf("#")===-1){c=c+"#"+t.pathInfo.anchor}return{type:f,path:c}}else{return{}}}function Sr(l,u){var f=u.xhr;var c=u.target;var e=u.etc;if(!oe(l,"htmx:beforeOnLoad",u))return;if(E(f,/HX-Trigger:/i)){Xe(f,"HX-Trigger",l)}if(E(f,/HX-Location:/i)){Bt();var t=f.getResponseHeader("HX-Location");var h;if(t.indexOf("{")===0){h=y(t);t=h["path"];delete h["path"]}xr("GET",t,h).then(function(){Vt(t)});return}if(E(f,/HX-Redirect:/i)){location.href=f.getResponseHeader("HX-Redirect");return}if(E(f,/HX-Refresh:/i)){if("true"===f.getResponseHeader("HX-Refresh")){location.reload();return}}if(E(f,/HX-Retarget:/i)){u.target=K().querySelector(f.getResponseHeader("HX-Retarget"))}var d=wr(l,u);var r=f.status>=200&&f.status<400&&f.status!==204;var v=f.response;var n=f.status>=400;var i=ne({shouldSwap:r,serverResponse:v,isError:n},u);if(!oe(c,"htmx:beforeSwap",i))return;c=i.target;v=i.serverResponse;n=i.isError;u.target=c;u.failed=n;u.successful=!n;if(i.shouldSwap){if(f.status===286){Je(l)}w(l,function(e){v=e.transformResponse(v,f,l)});if(d.type){Bt()}var a=e.swapOverride;if(E(f,/HX-Reswap:/i)){a=f.getResponseHeader("HX-Reswap")}var h=sr(l,a);c.classList.add(G.config.swappingClass);var g=null;var p=null;var o=function(){try{var e=document.activeElement;var t={};try{t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null}}catch(e){}var r;if(E(f,/HX-Reselect:/i)){r=f.getResponseHeader("HX-Reselect")}var n=S(c);De(h.swapStyle,c,l,v,n,r);if(t.elt&&!re(t.elt)&&J(t.elt,"id")){var i=document.getElementById(J(t.elt,"id"));var a={preventScroll:h.focusScroll!==undefined?!h.focusScroll:!G.config.defaultFocusScroll};if(i){if(t.start&&i.setSelectionRange){try{i.setSelectionRange(t.start,t.end)}catch(e){}}i.focus(a)}}c.classList.remove(G.config.swappingClass);te(n.elts,function(e){if(e.classList){e.classList.add(G.config.settlingClass)}oe(e,"htmx:afterSwap",u)});if(E(f,/HX-Trigger-After-Swap:/i)){var o=l;if(!re(l)){o=K().body}Xe(f,"HX-Trigger-After-Swap",o)}var s=function(){te(n.tasks,function(e){e.call()});te(n.elts,function(e){if(e.classList){e.classList.remove(G.config.settlingClass)}oe(e,"htmx:afterSettle",u)});if(d.type){if(d.type==="push"){Vt(d.path);oe(K().body,"htmx:pushedIntoHistory",{path:d.path})}else{jt(d.path);oe(K().body,"htmx:replacedInHistory",{path:d.path})}}if(u.pathInfo.anchor){var e=b("#"+u.pathInfo.anchor);if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}if(n.title){var t=b("title");if(t){t.innerHTML=n.title}else{window.document.title=n.title}}fr(n.elts,h);if(E(f,/HX-Trigger-After-Settle:/i)){var r=l;if(!re(l)){r=K().body}Xe(f,"HX-Trigger-After-Settle",r)}Q(g)};if(h.settleDelay>0){setTimeout(s,h.settleDelay)}else{s()}}catch(e){ae(l,"htmx:swapError",u);Q(p);throw e}};var s=G.config.globalViewTransitions;if(h.hasOwnProperty("transition")){s=h.transition}if(s&&oe(l,"htmx:beforeTransition",u)&&typeof Promise!=="undefined"&&document.startViewTransition){var m=new Promise(function(e,t){g=e;p=t});var x=o;o=function(){document.startViewTransition(function(){x();return m})}}if(h.swapDelay>0){setTimeout(o,h.swapDelay)}else{o()}}if(n){ae(l,"htmx:responseError",ne({error:"Response Status Error Code "+f.status+" from "+u.pathInfo.requestPath},u))}}var Er={};function Cr(){return{init:function(e){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,r){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,r,n){return false},encodeParameters:function(e,t,r){return null}}}function Rr(e,t){if(t.init){t.init(C)}Er[e]=ne(Cr(),t)}function Or(e){delete Er[e]}function Tr(e,r,n){if(e==undefined){return r}if(r==undefined){r=[]}if(n==undefined){n=[]}var t=Z(e,"hx-ext");if(t){te(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){n.push(e.slice(7));return}if(n.indexOf(e)<0){var t=Er[e];if(t&&r.indexOf(t)<0){r.push(t)}}})}return Tr(u(e),r,n)}var qr=false;K().addEventListener("DOMContentLoaded",function(){qr=true});function Hr(e){if(qr||K().readyState==="complete"){e()}else{K().addEventListener("DOMContentLoaded",e)}}function Lr(){if(G.config.includeIndicatorStyles!==false){K().head.insertAdjacentHTML("beforeend","")}}function Ar(){var e=K().querySelector('meta[name="htmx-config"]');if(e){return y(e.content)}else{return null}}function Nr(){var e=Ar();if(e){G.config=ne(G.config,e)}}Hr(function(){Nr();Lr();var e=K().body;Nt(e);var t=K().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){var t=e.target;var r=ee(t);if(r&&r.xhr){r.xhr.abort()}});var r=window.onpopstate;window.onpopstate=function(e){if(e.state&&e.state.htmx){zt();te(t,function(e){oe(e,"htmx:restored",{document:K(),triggerEvent:oe})})}else{if(r){r(e)}}};setTimeout(function(){oe(e,"htmx:load",{});e=null},0)});return G}()}); \ No newline at end of file diff --git a/chapter-10/2-adding-contacts/static/js/overflow.js b/chapter-10/2-adding-contacts/static/js/overflow.js new file mode 100644 index 00000000..d9d4e92d --- /dev/null +++ b/chapter-10/2-adding-contacts/static/js/overflow.js @@ -0,0 +1,67 @@ +// @ts-nocheck + +function overflowMenu(subtree = document) { + subtree.querySelectorAll("[data-overflow-menu]").forEach(menuRoot => { + const + button = menuRoot.querySelector("[aria-haspopup]"), + menu = menuRoot.querySelector("[role=menu]"), + items = [...menu.querySelectorAll("[role=menuitem]")]; + + const isOpen = () => !menu.hidden; + + items.forEach(item => item.setAttribute("tabindex", "-1")); + + function toggleMenu(open = !isOpen()) { + if (open) { + menu.hidden = false; + button.setAttribute("aria-expanded", "true"); + items[0].focus(); + } else { + menu.hidden = true; + button.setAttribute("aria-expanded", "false"); + } + } + + toggleMenu(isOpen()); + button.addEventListener("click", () => toggleMenu()); + menuRoot.addEventListener("blur", e => console.log(e) || toggleMenu(false)); + + window.addEventListener("click", function clickAway(event) { + if (!menuRoot.isConnected) window.removeEventListener("click", clickAway); + if (!menuRoot.contains(event.target)) toggleMenu(false); + }) + + const currentIndex = () => { + const idx = items.indexOf(document.activeElement); + if (idx === -1) return 0; + return idx; + } + + menuRoot.addEventListener("keydown", e => { + if (e.key === "ArrowUp") { + items[currentIndex() - 1]?.focus(); + + } else if (e.key === "ArrowDown") { + items[currentIndex() + 1]?.focus(); + + } else if (e.key === "Space") { + items[currentIndex()].click(); + + } else if (e.key === "Home") { + items[0].focus(); + + } else if (e.key === "End") { + items[items.length - 1].focus(); + + } else if (e.key === "Escape") { + toggleMenu(false); + button.focus(); + + } else if (e.key === "Tab") { + toggleMenu(false); + } + }) + }) +} + +addEventListener("htmx:load", e => overflowMenu(e.target)); diff --git a/chapter-10/2-adding-contacts/static/js/swal.js b/chapter-10/2-adding-contacts/static/js/swal.js new file mode 100644 index 00000000..4bc86a84 --- /dev/null +++ b/chapter-10/2-adding-contacts/static/js/swal.js @@ -0,0 +1,7 @@ +function sweetConfirm(elt, config) { + Swal.fire(config).then((result) => { + if (result.isConfirmed) { + elt.dispatchEvent(new Event("confirmed")); + } + }); +} diff --git a/chapter-10/2-adding-contacts/templates/archive_ui.html b/chapter-10/2-adding-contacts/templates/archive_ui.html new file mode 100644 index 00000000..b83b7037 --- /dev/null +++ b/chapter-10/2-adding-contacts/templates/archive_ui.html @@ -0,0 +1,21 @@ +
+ {% if archiver.status() == "Waiting" %} + + {% elif archiver.status() == "Running" %} +
+ Creating Archive... +
(1) +
+
+
+ {% elif archiver.status() == "Complete" %} + Archive Ready! Click here to download. ↓ + + {% endif %} +
diff --git a/chapter-10/2-adding-contacts/templates/edit.html b/chapter-10/2-adding-contacts/templates/edit.html new file mode 100644 index 00000000..6a611b73 --- /dev/null +++ b/chapter-10/2-adding-contacts/templates/edit.html @@ -0,0 +1,50 @@ +{% extends 'layout.html' %} + +{% block content %} + +
+
+ Contact Values +

+ + + {{ contact.errors['email'] }} +

+

+ + + {{ contact.errors['first'] }} +

+

+ + + {{ contact.errors['last'] }} +

+

+ + + {{ contact.errors['phone'] }} +

+ +
+
+ + + +

+ Back +

+ +{% endblock %} \ No newline at end of file diff --git a/chapter-10/2-adding-contacts/templates/index.html b/chapter-10/2-adding-contacts/templates/index.html new file mode 100644 index 00000000..914d572d --- /dev/null +++ b/chapter-10/2-adding-contacts/templates/index.html @@ -0,0 +1,96 @@ +{% extends 'layout.html' %} {% block content %} {% include 'archive_ui.html' %} +
+ + + Request In Flight... + +
+
+ + + + + + + + + + + + + + {% include 'rows.html' %} {% if contacts|length == 10 %} + + + + {% endif %} + +
FirstLastPhoneEmail
+ + Loading More + +
+ +
+

+ Add Contact + +

+{% endblock %} diff --git a/chapter-10/2-adding-contacts/templates/layout.html b/chapter-10/2-adding-contacts/templates/layout.html new file mode 100644 index 00000000..dc8e7c83 --- /dev/null +++ b/chapter-10/2-adding-contacts/templates/layout.html @@ -0,0 +1,24 @@ + + + + Contact App + + + + + + + + + +
+
+

+ contacts.app + A Demo Contacts Application +

+
+ {% block content %}{% endblock %} +
+ + diff --git a/chapter-10/2-adding-contacts/templates/new.html b/chapter-10/2-adding-contacts/templates/new.html new file mode 100644 index 00000000..285d5f8b --- /dev/null +++ b/chapter-10/2-adding-contacts/templates/new.html @@ -0,0 +1,39 @@ +{% extends 'layout.html' %} + +{% block content %} + +
+
+ Contact Values +
+

+ + + {{ contact.errors['email'] }} +

+

+ + + {{ contact.errors['first'] }} +

+

+ + + {{ contact.errors['last'] }} +

+

+ + + {{ contact.errors['phone'] }} +

+
+ +
+
+ +

+ Back +

+ + +{% endblock %} \ No newline at end of file diff --git a/chapter-10/2-adding-contacts/templates/rows.html b/chapter-10/2-adding-contacts/templates/rows.html new file mode 100644 index 00000000..150eef2f --- /dev/null +++ b/chapter-10/2-adding-contacts/templates/rows.html @@ -0,0 +1,42 @@ +{% for contact in contacts %} + + + + + {{ contact.first }} + {{ contact.last }} + {{ contact.phone }} + {{ contact.email }} + +
+ + +
+ + +{% endfor %} diff --git a/chapter-10/2-adding-contacts/templates/show.html b/chapter-10/2-adding-contacts/templates/show.html new file mode 100644 index 00000000..58bf3ff1 --- /dev/null +++ b/chapter-10/2-adding-contacts/templates/show.html @@ -0,0 +1,18 @@ +{% extends 'layout.html' %} + +{% block content %} + +

{{contact.first}} {{contact.last}}

+ +
+
Phone: {{contact.phone}}
+
Email: {{contact.email}}
+
+ +

+ Edit + Back +

+ + +{% endblock %} \ No newline at end of file diff --git a/chapter-10/2-adding-contacts/test_model.py b/chapter-10/2-adding-contacts/test_model.py new file mode 100644 index 00000000..398d089c --- /dev/null +++ b/chapter-10/2-adding-contacts/test_model.py @@ -0,0 +1,40 @@ +from contacts_model import Contact +from typing import Any + +Contact.load_db() + + +def test_contacts_all() -> None: + assert Contact.all()[0].id == 2 + assert Contact.all()[0].first == "Carson" + + +def test_contacts_search() -> None: + assert Contact.search("carson")[0].id == 2 + assert Contact.search("carson")[0].first == "Carson" + + +def test_contacts_validate_pass() -> None: + c: Contact = Contact(None, "Jane", "Doe", "555-555-5555", "jane.doe@example.com") + assert c.validate() + + +def test_contacts_validate_email_missing() -> None: + c: Contact = Contact(None, "Jane", "Doe", "555-555-5555") + assert not c.validate() + + +def test_contacts_validate_email_not_unique() -> None: + c: Contact = Contact(None, "Jane", "Doe", "555-555-5555" "carson@example.comz") + assert not c.validate() + + +def test_contacts_find() -> None: + assert Contact.find(2) + assert not Contact.find(200) + + +def test_contacts_delete() -> None: + contact: Any | None = Contact.find(Contact.all()[-1].id) + if contact is not None: + assert Contact.delete(contact) diff --git a/chapter-10/2-adding-contacts/test_routes.py b/chapter-10/2-adding-contacts/test_routes.py new file mode 100644 index 00000000..4fb6d2ff --- /dev/null +++ b/chapter-10/2-adding-contacts/test_routes.py @@ -0,0 +1,109 @@ +from app import app +import json +from typing import Any +import uuid +from werkzeug.test import TestResponse + + +def get_last_contact_id() -> int: + with open("contacts.json", "r") as contacts_file: + contacts: Any = json.load(contacts_file) + sorted_contacts: Any = sorted(contacts, key=lambda x: x["id"]) + return sorted_contacts[-1]["id"] + + +def test_index() -> None: + response: TestResponse = app.test_client().get("/") + assert response.status_code == 302 + + +def test_contacts_all() -> None: + response: TestResponse = app.test_client().get("/contacts") + assert b"Carson" in response.data + + +def test_contacts_page() -> None: + response: TestResponse = app.test_client().get("/contacts?page=2") + assert b"Blow" in response.data + + +def test_contacts_search() -> None: + response: TestResponse = app.test_client().get("/contacts?q=carson") + assert b"Carson" in response.data + + +def test_contacts_new_get() -> None: + response: TestResponse = app.test_client().get("/contacts/new") + assert b"Contact Values" in response.data + + +def test_contacts_new_post() -> None: + response: TestResponse = app.test_client().post( + "/contacts/new", + data={ + "first_name": "Test", + "last_name": "Contact", + "phone": "555-555-5555", + "email": f"{str(uuid.uuid4())[:8]}@example.com", + }, + ) + assert response.status_code == 302 + + +def test_contacts_view() -> None: + response: TestResponse = app.test_client().get("/contacts/2") + assert b"

Carson Gross

" in response.data + + +def test_contact_view_not_exist() -> None: + response: TestResponse = app.test_client().get("/contacts/200") + assert b"

" in response.data + + +def test_contacts_edit() -> None: + response: TestResponse = app.test_client().get("/contacts/2/edit") + assert b'hx-get="/contacts/2/email"' in response.data + + +def test_contacts_edit_not_exists() -> None: + response: TestResponse = app.test_client().get("/contacts/200/edit") + assert response.status_code == 500 + + +def test_contacts_edit_post() -> None: + response: TestResponse = app.test_client().post( + f"/contacts/{get_last_contact_id()}/edit", + data={ + "first_name": "Test", + "last_name": "Contact", + "phone": "666-666-6666", + "email": f"{str(uuid.uuid4())[:8]}@example.com", + }, + ) + assert response.status_code == 302 + + +def test_contacts_email_get_no_email() -> None: + response: TestResponse = app.test_client().get("/contacts/2/email") + assert b"Email Required" in response.data + + +def test_contacts_email_get_non_unique() -> None: + response: TestResponse = app.test_client().get( + "/contacts/2/email?email=joe@example.com" + ) + assert b"Email Must Be Unique" in response.data + + +def test_contacts_email_get_unique() -> None: + response: TestResponse = app.test_client().get( + "/contacts/2/email?email=carson@example.comz" + ) + assert b"" in response.data + + +def test_contacts_delete_post() -> None: + response: TestResponse = app.test_client().delete( + f"/contacts/{get_last_contact_id()}" + ) + assert response.status_code == 303 or 200