commit 1d204f26b8d55a71ada16bb08534b5f9e7059730 Author: apeters Date: Wed May 21 08:05:07 2025 +0000 pre-Korves.Net Signed-off-by: apeters diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..17d1877 --- /dev/null +++ b/.gitignore @@ -0,0 +1,168 @@ +system/certs/* +database/ +logs/* +misc/* + +.sync +rsync.sh + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/components/cache.py b/components/cache.py new file mode 100644 index 0000000..608d635 --- /dev/null +++ b/components/cache.py @@ -0,0 +1,25 @@ +from components.database import IN_MEMORY_DB +from components.models import UUID, validate_call +from components.utils import ensure_list + + +@validate_call +def buster(object_id: UUID | list[UUID]): + object_ids = ensure_list(object_id) + for object_id in object_ids: + object_id = str(object_id) + + for user_id in IN_MEMORY_DB["OBJECTS_CACHE"]: + cached_keys = list(IN_MEMORY_DB["OBJECTS_CACHE"][user_id].keys()) + if object_id in cached_keys: + mapping_name = IN_MEMORY_DB["OBJECTS_CACHE"][user_id][object_id].name + if object_id in IN_MEMORY_DB["OBJECTS_CACHE"][user_id]: + del IN_MEMORY_DB["OBJECTS_CACHE"][user_id][object_id] + + for user_id in IN_MEMORY_DB["FORM_OPTIONS_CACHE"]: + for option in IN_MEMORY_DB["FORM_OPTIONS_CACHE"][user_id].copy(): + if any( + d["value"] == object_id + for d in IN_MEMORY_DB["FORM_OPTIONS_CACHE"][user_id][option] + ): + del IN_MEMORY_DB["FORM_OPTIONS_CACHE"][user_id][option] diff --git a/components/cluster/__init__.py b/components/cluster/__init__.py new file mode 100644 index 0000000..f840834 --- /dev/null +++ b/components/cluster/__init__.py @@ -0,0 +1,624 @@ +import asyncio +import base64 +import json +import os +import random +import re +import socket +import sys +import zlib + +from config import defaults +from contextlib import closing, suppress +from components.cluster.exceptions import ( + IncompleteClusterResponses, + DistLockCancelled, + UnknownPeer, + ZombiePeer, +) +from components.cluster.cli import cli_processor +from components.models.cluster import ( + CritErrors, + Role, + FilePath, + validate_call, + ConnectionStatus, +) +from components.cluster.leader import elect_leader +from components.cluster.peers import Peers +from components.cluster.monitor import Monitor +from components.cluster.ssl import get_ssl_context +from components.logs import logger +from components.database import * +from components.utils.cryptography import dict_digest_sha1 +from components.utils.datetimes import ntime_utc_now +from components.utils import ensure_list, is_path_within_cwd, chunk_string + + +class Cluster: + def __init__(self, peers, port): + self.locks = dict() + self.port = port + self.receiving = asyncio.Condition() + self.tickets = dict() + self._partial_tickets = dict() + self.monitor = Monitor(self) + self.server_limit = 104857600 # 100 MiB + self.peers = Peers(peers) + self.sending = asyncio.Lock() + self._session_patched_tables = dict() + + def _release_tables(self, tables): + for t in tables: + with suppress(RuntimeError): + self.locks[t]["lock"].release() + self.locks[t]["ticket"] = None + + async def read_command(self, reader: asyncio.StreamReader) -> tuple[str, str, dict]: + bytes_to_read = int.from_bytes(await reader.readexactly(4), "big") + input_bytes = await reader.readexactly(bytes_to_read) + + input_decoded = input_bytes.strip().decode("utf-8") + data, _, meta = input_decoded.partition(" :META ") + ticket, _, cmd = data.partition(" ") + + patterns = [ + r"NAME (?P\S+)", + r"SWARM (?P\S+)", + r"STARTED (?P\S+)", + r"LEADER (?P\S+)", + ] + + match = re.search(" ".join(patterns), meta) + meta_dict = match.groupdict() + name = meta_dict["name"] + + if not name in self.peers.remotes: + raise UnknownPeer(name) + + self.peers.remotes[name].leader = meta_dict["leader"] + self.peers.remotes[name].started = float(meta_dict["started"]) + self.peers.remotes[name].swarm = meta_dict["swarm"] + + msg = cmd[:150] + (cmd[150:] and "...") + logger.debug(f"[← Receiving from {name}][{ticket}] - {msg}") + + return ticket, cmd, meta_dict + + async def incoming_handler( + self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter + ): + monitor_init = False + raddr, *_ = writer.get_extra_info("peername") + socket, *_ = writer.get_extra_info("socket").getsockname() + + if socket and raddr in defaults.CLUSTER_CLI_BINDINGS: + return await cli_processor((reader, writer)) + + if raddr not in self.peers.remote_ips(): + raise UnknownPeer(raddr) + + while True: + try: + ticket, cmd, peer_meta = await self.read_command(reader) + if not monitor_init: + self.peers.remotes[peer_meta["name"]].streams._in = ( + reader, + writer, + ) + await self.monitor.start_peer_monitoring(peer_meta["name"]) + monitor_init = True + except (asyncio.exceptions.IncompleteReadError, ConnectionResetError): + break + except TimeoutError as e: + if str(e) == "SSL shutdown timed out": + break + raise + except ZombiePeer: + await self.send_command( + CritErrors.ZOMBIE.response, peer_meta["name"], ticket=ticket + ) + break + try: + if not self.peers.local.leader and not any( + map( + lambda s: cmd.startswith(s), + ["ACK", "STATUS", "FULLTABLE", "INIT"], + ) + ): + await self.send_command( + CritErrors.NOT_READY.response, peer_meta["name"], ticket=ticket + ) + + elif cmd.startswith("PATCHTABLE") or cmd.startswith("FULLTABLE"): + _, _, payload = cmd.partition(" ") + table_w_hash, table_payload = payload.split(" ") + table, table_digest = table_w_hash.split("@") + db_params = evaluate_db_params(ticket) + + async with TinyDB(**db_params) as db: + if not table in db.tables(): + await self.send_command( + CritErrors.NO_SUCH_TABLE.response, + peer_meta["name"], + ticket=ticket, + ) + else: + if not ticket in self._session_patched_tables: + self._session_patched_tables[ticket] = set() + + self._session_patched_tables[ticket].add(table) + + try: + if cmd.startswith("PATCHTABLE"): + table_data = { + doc.doc_id: doc for doc in db.table(table).all() + } + errors = [] + local_table_digest = dict_digest_sha1(table_data) + if local_table_digest != table_digest: + await self.send_command( + CritErrors.TABLE_HASH_MISMATCH.response, + peer_meta["name"], + ticket=ticket, + ) + continue + + diff = json.loads(base64.b64decode(table_payload)) + + for doc_id, docs in diff["changed"].items(): + a, b = docs + c = db.table(table).get(doc_id=doc_id) + if c != a: + await self.send_command( + CritErrors.DOC_MISMATCH.response, + peer_meta["name"], + ticket=ticket, + ) + break + db.table(table).upsert( + Document(b, doc_id=doc_id) + ) + else: # if no break occured, continue + for doc_id, doc in diff["added"].items(): + db.table(table).insert( + Document(doc, doc_id=doc_id) + ) + db.table(table).remove( + Query().id.one_of( + [ + doc["id"] + for doc in diff["removed"].values() + ] + ) + ) + + elif cmd.startswith("FULLTABLE"): + insert_data = json.loads( + base64.b64decode(table_payload) + ) + db.table(table).truncate() + for doc_id, doc in insert_data.items(): + db.table(table).insert( + Document(doc, doc_id=doc_id) + ) + + await self.send_command( + "ACK", peer_meta["name"], ticket=ticket + ) + + except Exception as e: + await self.send_command( + CritErrors.CANNOT_APPLY.response, + peer_meta["name"], + ticket=ticket, + ) + continue + + elif cmd == "COMMIT": + if not ticket in self._session_patched_tables: + await self.send_command( + CritErrors.NOTHING_TO_COMMIT.response, + peer_meta["name"], + ticket=ticket, + ) + continue + + commit_tables = self._session_patched_tables[ticket] + del self._session_patched_tables[ticket] + await dbcommit(commit_tables, ticket) + await self.send_command("ACK", peer_meta["name"], ticket=ticket) + + elif cmd == "STATUS" or cmd == "INIT": + await self.send_command("ACK", peer_meta["name"], ticket=ticket) + + elif cmd == "BYE": + self.peers.remotes[peer_meta["name"]] = self.peers.remotes[ + peer_meta["name"] + ].reset() + + elif cmd.startswith("FILEGET"): + _, _, payload = cmd.partition(" ") + start, end, file = payload.split(" ") + + if not is_path_within_cwd(file) or not os.path.exists(file): + await self.send_command( + CritErrors.INVALID_FILE_PATH.response, + peer_meta["name"], + ticket=ticket, + ) + continue + + if os.stat(file).st_size < int(start): + await self.send_command( + CritErrors.START_BEHIND_FILE_END.response, + peer_meta["name"], + ticket=ticket, + ) + continue + + with open(file, "rb") as f: + f.seek(int(start)) + compressed_data = zlib.compress(f.read(int(end))) + compressed_data_encoded = base64.b64encode( + compressed_data + ).decode("utf-8") + + chunks = chunk_string(f"{file} {compressed_data_encoded}") + for idx, c in enumerate(chunks, 1): + await self.send_command( + f"PACK CHUNKED {idx} {len(chunks)} {c}", + peer_meta["name"], + ticket=ticket, + ) + + elif cmd.startswith("UNLOCK"): + _, _, tables_str = cmd.partition(" ") + tables = tables_str.split(",") + + for t in tables: + with suppress(RuntimeError): + self.locks[t]["lock"].release() + self.locks[t]["ticket"] = None + + await self.send_command("ACK", peer_meta["name"], ticket=ticket) + + elif cmd.startswith("LOCK"): + _, _, tables_str = cmd.partition(" ") + tables = tables_str.split(",") + + if peer_meta["swarm"] != self.peers.local.swarm: + await self.send_command( + CritErrors.PEERS_MISMATCH.response, + peer_meta["name"], + ticket=ticket, + ) + continue + + try: + for t in tables: + if t not in self.locks: + self.locks[t] = { + "lock": asyncio.Lock(), + "ticket": None, + } + await asyncio.wait_for( + self.locks[t]["lock"].acquire(), + 0.3 + random.uniform(0.1, 0.5), + ) + self.locks[t]["ticket"] = ticket + + except TimeoutError: + await self.send_command( + "ACK BUSY", peer_meta["name"], ticket=ticket + ) + else: + await self.send_command("ACK", peer_meta["name"], ticket=ticket) + + elif cmd.startswith("PACK"): + if not ticket in self._partial_tickets: + self._partial_tickets[ticket] = [] + + _, _, payload = cmd.partition(" ") + _, idx, total, partial_data = payload.split(" ", 3) + + self._partial_tickets[ticket].append(partial_data) + if idx == total: + self.tickets[ticket].add( + (peer_meta["name"], "".join(self._partial_tickets[ticket])) + ) + + elif cmd.startswith("ACK"): + _, _, payload = cmd.partition(" ") + if ticket in self.tickets: + self.tickets[ticket].add((peer_meta["name"], payload)) + + async with self.receiving: + self.receiving.notify_all() + + except ConnectionResetError: + break + + async def send_command(self, cmd, peers, ticket: str | None = None): + if not ticket: + ticket = str(ntime_utc_now()) + + if peers == "*": + peers = self.peers.remotes.keys() + else: + peers = ensure_list(peers) + + if ticket not in self.tickets: + self.tickets[ticket] = set() + + successful_receivers = [] + + for name in peers: + async with self.peers.remotes[name].sending_lock: + con, status = await self.peers.remotes[name].connect() + if con: + reader, writer = con + buffer_data = [ + ticket, + cmd, + ":META", + f"NAME {self.peers.local.name}", + "SWARM {swarm}".format( + swarm=self.peers.local.swarm or "?CONFUSED" + ), + f"STARTED {self.peers.local.started}", + "LEADER {leader}".format( + leader=self.peers.local.leader or "?CONFUSED" + ), + ] + buffer_bytes = " ".join(buffer_data).encode("utf-8") + writer.write(len(buffer_bytes).to_bytes(4, "big")) + writer.write(buffer_bytes) + await writer.drain() + + msg = cmd[:150] + (cmd[150:] and "...") + logger.debug(f"[→ Sending to {name}][{ticket}] - {msg}") + + successful_receivers.append(name) + else: + logger.warning(f"Cannot send to peer {name} - {status}") + + return ticket, successful_receivers + + async def release(self, tables: list = ["main"]) -> str: + CTX_TICKET.reset(self._ctx_ticket) + + if self.peers.local.role == Role.FOLLOWER: + try: + ticket, receivers = await self.send_command( + f"UNLOCK {','.join(tables)}", + self.peers.local.leader, + ) + async with self.receiving: + ret, responses = await self.await_receivers( + ticket, receivers, raise_err=True + ) + except IncompleteClusterResponses: + self._release_tables(tables) + raise DistLockCancelled( + "Leader did not respond properly to unlock request" + ) + + self._release_tables(tables) + + async def acquire_lock(self, tables: list = ["main"]) -> str: + try: + ticket = str(ntime_utc_now()) + for t in tables: + if t not in self.locks: + self.locks[t] = { + "lock": asyncio.Lock(), + "ticket": None, + } + + await asyncio.wait_for(self.locks[t]["lock"].acquire(), 20.0) + self.locks[t]["ticket"] = ticket + + except TimeoutError: + raise DistLockCancelled("Unable to acquire local lock") + + if self.peers.local.role == Role.FOLLOWER: + try: + if not self.peers.local.leader: + self._release_tables(tables) + raise IncompleteClusterResponses("Cluster is not ready") + + ticket, receivers = await self.send_command( + f"LOCK {','.join(tables)}", self.peers.local.leader + ) + + async with self.receiving: + result, responses = await self.await_receivers( + ticket, receivers, raise_err=False + ) + + if "BUSY" in responses: + self._release_tables(tables) + return await self.acquire_lock(tables) + + elif not result: + self._release_tables(tables) + if CritErrors.PEERS_MISMATCH in responses: + raise DistLockCancelled( + "Leader rejected lock due to member inconsistency" + ) + else: + raise DistLockCancelled( + "Cannot acquire lock from leader, trying a re-election" + ) + + except asyncio.CancelledError: + self._release_tables(tables) + raise DistLockCancelled("Application raised CancelledError") + + self._ctx_ticket = CTX_TICKET.set(ticket) + + @validate_call + async def request_files( + self, + files: FilePath | list[FilePath], + peers: str | list, + start: int = 0, + end: int = -1, + ): + assert self.locks["files"]["lock"].locked() + for file in ensure_list(files): + try: + if not is_path_within_cwd(file): + logger.error(f"File not within cwd: {file}") + continue + + for peer in ensure_list(peers): + peer_start = start + peer_end = end + assert peer in self.peers.remotes + if peer_start == -1: + if os.path.exists(f"peer_files/{peer}/{file}"): + peer_start = os.stat(f"peer_files/{peer}/{file}").st_size + else: + peer_start = 0 + + ticket, receivers = await self.send_command( + f"FILEGET {peer_start} {peer_end} {file}", peer + ) + async with self.receiving: + _, response = await self.await_receivers( + ticket, receivers, raise_err=True + ) + + for r in response: + r_file, r_data = r.split(" ") + assert FilePath(r_file) == file + file_dest = f"peer_files/{peer}/{file}" + os.makedirs(os.path.dirname(file_dest), exist_ok=True) + payload = zlib.decompress(base64.b64decode(r_data)) + if os.path.exists(file_dest): + mode = "r+b" + else: + mode = "w+b" + with open(file_dest, mode) as f: + f.seek(peer_start) + f.write(payload) + + except IncompleteClusterResponses: + logger.error(f"Sending command to peers '{peers}' failed") + raise + except Exception as e: + logger.error(f"Unhandled error: {e}") + raise + + async def run(self, shutdown_trigger) -> None: + server = await asyncio.start_server( + self.incoming_handler, + self.peers.local._all_bindings_as_str, + self.port, + ssl=get_ssl_context("server"), + limit=self.server_limit, + ) + self._shutdown = shutdown_trigger + + logger.info( + f"Listening on {self.port} on address {' and '.join(self.peers.local._all_bindings_as_str)}..." + ) + + async with server: + for name in self.peers.remotes: + ip, status = self.peers.remotes[name]._eval_ip() + if ip: + con, status = await self.peers.remotes[name].connect(ip) + if con: + ticket, receivers = await self.send_command("INIT", name) + async with self.receiving: + ret, responses = await self.await_receivers( + ticket, receivers, raise_err=False + ) + if CritErrors.ZOMBIE in responses: + logger.critical( + f"Peer {name} has not yet disconnected a previous session: {status}" + ) + shutdown_trigger.set() + break + else: + logger.debug(f"Not sending INIT to peer {name}: {status}") + else: + logger.debug(f"Not sending INIT to peer {name}: {status}") + + t = asyncio.create_task(self.monitor._ticket_worker(), name="tickets") + self.monitor.tasks.add(t) + t.add_done_callback(self.monitor.tasks.discard) + + try: + binds = [s.getsockname()[0] for s in server.sockets] + for local_rbind in self.peers.local._bindings_as_str: + if local_rbind not in binds: + logger.critical( + f"Could not bind requested address {local_rbind}" + ) + shutdown_trigger.set() + break + else: + await shutdown_trigger.wait() + except asyncio.CancelledError: + if self.peers.get_established(): + await self.send_command("BYE", "*") + + [t.cancel() for t in self.monitor.tasks] + + async def await_receivers( + self, + ticket, + receivers, + raise_err: bool, + timeout: float = defaults.CLUSTER_PEERS_TIMEOUT * len(defaults.CLUSTER_PEERS), + ): + errors = [] + missing_receivers = [] + + assert self.receiving.locked() + + if timeout >= defaults.CLUSTER_PEERS_TIMEOUT * len(defaults.CLUSTER_PEERS) + 10: + raise ValueError("Timeout is too high") + + try: + while not all( + r in [peer for peer, _ in self.tickets[ticket]] for r in receivers + ): + await asyncio.wait_for(self.receiving.wait(), timeout) + except TimeoutError: + missing_receivers = [ + r + for r in receivers + if r not in [peer for peer, _ in self.tickets[ticket]] + ] + errors.append( + f"Timeout waiting for receviers: {', '.join(missing_receivers)}" + ) + + finally: + responses = [] + + for peer, response in self.tickets[ticket]: + if response in CritErrors._value2member_map_: + responses.append(CritErrors(response)) + errors.append(f"CRIT response from {peer}: {CritErrors(response)}") + else: + responses.append(response) + + if not missing_receivers and len(responses) != len(receivers): + errors.append("Unplausible amount of responses for ticket") + + self.tickets.pop(ticket, None) + + if errors: + logger.error("\n".join(errors)) + if raise_err: + raise IncompleteClusterResponses("\n".join(errors)) + else: + logger.success(f"{ticket} OK") + + return not errors, responses diff --git a/components/cluster/cli.py b/components/cluster/cli.py new file mode 100644 index 0000000..193f18d --- /dev/null +++ b/components/cluster/cli.py @@ -0,0 +1,57 @@ +import asyncio +import json +import random + + +async def cli_processor(streams: tuple[asyncio.StreamReader, asyncio.StreamWriter]): + from components.users import what_id, get + from components.database import IN_MEMORY_DB + + try: + reader, writer = streams + while not reader.at_eof(): + cmd = await reader.readexactly(1) + if cmd == b"\x97": + data = await reader.readuntil(b"\n") + login = data.strip().decode("utf-8") + try: + user_id = await what_id(login=login) + user = await get(user_id=user_id) + if "system" not in user.acl: + IN_MEMORY_DB["PROMOTE_USERS"].add(user_id) + writer.write(b"\x01") + else: + writer.write(b"\x02") + except Exception as e: + writer.write(b"\x03") + await writer.drain() + elif cmd == b"\x98": + awaiting = dict() + idx = 1 + for k, v in IN_MEMORY_DB.items(): + if ( + isinstance(v, dict) + and v.get("token_type") == "cli_confirmation" + ): + awaiting[idx] = (k, v["intention"]) + idx += 1 + writer.write(f"{json.dumps(awaiting)}\n".encode("ascii")) + await writer.drain() + elif cmd == b"\x99": + data = await reader.readexactly(14) + confirmed = data.strip().decode("ascii") + code = "%06d" % random.randint(0, 999999) + IN_MEMORY_DB.get(confirmed, {}).update( + {"status": "confirmed", "code": code} + ) + writer.write(f"{code}\n".encode("ascii")) + await writer.drain() + except Exception as e: + if type(e) not in [ + asyncio.exceptions.IncompleteReadError, + ConnectionResetError, + ]: + raise + finally: + writer.close() + await writer.wait_closed() diff --git a/components/cluster/cluster.py b/components/cluster/cluster.py new file mode 100644 index 0000000..30c28c6 --- /dev/null +++ b/components/cluster/cluster.py @@ -0,0 +1,4 @@ +from config.defaults import CLUSTER_PEERS +from components.cluster import Cluster + +cluster = Cluster(peers=CLUSTER_PEERS, port=2102) diff --git a/components/cluster/exceptions.py b/components/cluster/exceptions.py new file mode 100644 index 0000000..68f305a --- /dev/null +++ b/components/cluster/exceptions.py @@ -0,0 +1,23 @@ +from werkzeug.exceptions import HTTPException + + +class DistLockCancelled(Exception): + pass + + +class IncompleteClusterResponses(Exception): + pass + + +class ZombiePeer(Exception): + pass + + +class UnknownPeer(Exception): + pass + + +class ClusterHTTPException(HTTPException): + def __init__(self, description=None): + super().__init__(description) + self.code = 999 diff --git a/components/cluster/leader.py b/components/cluster/leader.py new file mode 100644 index 0000000..717c996 --- /dev/null +++ b/components/cluster/leader.py @@ -0,0 +1,60 @@ +from components.logs import logger +from components.models.cluster import Role + + +def elect_leader(peers: "Peers") -> None: + def _destroy(): + peers.local.leader = None + peers.local.role = Role.FOLLOWER + peers.local.swarm = "" + + n_eligible_peers = len(peers.get_established(include_local=True)) + n_all_peers = len(peers.remotes) + 1 # + self + + if not (n_eligible_peers >= (51 / 100) * n_all_peers): + logger.info("Cannot elect leader node, not enough peers") + _destroy() + return + + leader, started = min( + ( + (peer, peer_data.started) + for peer, peer_data in peers.remotes.items() + if peer_data._fully_established + ), + key=lambda x: x[1], + default=(None, float("inf")), + ) + + if peers.local.started < started: + if peers.local.leader != peers.local.name: + logger.info("This node has been elected as the leader.") + peers.local.leader = peers.local.name + peers.local.role = Role.LEADER + + else: + if peers.remotes[leader].leader == "?CONFUSED": + _destroy() + logger.info( + f"""Potential leader node '{leader}' is still in the +election process or confused; waiting.""" + ) + return + + if peers.remotes[leader].leader != leader: + _destroy() + logger.warning( + f"Potential leader node '{leader}' reports a different leader; waiting" + ) + return + + if peers.local.leader != leader: + peers.local.leader = leader + peers.local.role = Role.FOLLOWER + logger.info(f"Elected node '{leader}' as the leader") + + if peers.local.leader: + peers.local.swarm = ";".join(peers.get_established(include_local=True)) + peers.local.swarm_complete = n_eligible_peers == n_all_peers + + logger.debug(f"Cluster size {n_eligible_peers}/{n_all_peers}") diff --git a/components/cluster/monitor.py b/components/cluster/monitor.py new file mode 100644 index 0000000..e0544a3 --- /dev/null +++ b/components/cluster/monitor.py @@ -0,0 +1,196 @@ +import asyncio +from contextlib import suppress + +from config import defaults +from components.logs import logger +from components.utils.datetimes import ntime_utc_now +from components.cluster.leader import elect_leader +from components.cluster.exceptions import ZombiePeer + + +class Monitor: + def __init__(self, cluster_instance: "Cluster"): + self.cluster_instance = cluster_instance + self.tasks = set() + + async def _ticket_worker(self): + """ + This asynchronous method checks for locks on tables and releases them if existent. It also deletes the tickets from + the cluster instance, which have exceeded the timeout threshold. It performs these checks every 10 seconds until + a shutdown is set. + + There are no input parameters for this function and it does not return anything. It gives an output through logs or errors. + """ + while not self.cluster_instance._shutdown.is_set(): + for table in self.cluster_instance.locks: + ticket = self.cluster_instance.locks[table]["ticket"] + table_locked = self.cluster_instance.locks[table]["lock"].locked() + + if (table_locked and not ticket) or ( + ntime_utc_now() - float(ticket or "inf") + ) > 20.0: + with suppress(RuntimeError): + self.cluster_instance.locks[table]["lock"].release() + self.cluster_instance.locks[table]["ticket"] = None + logger.error( + f"Force release of table '{table}': " + + f"Ticket: {ticket} / Lock status: {table_locked}" + ) + + for t in self.cluster_instance.tickets.copy(): + if ntime_utc_now() - float(t) > ( + defaults.CLUSTER_PEERS_TIMEOUT * len(defaults.CLUSTER_PEERS) + 10 + ): + with suppress(KeyError): + del self.cluster_instance.tickets[t] + + await asyncio.sleep(10) + + async def _cleanup_peer_connection(self, peer): + """ + Clean up a peer connection. Warning logs are created for the peer removal. The cluster instance of peers is reset + and if the shutdown is not set, leadership is elected and commands are sent to non-local peers. + + Args: + peer: the peer for whom the connection needs to be cleaned up. + """ + logger.warning(f"Removing {peer}") + self.cluster_instance.peers.remotes[peer] = self.cluster_instance.peers.remotes[ + peer + ].reset() + if not self.cluster_instance._shutdown.is_set(): + async with self.cluster_instance.receiving: + elect_leader(self.cluster_instance.peers) + if self.cluster_instance.peers.local.swarm != "": + for p in self.cluster_instance.peers.local.swarm.split(";"): + if p != self.cluster_instance.peers.local.name: + try: + ( + ticket, + receivers, + ) = await self.cluster_instance.send_command( + "STATUS", + p, + ) + except ConnectionResetError: + pass + + async def _peer_worker(self, name): + """ + The method works with the name as an input and evaluates the stream for the same. It also handles leader election + and various exception handling regarding connection reset and timeouts. + + Args: + name: the name for whom the peer works. + """ + ireader, iwriter = self.cluster_instance.peers.remotes[name].streams._in + timeout_c = 0 + c = -1 + + logger.info(f"Evaluating stream for {name}") + while not name in self.cluster_instance.peers.get_established(): + await asyncio.sleep(0.125) + + oreader, owriter = self.cluster_instance.peers.remotes[name].streams.out + + elect_leader(self.cluster_instance.peers) + + while True and timeout_c < 3: + try: + assert not all( + [ + oreader.at_eof(), + ireader.at_eof(), + iwriter.is_closing(), + owriter.is_closing(), + ] + ) + + async with asyncio.timeout(defaults.CLUSTER_PEERS_TIMEOUT * 3): + iwriter.write(b"\x11") + await iwriter.drain() + res = await oreader.readexactly(1) + assert res == b"\x11" + + timeout_c = 0 + c += 0.25 + await asyncio.sleep(0.25) + + if not c % 5: + if ( + not self.cluster_instance.peers.local.leader + or not self.cluster_instance.peers.local.swarm_complete + ): + try: + ( + ticket, + receivers, + ) = await self.cluster_instance.send_command( + "STATUS", + "*" + if self.cluster_instance.peers.local.leader + and not self.cluster_instance.peers.local.swarm_complete + else name, + ) + except ConnectionResetError: + break + + async with self.cluster_instance.receiving: + elect_leader(self.cluster_instance.peers) + await self.cluster_instance.await_receivers( + ticket, receivers, raise_err=False, timeout=3 + ) + c = 0 + + except TimeoutError: + timeout_c += 1 + continue + except ( + AssertionError, + ConnectionResetError, + asyncio.exceptions.IncompleteReadError, + ): + logger.error(f"Peer {name} failed") + break + + if c != -1: + try: + iwriter.close() + async with asyncio.timeout(0.1): + await iwriter.wait_closed() + except (ConnectionResetError, TimeoutError): + pass + + try: + owriter.close() + async with asyncio.timeout(0.1): + await owriter.wait_closed() + await owriter.wait_closed() + except (ConnectionResetError, TimeoutError): + pass + + def _on_task_done(self, task: asyncio.Task): + """ + Called when an asyncio Task is done. A cleanup peer connection task is created and the original task is removed + from the set of tasks. + + Args: + task: The task that was completed. + """ + asyncio.create_task(self._cleanup_peer_connection(task.get_name())) + self.tasks.discard(task) + + async def start_peer_monitoring(self, name): + """ + Start monitoring a peer. If the name of the peer is already in the list of tasks, it raises a ZombiePeer exception + and otherwise, the peer worker is created as a task with the peer’s name. + + Args: + name: the name of the peer to monitor. + """ + if name in [task.get_name() for task in self.tasks]: + raise ZombiePeer(name) + + t = asyncio.create_task(self._peer_worker(name), name=name) + self.tasks.add(t) + t.add_done_callback(self._on_task_done) diff --git a/components/cluster/peers.py b/components/cluster/peers.py new file mode 100644 index 0000000..e0da036 --- /dev/null +++ b/components/cluster/peers.py @@ -0,0 +1,71 @@ +from components.logs import logger +from components.models.cluster import LocalPeer, RemotePeer +from components.utils import ensure_list + + +class Peers: + def __init__(self, peers): + self.remotes = dict() + + for peer in peers: + if not peer.get("is_self", False): + _peer = RemotePeer(**peer) + self.remotes[_peer.name] = _peer + + self.local = LocalPeer(**next(peer for peer in peers if peer.get("is_self"))) + self._peers = peers + + def get_offline_peers(self): + return [p for p in self.remotes if p not in self.get_established()] + + def get_established(self, names_only: bool = True, include_local: bool = False): + peers = [] + for peer, peer_data in self.remotes.items(): + if peer_data._fully_established == True: + if names_only: + peers.append(peer_data.name) + else: + peers.append(peer_data) + + if include_local: + if names_only: + peers.append(self.local.name) + else: + peers.append(self.local) + + if names_only: + return sorted(peers) + return sorted(peers, key=lambda peer: peer.name) + + def get_remote_peer_ip( + self, name: str, ip_version: str | list = ["ip4", "ip6", "nat_ip4"] + ): + def _select_best_ip(peer): + return ( + str(getattr(peer, "ip4")) + or str(getattr(peer, "ip6")) + or str(getattr(peer, "nat_ip4")) + ) + + def _get_ips(peer, ip_version): + return [ + str(ip) for key in ensure_list(ip_version) if (ip := getattr(peer, key)) + ] + + peer = self.remotes.get(name) + if peer: + if ip_version == "best": + return _select_best_ip(peer) + return _get_ips(peer, ip_version) + + def remote_ips(self): + for name in self.remotes: + for ip in self.get_remote_peer_ip( + name=name, ip_version=["ip4", "ip6", "nat_ip4"] + ): + yield ip + + def get_remote_peer_name(self, ip): + for peer_data in self.remotes.values(): + if ip in peer_data._all_ips_as_str: + return peer_data.name diff --git a/components/cluster/ssl.py b/components/cluster/ssl.py new file mode 100644 index 0000000..5a55486 --- /dev/null +++ b/components/cluster/ssl.py @@ -0,0 +1,21 @@ +import ssl +from components.logs import logger +from config import defaults + + +def get_ssl_context(type_value: str): + if type_value == "client": + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + elif type_value == "server": + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + else: + raise Exception("Unknown type_value") + + context.load_cert_chain( + certfile=defaults.TLS_CERTFILE, keyfile=defaults.TLS_KEYFILE + ) + context.load_verify_locations(cafile=defaults.TLS_CA) + context.check_hostname = False + context.verify_mode = ssl.VerifyMode.CERT_REQUIRED + context.minimum_version = ssl.TLSVersion.TLSv1_3 + return context diff --git a/components/logs/__init__.py b/components/logs/__init__.py new file mode 100644 index 0000000..04a61a3 --- /dev/null +++ b/components/logs/__init__.py @@ -0,0 +1,18 @@ +from components.logs import log +from config.defaults import ( + LOG_LEVEL, + LOG_FILE_ROTATION, + LOG_FILE_RETENTION, + CLUSTER_PEERS, +) + +logger = log.Logger() +logger.add( + f"logs/application.log", + level=LOG_LEVEL, + colorize=False, + max_size_mb=LOG_FILE_ROTATION, + retention=LOG_FILE_RETENTION, + text=lambda _: next(p["name"] for p in CLUSTER_PEERS if p.get("is_self")), + serialize=True, +) diff --git a/components/logs/log.py b/components/logs/log.py new file mode 100644 index 0000000..679696c --- /dev/null +++ b/components/logs/log.py @@ -0,0 +1,124 @@ +import logging +import json +import traceback +from logging.handlers import RotatingFileHandler +from components.utils.datetimes import datetime, timedelta + +SUCCESS_LEVEL = 25 +CRITICAL_LEVEL = 50 +logging.addLevelName(SUCCESS_LEVEL, "SUCCESS") +logging.addLevelName(CRITICAL_LEVEL, "CRITICAL") + +LOG_COLORS = { + "DEBUG": "\033[94m", + "INFO": "\033[96m", + "SUCCESS": "\033[92m", + "WARNING": "\033[93m", + "ERROR": "\033[91m", + "CRITICAL": "\033[95m", + "RESET": "\033[0m", + "BOLD": "\033[1m", +} + + +class JSONFormatter(logging.Formatter): + def __init__(self, text): + super().__init__() + self.text = text + + def format(self, record): + exc_text = None + if record.exc_info: + exc_text = traceback.format_exc() + + log_entry = { + "text": self.text, + "record": { + "elapsed": { + "repr": str(timedelta(seconds=record.relativeCreated / 1000)), + "seconds": record.relativeCreated / 1000, + }, + "exception": exc_text, + "extra": record.__dict__.get("extra", {}), + "file": {"name": record.filename, "path": record.pathname}, + "function": record.funcName, + "level": { + "icon": "✅" + if record.levelno == SUCCESS_LEVEL + else "ℹ️" + if record.levelno == logging.INFO + else "⚠️", + "name": record.levelname, + "no": record.levelno, + }, + "line": record.lineno, + "message": record.getMessage(), + "module": record.module, + "name": record.name, + "process": {"id": record.process, "name": record.processName}, + "thread": {"id": record.thread, "name": record.threadName}, + "time": { + "repr": datetime.utcfromtimestamp(record.created).isoformat() + + "+00:00", + "timestamp": record.created, + }, + }, + } + return json.dumps(log_entry) + + +class PlainTextFormatter(logging.Formatter): + def format(self, record): + log_time = datetime.utcfromtimestamp(record.created).strftime( + "%Y-%m-%d %H:%M:%S.%f" + )[:-3] + level_color = LOG_COLORS.get(record.levelname, LOG_COLORS["RESET"]) + message_bold = f"{LOG_COLORS['BOLD']}{record.getMessage()}{LOG_COLORS['RESET']}" + return f"{log_time} | {LOG_COLORS['BOLD']}{level_color}{record.levelname:<8}{LOG_COLORS['RESET']} | {record.funcName}:{record.lineno} - {message_bold}" + + +class Logger: + def __init__(self): + self.logger = logging.getLogger("custom_logger") + self.logger.setLevel(logging.DEBUG) + stdout_handler = logging.StreamHandler() + stdout_handler.setLevel(logging.DEBUG) + stdout_handler.setFormatter(PlainTextFormatter()) + self.logger.addHandler(stdout_handler) + + def add(self, filepath, level, colorize, max_size_mb, retention, text, serialize): + for handler in self.logger.handlers[:]: + if isinstance(handler, RotatingFileHandler): + self.logger.removeHandler(handler) + handler.close() + + handler = RotatingFileHandler( + filepath, + maxBytes=max_size_mb * 1024 * 1024, + backupCount=retention, + encoding="utf-8", + ) + handler.setLevel(level) + handler.setFormatter(JSONFormatter(text(None))) + self.logger.addHandler(handler) + + def log(self, level, message): + self.logger.log(level, message, stacklevel=2) + + def info(self, message): + self.logger.info(message, stacklevel=2) + + def warning(self, message): + self.logger.warning(message, stacklevel=2) + + def error(self, message): + self.logger.error(message, stacklevel=2) + + def debug(self, message): + self.logger.debug(message, stacklevel=2) + + def success(self, message): + self.logger.log(SUCCESS_LEVEL, message, stacklevel=2) + + def critical(self, message): + self.logger.log(CRITICAL_LEVEL, message, exc_info=True, stacklevel=2) diff --git a/components/models/__init__.py b/components/models/__init__.py new file mode 100644 index 0000000..a4577ac --- /dev/null +++ b/components/models/__init__.py @@ -0,0 +1,26 @@ +import email_validator +from pydantic import ( + AfterValidator, + BaseModel, + BeforeValidator, + computed_field, + ConfigDict, + conint, + constr, + Field, + field_serializer, + field_validator, + FilePath, + model_validator, + TypeAdapter, + validate_call, + ValidationError, +) +from pydantic.networks import IPvAnyAddress +from pydantic_core import PydanticCustomError +from typing import List, Dict, Any, Literal, Annotated, Any +from uuid import UUID, uuid4 +from enum import Enum + +email_validator.TEST_ENVIRONMENT = True +validate_email = email_validator.validate_email diff --git a/components/models/cluster.py b/components/models/cluster.py new file mode 100644 index 0000000..ab8a33c --- /dev/null +++ b/components/models/cluster.py @@ -0,0 +1,193 @@ +import asyncio +import socket + +from components.cluster.ssl import get_ssl_context +from components.models import * +from components.utils.datetimes import ntime_utc_now +from config import defaults +from contextlib import closing + + +class Role(Enum): + LEADER = 1 + FOLLOWER = 2 + + +class ConnectionStatus(Enum): + CONNECTED = 0 + REFUSED = 1 + SOCKET_REFUSED = 2 + ALL_AVAILABLE_FAILED = 3 + OK = 4 + OK_WITH_PREVIOUS_ERRORS = 5 + + +class CritErrors(Enum): + NOT_READY = "CRIT:NOT_READY" + NO_SUCH_TABLE = "CRIT:NO_SUCH_TABLE" + TABLE_HASH_MISMATCH = "CRIT:TABLE_HASH_MISMATCH" + CANNOT_APPLY = "CRIT:CANNOT_APPLY" + NOTHING_TO_COMMIT = "CRIT:NOTHING_TO_COMMIT" + INVALID_FILE_PATH = "CRIT:INVALID_FILE_PATH" + START_BEHIND_FILE_END = "CRIT:START_BEHIND_FILE_END" + PEERS_MISMATCH = "CRIT:PEERS_MISMATCH" + DOC_MISMATCH = "CRIT:DOC_MISMATCH" + ZOMBIE = "CRIT:ZOMBIE" + + @property + def response(self): + return f"ACK {self.value}" + + +class LocalPeer(BaseModel): + @model_validator(mode="before") + @classmethod + def pre_init(cls, data: Any) -> Any: + if not data["ip4"] and not data["ip6"]: + raise ValueError("Neither a IPv4 nor a IPv6 address was provided") + return data + + @field_validator("is_self") + def local_self_validator(cls, v): + if v != True: + raise ValueError("LocalPeer does not have is_self flag") + return v + + is_self: bool + name: constr(pattern=r"^[a-zA-Z0-9\-_\.]+$", min_length=3) + ip4: IPvAnyAddress | None = None + ip6: IPvAnyAddress | None = None + cli_bindings: list[IPvAnyAddress] = defaults.CLUSTER_CLI_BINDINGS + leader: str | None = None + role: Role = Role.FOLLOWER + swarm: str = "" + started: float = ntime_utc_now() + swarm_complete: bool = False + + @computed_field + @property + def _bindings_as_str(self) -> str: + return [str(ip) for key in ("ip4", "ip6") if (ip := getattr(self, key))] + + @computed_field + @property + def _all_bindings_as_str(self) -> str: + return [ + str(ip) for key in ("ip4", "ip6") if (ip := getattr(self, key)) + ] + self.cli_bindings + + @model_validator(mode="after") + def cli_bindings_validator(self): + for ip in self.cli_bindings: + if ip == self.ip4 or ip == self.ip6: + raise ValueError("CLI bindings overlap local bindings") + return self + + +class Streams(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + out: tuple[asyncio.StreamReader, asyncio.StreamWriter] | None = None + _in: tuple[asyncio.StreamReader, asyncio.StreamWriter] | None = None + + +class RemotePeer(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + @model_validator(mode="before") + @classmethod + def pre_init(cls, data: Any) -> Any: + if not data["ip4"] and not data["ip6"]: + raise ValueError("Neither a IPv4 nor a IPv6 address was provided") + return data + + @field_validator("is_self") + def local_self_validator(cls, v): + if v: + raise ValueError("RemotePeer has is_self flag") + return v + + is_self: bool = False + sending_lock: asyncio.Lock = asyncio.Lock() + swarm: str = "" + leader: str | None = None + started: float | None = None + name: constr(pattern=r"^[a-zA-Z0-9\-_\.]+$", min_length=3) + ip4: IPvAnyAddress | None = None + ip6: IPvAnyAddress | None = None + nat_ip4: IPvAnyAddress | None = None + streams: Streams = Streams() + port: int = 2102 + + def reset(self): + self.streams._in = None + self.streams.out = None + self.leader = None + self.started = None + self.swarm = "" + return self + + def _eval_ip( + self, + ) -> tuple[IPvAnyAddress | None, tuple[ConnectionStatus, dict]]: + errors = dict() + peer_ips = [ip for ip in [self.ip4, self.ip6] if ip is not None] + for ip in peer_ips: + with closing( + socket.socket( + socket.AF_INET if ip.version == 4 else socket.AF_INET6, + socket.SOCK_STREAM, + ) + ) as sock: + sock.settimeout(defaults.CLUSTER_PEERS_TIMEOUT) + connection_return = sock.connect_ex((str(ip), self.port)) + if connection_return != 0: + errors[ip] = ( + ConnectionStatus.SOCKET_REFUSED, + socket.errno.errorcode.get(connection_return), + ) + else: + if errors: + return ip, (ConnectionStatus.OK_WITH_PREVIOUS_ERRORS, errors) + return ip, (ConnectionStatus.OK, errors) + else: + return None, (ConnectionStatus.ALL_AVAILABLE_FAILED, errors) + + async def connect( + self, + ip: IPvAnyAddress | None = None, + ) -> tuple[ + tuple[asyncio.StreamReader, asyncio.StreamWriter] | None, + tuple[ConnectionStatus, Any], + ]: + if not self.streams.out: + if not ip: + ip, status = self._eval_ip() + if not ip: + return None, status + try: + self.streams.out = await asyncio.open_connection( + str(ip), self.port, ssl=get_ssl_context("client") + ) + except ConnectionRefusedError as e: + return None, (ConnectionStatus.REFUSED, e) + + return self.streams.out, (ConnectionStatus.CONNECTED, None) + + @computed_field + @property + def _all_ips_as_str(self) -> str: + return [str(ip) for key in ("ip4", "ip6") if (ip := getattr(self, key))] + + @computed_field + @property + def _fully_established(self) -> str: + return ( + True + if self.streams.out + and self.streams._in + and self.swarm + and self.started + and self.leader + else False + ) diff --git a/components/models/objects.py b/components/models/objects.py new file mode 100644 index 0000000..a84cab0 --- /dev/null +++ b/components/models/objects.py @@ -0,0 +1,628 @@ +from components.utils import ensure_list, to_unique_sorted_str_list +from components.utils.datetimes import utc_now_as_str +from components.utils.cryptography import generate_rsa_dkim +from components.models import * + +POLICIES = [ + ("-- None --", "disallow_all"), + ("Send to external", "send_external"), + ("Receive from external", "receive_external"), + ("Send to internal", "send_internal"), + ("Receive from internal", "receive_internal"), +] + +POLICY_DESC = [p[1] for p in POLICIES] + + +def ascii_email(v): + try: + name = validate_email(v).ascii_email + except: + raise PydanticCustomError( + "name_invalid", + "The provided name is not a valid local part", + dict(name_invalid=v), + ) + return name + + +def ascii_domain(v): + try: + name = validate_email(f"name@{v}").ascii_domain + except: + raise PydanticCustomError( + "name_invalid", + "The provided name is not a valid domain name", + dict(name_invalid=v), + ) + return name + + +def ascii_local_part(v): + try: + name = validate_email(f"{v}@example.org").ascii_local_part + except: + raise PydanticCustomError( + "name_invalid", + "The provided name is not a valid local part", + dict(name_invalid=v), + ) + return name + + +class ObjectDomain(BaseModel): + def model_post_init(self, __context): + if ( + self.dkim_selector == self.arc_selector + and self.assigned_dkim_keypair != self.assigned_arc_keypair + ): + raise PydanticCustomError( + "selector_conflict", + "ARC and DKIM selectors cannot be the same while using different keys", + dict(), + ) + + @field_validator("bcc_inbound") + def bcc_inbound_validator(cls, v): + if v in [None, ""]: + return "" + return ascii_email(v) + + @field_validator("bcc_outbound") + def bcc_outbound_validator(cls, v): + if v in [None, ""]: + return "" + return ascii_email(v) + + @field_validator("domain") + def domain_validator(cls, v): + return ascii_domain(v) + + display_name: str = Field( + default="", + json_schema_extra={ + "title": "Display name", + "description": "The display name of the mailbox", + "type": "text", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"mailbox-{str(uuid4())}", + }, + ) + + domain: str = Field( + json_schema_extra={ + "title": "Domain name", + "description": "A unique domain name", + "type": "text", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"domain-{str(uuid4())}", + }, + ) + + bcc_inbound: str = Field( + default="", + json_schema_extra={ + "title": "BCC inbound", + "description": "BCC destination for incoming messages", + "type": "email", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"bcc-inbound-{str(uuid4())}", + }, + ) + + bcc_outbound: str = Field( + default="", + json_schema_extra={ + "title": "BCC outbound", + "description": "BCC destination for outbound messages", + "type": "email", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"bcc-outbound-{str(uuid4())}", + }, + ) + + n_mailboxes: conint(ge=0) | Literal[""] = Field( + default="", + json_schema_extra={ + "title": "Max. mailboxes", + "description": "Limit domain to n mailboxes", + "type": "number", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"n-mailboxes-outbound-{str(uuid4())}", + }, + ) + + ratelimit: conint(ge=0) | Literal[""] = Field( + default="", + json_schema_extra={ + "title": "Ratelimit", + "description": "Amount of elements to allow in a given time unit (see below)", + "type": "number", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"ratelimit-{str(uuid4())}", + }, + ) + + ratelimit_unit: Literal["day", "hour", "minute"] = Field( + default="hour", + json_schema_extra={ + "title": "Ratelimit unit", + "description": "Policy override options.", + "type": "select", + "options": [ + {"name": "Day", "value": "day"}, + {"name": "Hour", "value": "hour"}, + {"name": "Minute", "value": "minute"}, + ], + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"ratelimit-unit-{str(uuid4())}", + }, + ) + + dkim: Annotated[ + Literal[True, False], + BeforeValidator(lambda x: True if str(x).lower() == "true" else False), + AfterValidator(lambda x: True if str(x).lower() == "true" else False), + ] = Field( + default=True, + json_schema_extra={ + "title": "DKIM", + "description": "Enable DKIM signatures", + "type": "radio", + "input_extra": 'autocomplete="off"', + "form_id": f"dkim-signatures-{str(uuid4())}", + }, + ) + + arc: Annotated[ + Literal[True, False], + BeforeValidator(lambda x: True if str(x).lower() == "true" else False), + AfterValidator(lambda x: True if str(x).lower() == "true" else False), + ] = Field( + default=True, + json_schema_extra={ + "title": "ARC", + "description": "Enable ARC signatures", + "type": "radio", + "input_extra": 'autocomplete="off"', + "form_id": f"arc-signatures-{str(uuid4())}", + }, + ) + + dkim_selector: constr(strip_whitespace=True, min_length=1) = Field( + default="mail", + json_schema_extra={ + "title": "DKIM Selector", + "description": "Selector name", + "type": "text", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"dkim-selector-{str(uuid4())}", + }, + ) + + arc_selector: constr(strip_whitespace=True, min_length=1) = Field( + default="mail", + json_schema_extra={ + "title": "ARC Selector", + "description": "Selector name", + "type": "text", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"arc-selector-{str(uuid4())}", + }, + ) + + assigned_dkim_keypair: BaseModel | str = Field( + default="", + json_schema_extra={ + "title": "DKIM key pair", + "description": "Assign a key pair for DKIM signatures.", + "type": "keypair", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"assigned-dkim-keypair-{str(uuid4())}", + }, + ) + + assigned_arc_keypair: BaseModel | str = Field( + default="", + json_schema_extra={ + "title": "ARC key pair", + "description": "Assign a key pair for ARC signatures.", + "type": "keypair", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"assigned-arc-keypair-{str(uuid4())}", + }, + ) + + policies: Annotated[ + Literal[*POLICY_DESC] | list[Literal[*POLICY_DESC]], + AfterValidator(lambda x: to_unique_sorted_str_list(ensure_list(x))), + ] = Field( + default=["disallow_all"], + json_schema_extra={ + "title": "Policies", + "description": "Policies for this domain.", + "type": "select:multi", + "options": [{"name": p[0], "value": p[1]} for p in POLICIES], + "input_extra": '_="on change if event.target.value is \'disallow_all\' set my selectedIndex to 0 end" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"policies-{str(uuid4())}", + }, + ) + + policy_weight: str = Field( + default="domain_mailbox", + json_schema_extra={ + "title": "Policy weight", + "description": "Policy override options.", + "type": "select", + "options": [ + {"name": "Domain > Mailbox", "value": "domain_mailbox"}, + {"name": "Mailbox > Domain", "value": "mailbox_domain"}, + ], + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"policy-weight-{str(uuid4())}", + }, + ) + + assigned_users: Annotated[ + str | list, + AfterValidator(lambda x: to_unique_sorted_str_list(ensure_list(x))), + ] = Field( + json_schema_extra={ + "title": "Assigned users", + "description": "Assign this object to users.", + "type": "users:multi", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"assigned-users-{str(uuid4())}", + }, + ) + + +class ObjectAddress(BaseModel): + local_part: constr(strip_whitespace=True, min_length=1) = Field( + json_schema_extra={ + "title": "Local part", + "description": "A local part as in @example.org; must be unique in combination with its assigned domain", + "type": "text", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"local-part-{str(uuid4())}", + }, + ) + + assigned_domain: BaseModel | str = Field( + json_schema_extra={ + "title": "Assigned domain", + "description": "Assign a domain for this address.", + "type": "domain", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"domain-{str(uuid4())}", + }, + ) + + assigned_emailusers: Annotated[ + str | BaseModel | None | list[BaseModel | str | None], + AfterValidator(lambda x: to_unique_sorted_str_list(ensure_list(x))), + ] = Field( + default=[], + json_schema_extra={ + "title": "Assigned email users", + "description": "Assign this mailbox to email users.", + "type": "emailusers:multi", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"assigned-emailusers-{str(uuid4())}", + }, + ) + + display_name: str = Field( + default="%DOMAIN_DISPLAY_NAME%", + json_schema_extra={ + "title": "Display name", + "description": "You can use %DOMAIN_DISPLAY_NAME% as variable", + "type": "text", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"mailbox-{str(uuid4())}", + }, + ) + + policies: Annotated[ + Literal[*POLICY_DESC] | list[Literal[*POLICY_DESC]], + AfterValidator(lambda x: to_unique_sorted_str_list(ensure_list(x))), + ] = Field( + default=["disallow_all"], + json_schema_extra={ + "title": "Policies", + "description": "Policies for this address.", + "type": "select:multi", + "options": [{"name": p[0], "value": p[1]} for p in POLICIES], + "input_extra": '_="on change if event.target.value is \'disallow_all\' set my selectedIndex to 0 end" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"policies-{str(uuid4())}", + }, + ) + + assigned_users: Annotated[ + str | list, + AfterValidator(lambda x: to_unique_sorted_str_list(ensure_list(x))), + ] = Field( + json_schema_extra={ + "title": "Assigned users", + "description": "Assign this object to users.", + "type": "users:multi", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"assigned-users-{str(uuid4())}", + }, + ) + + +class ObjectUser(BaseModel): + username: constr(strip_whitespace=True, min_length=1) = Field( + json_schema_extra={ + "title": "Username", + "description": "A unique username", + "type": "text", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"login-{str(uuid4())}", + }, + ) + + display_name: str = Field( + default="%MAILBOX_DISPLAY_NAME%", + json_schema_extra={ + "title": "Display name", + "description": "You can use %MAILBOX_DISPLAY_NAME% and %DOMAIN_DISPLAY_NAME% variables", + "type": "text", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"mailbox-{str(uuid4())}", + }, + ) + + policies: Annotated[ + Literal[*POLICY_DESC] | list[Literal[*POLICY_DESC]], + AfterValidator(lambda x: to_unique_sorted_str_list(ensure_list(x))), + ] = Field( + default=["disallow_all"], + json_schema_extra={ + "title": "Policies", + "description": "Policies for this address.", + "type": "select:multi", + "options": [{"name": p[0], "value": p[1]} for p in POLICIES], + "input_extra": '_="on change if event.target.value is \'disallow_all\' set my selectedIndex to 0 end" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"policies-{str(uuid4())}", + }, + ) + + assigned_users: Annotated[ + str | list, + AfterValidator(lambda x: to_unique_sorted_str_list(ensure_list(x))), + ] = Field( + json_schema_extra={ + "title": "Assigned users", + "description": "Assign this object to users.", + "type": "users:multi", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"assigned-users-{str(uuid4())}", + }, + ) + + +class ObjectKeyPair(BaseModel): + private_key_pem: str + public_key_base64: str + key_size: int + key_name: constr(strip_whitespace=True, min_length=1) = Field( + default="KeyPair", + json_schema_extra={ + "title": "Name", + "description": "A human readable name", + "type": "text", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"keyname-{str(uuid4())}", + }, + ) + assigned_users: Annotated[ + str | list, + AfterValidator(lambda x: to_unique_sorted_str_list(ensure_list(x))), + ] = Field( + json_schema_extra={ + "title": "Assigned users", + "description": "Assign this object to users.", + "type": "users:multi", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"assigned-users-{str(uuid4())}", + }, + ) + + @computed_field + @property + def dns_formatted(self) -> str: + return ( + "v=DKIM1; p=" + self.public_key_base64 if self.public_key_base64 else None + ) + + +class ObjectBase(BaseModel): + id: Annotated[str, AfterValidator(lambda v: str(UUID(v)))] + created: str + updated: str + + +class ObjectBaseDomain(ObjectBase): + details: ObjectDomain + + @computed_field + @property + def name(self) -> str: + return self.details.domain + + +class ObjectBaseUser(ObjectBase): + details: ObjectUser + + @computed_field + @property + def name(self) -> str: + return self.details.username + + +class ObjectBaseAddress(ObjectBase): + details: ObjectAddress + + @computed_field + @property + def name(self) -> str: + return self.details.local_part + + +class ObjectBaseKeyPair(ObjectBase): + details: ObjectKeyPair + + @computed_field + @property + def name(self) -> str: + return self.details.key_name + + +class ObjectAdd(BaseModel): + @computed_field + @property + def id(self) -> str: + return str(uuid4()) + + @computed_field + @property + def created(self) -> str: + return utc_now_as_str() + + @computed_field + @property + def updated(self) -> str: + return utc_now_as_str() + + +class ObjectAddDomain(ObjectAdd): + details: ObjectDomain + + +class ObjectAddAddress(ObjectAdd): + details: ObjectAddress + + +class ObjectAddUser(ObjectAdd): + details: ObjectUser + + +class ObjectAddKeyPair(ObjectAdd): + def model_post_init(self, __context): + print(self) + print(__context) + + @model_validator(mode="before") + @classmethod + def pre_init(cls, data: Any) -> Any: + if not all( + data["details"].get(k) + for k in ObjectKeyPair.__fields__ + if k != "assigned_users" + ): + data["details"] = cls.generate_rsa( + 2048, + data["details"].get("assigned_users", []), + data["details"].get("key_name", "KeyPair"), + ) + return data + + @classmethod + def generate_rsa( + cls, key_size: int = 2048, assigned_users: list = [], key_name: str = "KeyPair" + ) -> "ObjectKeyPair": + priv, pub = generate_rsa_dkim(key_size) + return ObjectKeyPair( + private_key_pem=priv, + public_key_base64=pub, + key_size=key_size, + assigned_users=assigned_users, + key_name=key_name, + ).dict() + + details: ObjectKeyPair + + +class ObjectPatch(BaseModel): + model_config = ConfigDict(validate_assignment=True) + + @computed_field + @property + def updated(self) -> str: + return utc_now_as_str() + + +class ObjectPatchDomain(ObjectPatch): + details: ObjectDomain + + +class ObjectPatchUser(ObjectPatch): + details: ObjectUser + + +class ObjectPatchAddress(ObjectPatch): + details: ObjectAddress + + +class _KeyPairHelper(ObjectPatch): + key_name: constr(strip_whitespace=True, min_length=1) = Field( + default="KeyPair", + ) + assigned_users: Annotated[ + str | list, + AfterValidator(lambda x: to_unique_sorted_str_list(ensure_list(x))), + ] + + +class ObjectPatchKeyPair(ObjectPatch): + details: _KeyPairHelper + + +model_classes = { + "types": ["domains", "addresses", "emailusers", "keypairs"], + "forms": { + "domains": ObjectDomain, + "addresses": ObjectAddress, + "emailusers": ObjectUser, + "keypairs": ObjectKeyPair, + }, + "patch": { + "domains": ObjectPatchDomain, + "addresses": ObjectPatchAddress, + "emailusers": ObjectPatchUser, + "keypairs": ObjectPatchKeyPair, + }, + "add": { + "domains": ObjectAddDomain, + "addresses": ObjectAddAddress, + "emailusers": ObjectAddUser, + "keypairs": ObjectAddKeyPair, + }, + "base": { + "domains": ObjectBaseDomain, + "addresses": ObjectBaseAddress, + "emailusers": ObjectBaseUser, + "keypairs": ObjectBaseKeyPair, + }, + "unique_fields": { + "domains": ["domain"], + "addresses": ["local_part", "assigned_domain"], + "emailusers": ["username"], + "keypairs": ["key_name"], + }, + "system_fields": { + "domains": ["assigned_users", "n_mailboxes"], + "addresses": ["assigned_users"], + "emailusers": ["assigned_users"], + "keypairs": ["assigned_users"], + }, +} + + +class ObjectIdList(BaseModel): + object_id: Annotated[ + UUID | list[UUID], + AfterValidator(lambda x: to_unique_sorted_str_list(ensure_list(x))), + ] diff --git a/components/models/system.py b/components/models/system.py new file mode 100644 index 0000000..27a346d --- /dev/null +++ b/components/models/system.py @@ -0,0 +1,91 @@ +from config import defaults +from components.utils import ensure_list +from components.utils.datetimes import utc_now_as_str +from components.models import * + + +class SystemSettings(BaseModel): + ACCEPT_LANGUAGES: Annotated[ + list[Literal["en", "de"]], + BeforeValidator(lambda x: ensure_list(x)), + ] = Field( + min_length=1, + default=defaults.ACCEPT_LANGUAGES, + json_schema_extra={ + "title": "Accepted languages", + "description": "Accepted languages by the clients browser.", + "type": "list:text", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"accept-languages-{str(uuid4())}", + }, + ) + + AUTH_REQUEST_TIMEOUT: int = Field( + default=defaults.AUTH_REQUEST_TIMEOUT, + json_schema_extra={ + "title": "Proxy timeout", + "description": "Proxy authentication timeout", + "type": "number", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"proxy-auth-timeout-{str(uuid4())}", + }, + ) + + TABLE_PAGE_SIZE: int = Field( + default=defaults.TABLE_PAGE_SIZE, + json_schema_extra={ + "title": "Table page size", + "description": "Default table page size", + "type": "number", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"table-page-size-{str(uuid4())}", + }, + ) + + LOG_FILE_RETENTION: int = Field( + default=defaults.LOG_FILE_RETENTION, + json_schema_extra={ + "title": "Log file retention", + "description": "Number of log files to keep after each rotation", + "type": "number", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"log-file-retention-{str(uuid4())}", + }, + ) + + LOG_FILE_ROTATION: int = Field( + default=defaults.LOG_FILE_ROTATION, + json_schema_extra={ + "title": "Max size in bytes", + "description": "Log files will rotate after given bytes", + "type": "number", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"log-file-rotation-{str(uuid4())}", + }, + ) + + TEMPLATES_AUTO_RELOAD: Annotated[ + Literal[True, False], + BeforeValidator(lambda x: True if str(x).lower() == "true" else False), + AfterValidator(lambda x: True if str(x).lower() == "true" else False), + ] = Field( + default=defaults.TEMPLATES_AUTO_RELOAD, + json_schema_extra={ + "title": "Reload templates", + "description": "Automatically reload templates on change", + "type": "radio", + "input_extra": 'autocomplete="off"', + "form_id": f"templats-auto-reload-{str(uuid4())}", + }, + ) + + +class SystemSettingsBase(BaseModel): + details: SystemSettings = SystemSettings() + + +class UpdateSystemSettings(SystemSettingsBase): + @computed_field + @property + def updated(self) -> str: + return utc_now_as_str() diff --git a/components/models/tables.py b/components/models/tables.py new file mode 100644 index 0000000..746ac73 --- /dev/null +++ b/components/models/tables.py @@ -0,0 +1,49 @@ +from config import defaults +from components.utils import ensure_list, to_unique_sorted_str_list +from components.models import * + + +class TableSearch(BaseModel): + q: Annotated[Any, AfterValidator(lambda x: str(x))] = "" + page: Annotated[Any, AfterValidator(lambda x: int(x) if x else 1)] = 1 + page_size: Annotated[ + Any, + AfterValidator(lambda x: int(x) if x else defaults.TABLE_PAGE_SIZE), + ] = defaults.TABLE_PAGE_SIZE + sorting: tuple = ("created", True) + filters: Annotated[ + Any, + AfterValidator(lambda x: to_unique_sorted_str_list(ensure_list(x))), + ] = {} + + @field_validator("filters", mode="after") + def filters_formatter(cls, v): + filters = dict() + for f in v: + key_name, key_value = f.split(":") + if key_name == "assigned_users": + continue + if key_name not in filters.keys(): + filters[key_name] = key_value + else: + if isinstance(filters[key_name], list): + filters[key_name].append(key_value) + else: + filters[key_name] = [filters[key_name], key_value] + + return filters + + @field_validator("sorting", mode="before") + def split_sorting(cls, v): + if isinstance(v, str): + match v.split(":"): + case [ + sort_attr, + direction, + ]: + sort_reverse = True if direction == "desc" else False + case _: + sort_reverse = True + sort_attr = "created" + + return (sort_attr, sort_reverse) diff --git a/components/models/users.py b/components/models/users.py new file mode 100644 index 0000000..6adfc61 --- /dev/null +++ b/components/models/users.py @@ -0,0 +1,264 @@ +import random +import json +from config.defaults import USER_ACLS, ACCESS_TOKEN_FORMAT, ACCEPT_LANGUAGES +from components.utils import ensure_list, to_unique_sorted_str_list +from components.utils.datetimes import ntime_utc_now, utc_now_as_str +from webauthn.helpers.structs import AuthenticatorTransport +from components.models import * + + +class TokenConfirmation(BaseModel): + confirmation_code: Annotated[int, AfterValidator(lambda i: "%06d" % i)] + token: str = constr(strip_whitespace=True, min_length=14, max_length=14) + + +class AuthToken(BaseModel): + login: str = constr(strip_whitespace=True, min_length=1) + + @computed_field + @property + def token(self) -> str: + return "%04d-%04d-%04d" % ( + random.randint(0, 9999), + random.randint(0, 9999), + random.randint(0, 9999), + ) + + +class Credential(BaseModel): + id: Annotated[str, AfterValidator(lambda x: bytes.fromhex(x))] | bytes + public_key: Annotated[str, AfterValidator(lambda x: bytes.fromhex(x))] | bytes + friendly_name: constr(strip_whitespace=True, min_length=1) + last_login: str + sign_count: int + transports: list[AuthenticatorTransport] | None = [] + active: bool + updated: str + created: str + + @field_serializer("id", "public_key") + def serialize_bytes_to_hex(self, v: bytes, _info): + return v.hex() if isinstance(v, bytes) else v + + +class AddCredential(BaseModel): + id: Annotated[bytes, AfterValidator(lambda x: x.hex())] | str + public_key: Annotated[bytes, AfterValidator(lambda x: x.hex())] | str + sign_count: int + friendly_name: str = "New passkey" + transports: list[AuthenticatorTransport] | None = [] + active: bool = True + last_login: str = "" + + @computed_field + @property + def created(self) -> str: + return utc_now_as_str() + + @computed_field + @property + def updated(self) -> str: + return utc_now_as_str() + + +class UserProfile(BaseModel): + model_config = ConfigDict(validate_assignment=True) + + @field_validator("email", mode="before") + def email_validator(cls, v): + if v in [None, ""]: + return "" + try: + email = validate_email(v, check_deliverability=False).ascii_email + except: + raise PydanticCustomError( + "email_invalid", + "The provided email address is invalid", + dict(provided_email=v), + ) + return email + + @field_validator("access_tokens", mode="after") + def access_tokens_validator(cls, v): + for s in v: + try: + TypeAdapter(ACCESS_TOKEN_FORMAT).validate_python(s) + except ValidationError as e: + s_priv = s[:3] + (s[3:] and "***") + raise PydanticCustomError( + "access_tokens", + f"The provided token {s_priv} is invalid", + dict(), + ) + + return v + + @field_validator("tresor", mode="after") + def parse_tresor(cls, v): + try: + if v: + tresor = json.loads(v) + assert all( + k in tresor.keys() + for k in ["public_key_pem", "wrapped_private_key", "iv", "salt"] + ) + return v + except: + raise PydanticCustomError( + "tresor", + f"The provided tresor data is invalid", + dict(), + ) + + tresor: str | None = Field( + default=None, + json_schema_extra={ + "title": "Personal tresor", + "type": "tresor", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"tresor-{str(uuid4())}", + }, + ) + + email: str = Field( + default="", + json_schema_extra={ + "title": "Email address", + "description": "Your email address is optional", + "type": "email", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"email-{str(uuid4())}", + }, + ) + + access_tokens: Annotated[ + str | list, + AfterValidator(lambda x: to_unique_sorted_str_list(ensure_list(x))), + ] = Field( + default=[], + json_schema_extra={ + "title": "Access tokens", + "description": "Tokens to access the API. Save profile after removing a token.", + "type": "list:text", + "input_extra": 'autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"', + "form_id": f"access-tokens-{str(uuid4())}", + }, + ) + + permit_auth_requests: Annotated[ + Literal[True, False], + BeforeValidator(lambda x: True if str(x).lower() == "true" else False), + AfterValidator(lambda x: True if str(x).lower() == "true" else False), + ] = Field( + default=True, + json_schema_extra={ + "title": "Authentication requests", + "description": "Allow other devices to issue authentication requests to active sessions via pop-up", + "type": "radio", + "input_extra": 'autocomplete="off"', + "form_id": f"proxy-login-{str(uuid4())}", + }, + ) + + updated: str | None = None + + +class User(BaseModel): + model_config = ConfigDict(validate_assignment=True) + + id: Annotated[str, AfterValidator(lambda v: str(UUID(v)))] + login: constr(strip_whitespace=True, min_length=1) + credentials: dict[str, Credential] | Annotated[ + str | list[str], + AfterValidator(lambda v: ensure_list(v)), + ] = {} + acl: Annotated[ + Literal[*USER_ACLS] | list[Literal[*USER_ACLS]], + AfterValidator(lambda v: ensure_list(v)), + ] + groups: Annotated[ + constr(strip_whitespace=True, min_length=1) + | list[constr(strip_whitespace=True, min_length=1)], + AfterValidator(lambda v: ensure_list(v)), + ] = [] + profile: UserProfile + created: str + updated: str + + +class UserGroups(BaseModel): + name: constr(strip_whitespace=True, min_length=1) + new_name: constr(strip_whitespace=True, min_length=1) + members: Annotated[ + str | list, + AfterValidator(lambda x: to_unique_sorted_str_list(ensure_list(x))), + ] = [] + + +class UserAdd(BaseModel): + login: str = constr(strip_whitespace=True, min_length=1) + credentials: list[str] = [] + acl: list[Literal[*USER_ACLS]] = ["user"] + profile: UserProfile = UserProfile.parse_obj({}) + groups: list[constr(strip_whitespace=True, min_length=1)] = [] + + @computed_field + @property + def id(self) -> str: + return str(uuid4()) + + @computed_field + @property + def created(self) -> str: + return utc_now_as_str() + + @computed_field + @property + def updated(self) -> str: + return utc_now_as_str() + + +class UserPatch(BaseModel): + login: str | None = None + acl: str | list = [] + credentials: str | list = [] + groups: str | list | None = None + + @computed_field + @property + def updated(self) -> str: + return utc_now_as_str() + + +class UserProfilePatch(BaseModel): + email: str | None = None + tresor: str | None = None + access_tokens: str | list = [] + permit_auth_requests: bool | None = None + + @computed_field + @property + def updated(self) -> str: + return utc_now_as_str() + + +class CredentialPatch(BaseModel): + friendly_name: str | None = None + active: bool | None = None + last_login: str | None = None + sign_count: int | None = None + + @computed_field + @property + def updated(self) -> str: + return utc_now_as_str() + + +class UserSession(BaseModel): + id: str + login: str + acl: list | str + cred_id: str | None = None + lang: Literal[*ACCEPT_LANGUAGES] = "en" + profile: dict | UserProfile | None = {} + login_ts: float = Field(default_factory=ntime_utc_now) diff --git a/components/objects.py b/components/objects.py new file mode 100644 index 0000000..6842ef8 --- /dev/null +++ b/components/objects.py @@ -0,0 +1,401 @@ +from components.models.objects import ( + ObjectIdList, + model_classes, + validate_call, + UUID, + Literal, +) +from components.web.utils.quart import current_app, session +from components.utils import ensure_list, merge_models +from components.cache import buster +from components.database import * + + +@validate_call +async def get( + object_type: Literal[*model_classes["types"]], + object_id: UUID | list[UUID], + permission_validation=True, +): + get_objects = ObjectIdList(object_id=object_id).object_id + db_params = evaluate_db_params() + + if current_app and session.get("id"): + user_id = session["id"] + else: + user_id = "anonymous" + + if not user_id in IN_MEMORY_DB["OBJECTS_CACHE"]: + IN_MEMORY_DB["OBJECTS_CACHE"][user_id] = dict() + + async with TinyDB(**db_params) as db: + found_objects = db.table(object_type).search(Query().id.one_of(get_objects)) + + object_data = [] + + for o in found_objects: + o_parsed = model_classes["base"][object_type].model_validate(o) + + if ( + not "system" in session["acl"] + and permission_validation == True + and user_id not in o_parsed.details.assigned_users + ): + continue + + for k, v in o_parsed.details.model_dump(mode="json").items(): + if k == "assigned_domain": + if not v in IN_MEMORY_DB["OBJECTS_CACHE"][user_id]: + IN_MEMORY_DB["OBJECTS_CACHE"][user_id][v] = await get( + object_type="domains", + object_id=v, + permission_validation=False, + ) + + o_parsed.details.assigned_domain = IN_MEMORY_DB["OBJECTS_CACHE"][ + user_id + ][v] + elif k in ["assigned_arc_keypair", "assigned_dkim_keypair"] and v: + if not v in IN_MEMORY_DB["OBJECTS_CACHE"][user_id]: + IN_MEMORY_DB["OBJECTS_CACHE"][user_id][v] = await get( + object_type="keypairs", + object_id=v, + permission_validation=False, + ) + setattr(o_parsed.details, k, IN_MEMORY_DB["OBJECTS_CACHE"][user_id][v]) + elif k == "assigned_emailusers" and v: + o_parsed.details.assigned_emailusers = [] + for u in ensure_list(v): + if not u in IN_MEMORY_DB["OBJECTS_CACHE"][user_id]: + IN_MEMORY_DB["OBJECTS_CACHE"][user_id][u] = await get( + object_type="emailusers", + object_id=u, + permission_validation=False, + ) + o_parsed.details.assigned_emailusers.append( + IN_MEMORY_DB["OBJECTS_CACHE"][user_id][u] + ) + + object_data.append(o_parsed) + + if len(object_data) == 1: + return object_data.pop() + + return object_data if object_data else None + + +async def delete( + object_type: Literal[*model_classes["types"]], + object_id: UUID | list[UUID], +): + delete_objects = [o for o in ensure_list(await get(object_type, object_id))] + db_params = evaluate_db_params() + + if object_type == "domains": + for o in delete_objects: + addresses = await search(object_type="addresses", fully_resolve=False) + if o.id in [address.details.assigned_domain for address in addresses]: + raise ValueError("name", f"Domain {o.name} is not empty") + + async with TinyDB(**db_params) as db: + buster([o.id for o in delete_objects]) + return db.table(object_type).remove( + Query().id.one_of([o.id for o in delete_objects]) + ) + + +@validate_call +async def patch( + object_type: Literal[*model_classes["types"]], + object_id: UUID | list[UUID], + data: dict, +): + assert current_app and session.get("id") + + to_patch_objects = [o for o in ensure_list(await get(object_type, object_id))] + db_params = evaluate_db_params() + + for to_patch in to_patch_objects: + if not "system" in session["acl"]: + if not "details" in data: + data["details"] = dict() + for f in model_classes["system_fields"][object_type]: + data["details"][f] = getattr(to_patch.details, f) + + patch_data = model_classes["patch"][object_type].model_validate(data) + patched_object = merge_models( + to_patch, patch_data + ) # returns updated to_patch model + + conflicts = await search( + object_type=object_type, + match_all={ + f: getattr(patched_object.details, f) + for f in model_classes["unique_fields"][object_type] + }, + fully_resolve=False, + ) + if [o.id for o in conflicts if o.id != patched_object.id]: + raise ValueError( + f"details.{model_classes['unique_fields'][object_type][0]}", + "The provided object exists", + ) + + if object_type == "domains": + if "system" in session["acl"]: + addresses_in_domain = await search( + object_type="addresses", + match_all={"assigned_domain": patched_object.id}, + fully_resolve=False, + ) + if ( + patched_object.details.n_mailboxes + and len(addresses_in_domain) > patched_object.details.n_mailboxes + ): + raise ValueError( + f"details.n_mailboxes", + f"Cannot reduce allowed mailboxes below {len(addresses_in_domain)}", + ) + + else: + for attr in ["assigned_dkim_keypair", "assigned_arc_keypair"]: + # keypairs default to "", verify + if attr not in data.get("details", {}): + continue + + patched_obj_keypair = getattr(patched_object.details, attr) + patched_obj_keypair_id = ( + patched_obj_keypair.id + if hasattr(patched_obj_keypair, "id") + else patched_obj_keypair + ) + + to_patch_obj_keypair = getattr(to_patch.details, attr) + to_patch_obj_keypair_id = ( + to_patch_obj_keypair.id + if hasattr(to_patch_obj_keypair, "id") + else to_patch_obj_keypair + ) + + if patched_obj_keypair_id != to_patch_obj_keypair_id: + if not "system" in session["acl"]: + if not await get("keypairs", to_patch_obj_keypair_id): + raise ValueError( + f"details.{attr}", + f"Cannot unassign a non-permitted keypair", + ) + if not await get("keypairs", patched_obj_keypair_id): + raise ValueError( + f"details.{attr}", + f"Cannot assign non-permitted keypair", + ) + if object_type == "addresses": + if not "system" in session["acl"]: + if ( + patched_object.details.assigned_domain + != to_patch.details.assigned_domain.id + ): # only when assigned_domain changed + if ( + not len( + await get( + "domains", + [ + patched_object.details.assigned_domain, + to_patch.details.assigned_domain.id, + ], + ) + ) + == 2 + ): + # disallow a change to a permitted domain if the current domain is not permitted + raise ValueError( + "details.assigned_domain", + f"Cannot assign selected domain for object {to_patch.details.local_part}", + ) + + if set(patched_object.details.assigned_emailusers) != set( + [u.id for u in to_patch.details.assigned_emailusers] + ): # only when assigned_emailusers changed + non_permitted_users = set() + + for emailuser in [ + *patched_object.details.assigned_emailusers, + *[u.id for u in to_patch.details.assigned_emailusers], + ]: + _ = await get( + object_type="emailusers", + object_id=emailuser, + permission_validation=False, + ) + if session["id"] not in _.details.assigned_users: + non_permitted_users.add(_.name if _ else "") + + if non_permitted_users: + # disallow a change to a permitted domain if the current domain is not permitted + raise ValueError( + "details.assigned_emailusers", + f"You are not allow to change email user assignments for {', '.join(non_permitted_users)} of address {to_patch.details.local_part}", + ) + if ( + patched_object.details.assigned_domain + != to_patch.details.assigned_domain.id + ): + addresses_in_new_domain_now = await search( + object_type="addresses", + match_all={ + "assigned_domain": patched_object.details.assigned_domain + }, + fully_resolve=False, + ) + new_domain_data = await get( + object_type="domains", + object_id=patched_object.details.assigned_domain, + permission_validation=True, + ) + if ( + new_domain_data.details.n_mailboxes + and new_domain_data.details.n_mailboxes + < (len(addresses_in_new_domain_now) + 1) + ): + raise ValueError( + f"details.assigned_domain", + "The domain's mailbox limit is reached", + ) + + async with TinyDB(**db_params) as db: + db.table(object_type).update( + patched_object.model_dump( + mode="json", exclude_none=True, exclude={"name", "id", "created"} + ), + Query().id == to_patch.id, + ) + buster([o.id for o in to_patch_objects]) + + return [o.id for o in to_patch_objects] + + +async def create( + object_type: Literal[*model_classes["types"]], + data: dict, +): + assert current_app and session.get("id") + + db_params = evaluate_db_params() + + if not "details" in data: + data["details"] = dict() + + data["details"]["assigned_users"] = session["id"] + + create_object = model_classes["add"][object_type].model_validate(data) + + conflicts = await search( + object_type=object_type, + match_all={ + f: getattr(create_object.details, f) + for f in model_classes["unique_fields"][object_type] + }, + fully_resolve=False, + ) + if [o.id for o in conflicts]: + raise ValueError( + f"details.{model_classes['unique_fields'][object_type][0]}", + "The provided object exists", + ) + + if object_type == "addresses": + if not "system" in session["acl"]: + if not await get("domains", create_object.details.assigned_domain): + raise ValueError("name", "The provided domain is unavailable") + + addresses_in_domain = await search( + object_type="addresses", + match_all={"assigned_domain": create_object.details.assigned_domain}, + fully_resolve=False, + ) + domain_data = await get( + object_type="domains", + object_id=create_object.details.assigned_domain, + permission_validation=True, + ) + + domain_assigned_users = set(domain_data.details.assigned_users) + domain_assigned_users.add(session["id"]) + create_object.details.assigned_users = list(domain_assigned_users) + + if domain_data.details.n_mailboxes and domain_data.details.n_mailboxes < ( + len(addresses_in_domain) + 1 + ): + raise ValueError( + f"details.assigned_domain", + "The domain's mailbox limit is reached", + ) + + if object_type == "domains": + if not "system" in session["acl"]: + raise ValueError("name", "You need system permission to create a domain") + + async with TinyDB(**db_params) as db: + insert_data = create_object.model_dump(mode="json") + db.table(object_type).insert(insert_data) + + try: + del IN_MEMORY_DB["FORM_OPTIONS_CACHE"][session.get("id")][object_type] + finally: + return insert_data["id"] + + +async def search( + object_type: Literal[*model_classes["types"]], + object_id: UUID | None = None, + match_all: dict = {}, + match_any: dict = {}, + fully_resolve: bool = False, +): + db_params = evaluate_db_params() + + def search_object_id(s): + return (object_id and str(object_id) == s) or not object_id + + def filter_details(s, _any: bool = False): + def match(key, value, current_data): + if key in current_data: + if isinstance(value, list): + return any(item in ensure_list(current_data[key]) for item in value) + + return value in current_data[key] + + for sub_key, sub_value in current_data.items(): + if isinstance(sub_value, dict): + if match(key, value, sub_value): # Recursive call + return True + return False + + if _any: + return any(match(k, v, s) for k, v in match_any.items()) + + return all(match(k, v, s) for k, v in match_all.items()) + + query = Query().id.test(search_object_id) + if match_all: + query = query & Query().details.test(filter_details) + if match_any: + query = query & Query().details.test(filter_details, True) + + async with TinyDB(**db_params) as db: + matches = db.table(object_type).search(query) + + if fully_resolve: + return ensure_list( + await get( + object_type=object_type, + object_id=[o["id"] for o in matches], + permission_validation=False, + ) + or [] + ) + else: + _parsed = [] + for o in matches: + _parsed.append(model_classes["base"][object_type].model_validate(o)) + return _parsed diff --git a/components/system.py b/components/system.py new file mode 100644 index 0000000..f903670 --- /dev/null +++ b/components/system.py @@ -0,0 +1,18 @@ +import os +import glob +from components.models.system import SystemSettingsBase +from components.database import TinyDB, TINYDB_PARAMS + + +async def get_system_settings(): + async with TinyDB(**TINYDB_PARAMS) as db: + settings = db.table("system_settings").get(doc_id=1) or {} + return SystemSettingsBase.parse_obj(settings).dict() + + +def list_application_log_files(): + yield "logs/application.log" + for peer_dir in glob.glob("peer_files/*"): + peer_log = os.path.join(peer_dir, "logs/application.log") + if os.path.isfile(peer_log): + yield peer_log diff --git a/components/users.py b/components/users.py new file mode 100644 index 0000000..7d9acc5 --- /dev/null +++ b/components/users.py @@ -0,0 +1,228 @@ +from components.models.users import ( + AddCredential, + CredentialPatch, + Credential, + User, + UserAdd, + UserPatch, + UserProfile, + UserProfilePatch, + UserSession, + constr, + validate_call, + UUID, +) +from components.utils import merge_models +from components.database import * +from components.cache import buster + + +def _create_credentials_mapping(credentials: dict): + user_credentials = dict() + for c in credentials: + user_credentials.update({c["id"]: Credential.model_validate(c)}) + return user_credentials + + +@validate_call +async def what_id(login: str): + db_params = evaluate_db_params() + + async with TinyDB(**db_params) as db: + user = db.table("users").get(Query().login == login) + + if user: + return user["id"] + else: + raise ValueError("login", "The provided login name is unknown") + + +@validate_call +async def create(data: dict): + db_params = evaluate_db_params() + create_user = UserAdd.model_validate(data) + + async with TinyDB(**db_params) as db: + if db.table("users").search(Query().login == create_user.login): + raise ValueError("name", "The provided login name exists") + insert_data = create_user.model_dump(mode="json") + db.table("users").insert(insert_data) + + for user_id in IN_MEMORY_DB["FORM_OPTIONS_CACHE"].copy(): + if "users" in IN_MEMORY_DB["FORM_OPTIONS_CACHE"][user_id]: + del IN_MEMORY_DB["FORM_OPTIONS_CACHE"][user_id]["users"] + + return insert_data["id"] + + +@validate_call +async def get(user_id: UUID, join_credentials: bool = True): + db_params = evaluate_db_params() + + async with TinyDB(**db_params) as db: + user = User.model_validate(db.table("users").get(Query().id == str(user_id))) + credentials = db.table("credentials").search( + (Query().id.one_of(user.credentials)) + ) + if join_credentials: + user.credentials = _create_credentials_mapping(credentials) + + return user + + +@validate_call +async def delete(user_id: UUID): + db_params = evaluate_db_params() + user = await get(user_id=user_id, join_credentials=False) + + if not user: + raise ValueError("name", "The provided user does not exist") + + async with TinyDB(**db_params) as db: + if len(db.table("users").all()) == 1: + raise ValueError("name", "Cannot delete last user") + + db.table("credentials").remove(Query().id.one_of(user.credentials)) + deleted = db.table("users").remove(Query().id == str(user_id)) + buster(user.id) + return user.id + + +@validate_call +async def create_credential(user_id: UUID, data: dict): + db_params = evaluate_db_params() + credential = AddCredential.model_validate(data) + user = await get(user_id=user_id, join_credentials=False) + if not user: + raise ValueError("name", "The provided user does not exist") + + async with TinyDB(**db_params) as db: + db.table("credentials").insert(credential.model_dump(mode="json")) + user.credentials.append(credential.id) + db.table("users").update( + {"credentials": user.credentials}, + Query().id == str(user_id), + ) + return credential.id + + +@validate_call +async def delete_credential( + user_id: UUID, hex_id: constr(pattern=r"^[0-9a-fA-F]+$", min_length=2) +): + db_params = evaluate_db_params() + user = await get(user_id=user_id, join_credentials=False) + + if not user: + raise ValueError("name", "The provided user does not exist") + + async with TinyDB(**db_params) as db: + if hex_id in user.credentials: + user.credentials.remove(hex_id) + db.table("credentials").remove(Query().id == hex_id) + db.table("users").update( + {"credentials": user.credentials}, Query().id == str(user_id) + ) + return hex_id + + +@validate_call +async def patch(user_id: UUID, data: dict): + db_params = evaluate_db_params() + user = await get(user_id=user_id, join_credentials=False) + + if not user: + raise ValueError("name", "The provided user does not exist") + + patch_data = UserPatch.model_validate(data) + patched_user = merge_models( + user, + patch_data, + exclude_strategies=["exclude_override_none"], + ) + + async with TinyDB(**db_params) as db: + if db.table("users").get( + (Query().login == patched_user.login) & (Query().id != str(user_id)) + ): + raise ValueError("login", "The provided login name exists") + + orphaned_credentials = [ + c for c in user.credentials if c not in patched_user.credentials + ] + db.table("users").update( + patched_user.model_dump(mode="json"), + Query().id == str(user_id), + ) + db.table("credentials").remove(Query().id.one_of(orphaned_credentials)) + + buster(user.id) + return user.id + + +@validate_call +async def patch_profile(user_id: UUID, data: dict): + db_params = evaluate_db_params() + user = await get(user_id=user_id, join_credentials=False) + + if not user: + raise ValueError("name", "The provided user does not exist") + + patch_data = UserProfilePatch.model_validate(data) + patched_user_profile = merge_models( + user.profile, patch_data, exclude_strategies=["exclude_override_none"] + ) + + async with TinyDB(**db_params) as db: + db.table("users").update( + {"profile": patched_user_profile.model_dump(mode="json")}, + Query().id == str(user_id), + ) + return user_id + + +@validate_call +async def patch_credential( + user_id: UUID, hex_id: constr(pattern=r"^[0-9a-fA-F]+$", min_length=2), data: dict +): + db_params = evaluate_db_params() + user = await get(user_id=user_id, join_credentials=True) + + if not user: + raise ValueError("name", "The provided user does not exist") + + if hex_id not in user.credentials: + raise ValueError( + "hex_id", + "The provided credential ID was not found in user context", + ) + + patch_data = CredentialPatch.model_validate(data) + patched_credential = merge_models( + user.credentials[hex_id], + patch_data, + exclude_strategies=["exclude_override_none"], + ) + + async with TinyDB(**db_params) as db: + db.table("credentials").update( + patched_credential.model_dump(mode="json"), Query().id == hex_id + ) + return hex_id + + +@validate_call +async def search( + name: constr(strip_whitespace=True, min_length=0), join_credentials: bool = True +): + db_params = evaluate_db_params() + + def search_name(s): + return name in s + + async with TinyDB(**db_params) as db: + matches = db.table("users").search(Query().login.test(search_name)) + + return [ + await get(user["id"], join_credentials=join_credentials) for user in matches + ] diff --git a/components/utils/__init__.py b/components/utils/__init__.py new file mode 100644 index 0000000..bf84cd1 --- /dev/null +++ b/components/utils/__init__.py @@ -0,0 +1,99 @@ +import asyncio +import os +import re + +from components.models import Any, BaseModel, Literal, UUID +from copy import deepcopy + + +__all__ = [ + "merge_models", + "merge_deep", + "batch", + "ensure_list", + "to_unique_sorted_str_list", + "is_path_within_cwd", + "expire_key", +] + + +# Returns new model (based on original model) using +# deep merged data of both input models data +def merge_models( + original: BaseModel, + override: BaseModel, + exclude_strategies: list = Literal[ + "exclude_original_none", + "exclude_original_unset", + "exclude_override_none", + "exclude_override_unset", + ], +) -> BaseModel: + # Convert both models to dictionaries + original_data = original.model_dump( + mode="json", + exclude_none=True if "exclude_original_none" in exclude_strategies else False, + exclude_unset=True if "exclude_original_unset" in exclude_strategies else False, + ) + override_data = override.model_dump( + mode="json", + exclude_none=True if "exclude_override_none" in exclude_strategies else False, + exclude_unset=True if "exclude_override_unset" in exclude_strategies else False, + ) + + # Merge with override taking priority + merged_data = merge_deep(original_data, override_data) + + # Return a revalidated model using the original model's class + return original.__class__(**merged_data) + + +def merge_deep(original_data: dict, override_data: dict): + result = deepcopy(original_data) + + def _recursive_merge(_original_data, _override_data): + for key in _override_data: + if ( + key in _original_data + and isinstance(_original_data[key], dict) + and isinstance(_override_data[key], dict) + ): + _recursive_merge(_original_data[key], _override_data[key]) + else: + _original_data[key] = _override_data[key] + + _recursive_merge(result, override_data) + return result + + +def batch(l: list, n: int): + _l = len(l) + for ndx in range(0, _l, n): + yield l[ndx : min(ndx + n, _l)] + + +def chunk_string(s, size=1_000_000): + return [s[i : i + size] for i in range(0, len(s), size)] + + +def ensure_list(a: Any | list[Any] | None) -> list: + if a: + if not isinstance(a, list): + return [a] + return a + return [] + + +def to_unique_sorted_str_list(l: list[str | UUID]) -> list: + _l = [str(x) for x in set(l) if x] + return sorted(_l) + + +def is_path_within_cwd(path): + requested_path = os.path.abspath(path) + return requested_path.startswith(os.path.abspath(".")) + + +async def expire_key(in_dict: dict, dict_key: int | str, wait_time: float | int): + await asyncio.sleep(wait_time) + in_dict.pop(dict_key, None) diff --git a/components/utils/cryptography.py b/components/utils/cryptography.py new file mode 100644 index 0000000..5708901 --- /dev/null +++ b/components/utils/cryptography.py @@ -0,0 +1,129 @@ +import base64 +import hashlib +import os +import json + +from cryptography.fernet import Fernet +from cryptography.fernet import InvalidToken +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import padding +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from functools import lru_cache + + +__all__ = [ + "InvalidToken", + "fernet_encrypt", + "fernet_decrypt", + "aes_cbc_encrypt", + "aes_cbc_decrypt", +] + + +def generate_rsa_dkim(key_size, public_exponent=65537): + private_key = rsa.generate_private_key( + public_exponent=public_exponent, + key_size=key_size, + ) + + public_key_bytes = private_key.public_key().public_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + private_key_pem_bytes = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + public_key_string = base64.b64encode(public_key_bytes).decode("utf-8") + private_key_pem_string = private_key_pem_bytes.decode("utf-8") + + return private_key_pem_string, public_key_string + + +def file_digest_sha256(filename: str): + with open(filename, "rb", buffering=0) as f: + return hashlib.file_digest(f, "256").hexdigest() + + +def dict_digest_sha1(d: str): + return hashlib.sha1(json.dumps(d, sort_keys=True).encode("utf-8")).hexdigest() + + +def dict_digest_sha256(d: str): + return hashlib.sha256(json.dumps(d, sort_keys=True).encode("utf-8")).hexdigest() + + +def aes_cbc_encrypt( + data: str, code: str, iv: bytes = os.urandom(16), salt: bytes = os.urandom(16) +) -> bytes: + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=salt, + iterations=100000, + backend=default_backend(), + ) + key = kdf.derive(code.encode("utf-8")) + cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) + padder = padding.PKCS7(algorithms.AES.block_size).padder() + padded_data = padder.update(data.encode("utf-8")) + padder.finalize() + encryptor = cipher.encryptor() + ciphertext = encryptor.update(padded_data) + encryptor.finalize() + return salt + iv + ciphertext + + +@lru_cache +def aes_cbc_decrypt(data: bytes, code: str) -> str: + salt = data[:16] + iv = data[16:32] + ciphertext = data[32:] + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=salt, + iterations=100000, + backend=default_backend(), + ) + key = kdf.derive(code.encode("utf-8")) + cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) + decryptor = cipher.decryptor() + padded_plaintext = decryptor.update(ciphertext) + decryptor.finalize() + unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() + plaintext = unpadder.update(padded_plaintext) + unpadder.finalize() + return plaintext.decode(encoding="utf-8") + + +def fernet_encrypt(data: str, code: str, salt: bytes = os.urandom(16)) -> bytes: + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=salt, + iterations=100000, + backend=default_backend(), + ) + fernet = Fernet( + base64.urlsafe_b64encode(kdf.derive(code.encode("utf-8"))), + ) + return base64.urlsafe_b64encode(salt + fernet.encrypt(data.encode("utf-8"))) + + +@lru_cache +def fernet_decrypt(data: str, code: str) -> str: + data = base64.urlsafe_b64decode(data) + salt = data[:16] + encrypted_data = data[16:] + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=salt, + iterations=100000, + backend=default_backend(), + ) + fernet = Fernet(base64.urlsafe_b64encode(kdf.derive(code.encode("utf-8")))) + return fernet.decrypt(encrypted_data).decode(encoding="utf-8") diff --git a/components/utils/datetimes.py b/components/utils/datetimes.py new file mode 100644 index 0000000..634dd38 --- /dev/null +++ b/components/utils/datetimes.py @@ -0,0 +1,28 @@ +import os +from datetime import datetime, timezone, UTC, timedelta + + +def system_now_as_str(): + return datetime.now().astimezone().strftime("%Y-%m-%dT%H:%M:%S%z") + + +def ntime_utc_now(): + return datetime.now(UTC).timestamp() + + +def utc_now_as_str(dtformat="%Y-%m-%dT%H:%M:%S%z"): + return datetime.now(timezone.utc).strftime(dtformat) + + +def last_modified_http(file): + try: + last_modified_time = os.path.getmtime(file) + except FileNotFoundError: + last_modified_time = 0 + return datetime.utcfromtimestamp(last_modified_time).strftime( + "%a, %d %b %Y %H:%M:%S GMT" + ) + + +def parse_last_modified_http(last_modified_http): + return datetime.strptime(last_modified_http, "%a, %d %b %Y %H:%M:%S GMT") diff --git a/components/web/__init__.py b/components/web/__init__.py new file mode 100644 index 0000000..e12268f --- /dev/null +++ b/components/web/__init__.py @@ -0,0 +1,149 @@ +import asyncio +import json +import os +import time + +from config import defaults +from components.database import IN_MEMORY_DB +from components.utils import merge_deep, ensure_list +from components.utils.datetimes import ntime_utc_now +from components.web.blueprints import * +from components.web.utils import * + +app = Quart( + __name__, + static_url_path="/static", + static_folder="static_files", + template_folder="templates", +) + +app.register_blueprint(root.blueprint) +app.register_blueprint(auth.blueprint) +app.register_blueprint(objects.blueprint) +app.register_blueprint(profile.blueprint) +app.register_blueprint(system.blueprint) +app.register_blueprint(users.blueprint) + +app.config["SEND_FILE_MAX_AGE_DEFAULT"] = defaults.SEND_FILE_MAX_AGE_DEFAULT +app.config["SECRET_KEY"] = defaults.SECRET_KEY +app.config["TEMPLATES_AUTO_RELOAD"] = defaults.TEMPLATES_AUTO_RELOAD +app.config["SERVER_NAME"] = defaults.HOSTNAME +app.config["MOD_REQ_LIMIT"] = 10 +IN_MEMORY_DB["SESSION_VALIDATED"] = dict() +IN_MEMORY_DB["WS_CONNECTIONS"] = dict() +IN_MEMORY_DB["FORM_OPTIONS_CACHE"] = dict() +IN_MEMORY_DB["OBJECTS_CACHE"] = dict() +IN_MEMORY_DB["APP_LOGS_FULL_PULL"] = dict() +IN_MEMORY_DB["PROMOTE_USERS"] = set() + +modifying_request_limiter = asyncio.Semaphore(app.config["MOD_REQ_LIMIT"]) + + +@app.context_processor +def load_context(): + enforce_dbupdate = IN_MEMORY_DB.get("ENFORCE_DBUPDATE", False) + if enforce_dbupdate: + enforce_dbupdate = defaults.CLUSTER_ENFORCE_DBUPDATE_TIMEOUT - ( + round(ntime_utc_now() - enforce_dbupdate) + ) + + return { + "ENFORCE_DBUPDATE": enforce_dbupdate, + } + + +@app.errorhandler(ClusterHTTPException) +async def handle_cluster_error(error): + error_msg = str(error.description) + await ws_htmx( + "system", + "beforeend", + """""".format( + error=error_msg + ), + ) + return trigger_notification( + level="error", + response_body="", + response_code=error.code, + title="Cluster error", + message=error_msg, + ) + + +@app.before_request +async def before_request(): + request.form_parsed = {} + request.locked = False + + if session.get("id") and session["id"] in IN_MEMORY_DB["PROMOTE_USERS"]: + IN_MEMORY_DB["PROMOTE_USERS"].discard(session["id"]) + user = await get(user_id=session["id"]) + if "system" not in user.acl: + user.acl.append("system") + session["acl"] = user.acl + IN_MEMORY_DB["SESSION_VALIDATED"].update({session["id"]: user.acl}) + + if request.method in ["POST", "PATCH", "PUT", "DELETE"]: + await modifying_request_limiter.acquire() + form = await request.form + request.form_parsed = dict() + if form: + for k in form: + v = form.getlist(k) + if len(v) == 1: + request.form_parsed = merge_deep( + request.form_parsed, parse_form_to_dict(k, v.pop()) + ) + else: + request.form_parsed = merge_deep( + request.form_parsed, parse_form_to_dict(k, v) + ) + + +@app.after_request +async def after_request(response): + if defaults.DISABLE_CACHE == False: + return response + + response.headers["Pragma"] = "no-cache" + response.headers["Expires"] = "0" + response.headers["Cache-Control"] = "public, max-age=0" + + return response + + +@app.teardown_request +async def teardown_request(exc): + modifying_request_limiter.release() + + +@app.context_processor +def load_defaults(): + _defaults = { + k: v + for k, v in defaults.__dict__.items() + if not (k.startswith("__") or k.startswith("_")) + } + return _defaults + + +@app.template_filter(name="hex") +def to_hex(value): + return value.hex() + + +@app.template_filter(name="ensurelist") +def ensurelist(value): + return ensure_list(value) + + +@app.template_filter(name="toprettyjson") +def to_prettyjson(value): + return json.dumps(value, sort_keys=True, indent=2, separators=(",", ": ")) diff --git a/components/web/blueprints/__init__.py b/components/web/blueprints/__init__.py new file mode 100644 index 0000000..f786e6a --- /dev/null +++ b/components/web/blueprints/__init__.py @@ -0,0 +1,6 @@ +from components.web.blueprints import auth +from components.web.blueprints import objects +from components.web.blueprints import profile +from components.web.blueprints import root +from components.web.blueprints import system +from components.web.blueprints import users diff --git a/components/web/blueprints/auth.py b/components/web/blueprints/auth.py new file mode 100644 index 0000000..fa2d15a --- /dev/null +++ b/components/web/blueprints/auth.py @@ -0,0 +1,647 @@ +import asyncio +import json + +from base64 import b64decode, b64encode +from components.models.users import ( + UserSession, + AuthToken, + TokenConfirmation, + TypeAdapter, + uuid4, +) +from components.web.utils import * +from secrets import token_urlsafe +from components.utils import expire_key +from components.utils.datetimes import utc_now_as_str +from components.users import ( + get as get_user, + what_id, + patch_credential, + create as create_user, + create_credential, +) +from webauthn import ( + generate_registration_options, + options_to_json, + verify_registration_response, + verify_authentication_response, + generate_authentication_options, +) +from webauthn.helpers import ( + parse_registration_credential_json, + parse_authentication_credential_json, +) +from webauthn.helpers.structs import ( + AuthenticatorSelectionCriteria, + AttestationConveyancePreference, + UserVerificationRequirement, + ResidentKeyRequirement, + PublicKeyCredentialDescriptor, +) + +blueprint = Blueprint("auth", __name__, url_prefix="/auth") + + +# A link to be sent to a user to login using webauthn authentication +# /auth/login/request/confirm/ +@blueprint.route("/login/request/confirm/") +async def login_request_confirm(request_token: str): + try: + TypeAdapter(str).validate_python(request_token) + except: + return "", 200, {"HX-Redirect": "/"} + + token_status = IN_MEMORY_DB.get(request_token, {}).get("status") + + if token_status == "awaiting": + session["request_token"] = request_token + requested_login = IN_MEMORY_DB[request_token]["requested_login"] + + return await render_template( + "auth/login/request/confirm.html", + login=requested_login, + ) + + session["request_token"] = None + + return "", 200, {"HX-Redirect": "/profile", "HX-Refresh": False} + + +@blueprint.route("/register/request/confirm/", methods=["POST", "GET"]) +@acl("system") +async def register_request_confirm_modal(login: str): + return await render_template("auth/register/request/confirm.html") + + +# As shown to user that is currently logged in +# /auth/login/request/confirm/modal/ +@blueprint.route( + "/login/request/confirm/internal/", methods=["POST", "GET"] +) +@acl("any") +async def login_request_confirm_modal(request_token: str): + try: + TypeAdapter(str).validate_python(request_token) + except: + return "", 204 + + if request.method == "POST": + if ( + request_token in IN_MEMORY_DB + and IN_MEMORY_DB[request_token]["status"] == "awaiting" + ): + IN_MEMORY_DB[request_token].update( + { + "status": "confirmed", + "credential_id": "", + } + ) + current_app.add_background_task( + expire_key, + IN_MEMORY_DB, + request_token, + 10, + ) + + await ws_htmx(session["login"], "delete:#auth-login-request", "") + + return "", 204 + + return trigger_notification( + level="warning", + response_code=403, + title="Confirmation failed", + message="Token denied", + ) + + return await render_template("auth/login/request/internal/confirm.html") + + +# An unknown user issues a login request to users that are currently logged in +# /auth/login/request/start +@blueprint.route("/login/request/start", methods=["POST"]) +async def login_request_start(): + try: + request_data = AuthToken.parse_obj(request.form_parsed) + except ValidationError as e: + return validation_error(e.errors()) + + session_clear() + + try: + user_id = await what_id(login=request_data.login) + user = await get_user(user_id=user_id) + except (ValidationError, ValueError): + return validation_error([{"loc": ["login"], "msg": f"User is not available"}]) + + request_token = token_urlsafe() + + IN_MEMORY_DB[request_token] = { + "intention": f"Authenticate user: {request_data.login}", + "status": "awaiting", + "token_type": "web_confirmation", + "requested_login": request_data.login, + } + + current_app.add_background_task( + expire_key, + IN_MEMORY_DB, + request_token, + defaults.AUTH_REQUEST_TIMEOUT, + ) + + if user.profile.permit_auth_requests: + await ws_htmx( + request_data.login, + "beforeend", + f'
', + ) + + return await render_template( + "auth/login/request/start.html", + data={ + "request_token": request_token, + "request_issued_to_user": user.profile.permit_auth_requests, + }, + ) + + +# Polled every second by unknown user that issued a login request +# /auth/login/request/check/ +@blueprint.route("/login/request/check/") +async def login_request_check(request_token: str): + try: + TypeAdapter(str).validate_python(request_token) + except: + session.clear() + return "", 200, {"HX-Redirect": "/"} + + token_status, requested_login, credential_id = map( + IN_MEMORY_DB.get(request_token, {}).get, + ["status", "requested_login", "credential_id"], + ) + + if token_status == "confirmed": + try: + user_id = await what_id(login=requested_login) + user = await get_user(user_id=user_id) + except ValidationError as e: + return validation_error( + [{"loc": ["login"], "msg": f"User is not available"}] + ) + + for k, v in ( + UserSession( + login=user.login, + id=user.id, + acl=user.acl, + cred_id=credential_id, + lang=request.accept_languages.best_match(defaults.ACCEPT_LANGUAGES), + profile=user.profile, + ) + .dict() + .items() + ): + session[k] = v + + else: + if token_status: + return "", 204 + + return "", 200, {"HX-Redirect": "/"} + + +@blueprint.route("/login/token", methods=["POST"]) +async def login_token(): + try: + request_data = AuthToken.parse_obj(request.form_parsed) + token = request_data.token + IN_MEMORY_DB[token] = { + "intention": f"Authenticate user: {request_data.login}", + "status": "awaiting", + "token_type": "cli_confirmation", + "login": request_data.login, + } + current_app.add_background_task( + expire_key, + IN_MEMORY_DB, + token, + 120, + ) + + except ValidationError as e: + return validation_error(e.errors()) + + return await render_template( + "auth/login/token.html", + token=token, + ) + + +@blueprint.route("/login/token/verify", methods=["POST"]) +async def login_token_verify(): + try: + request_data = TokenConfirmation.parse_obj(request.form_parsed) + + token_status, token_login, token_confirmation_code = map( + IN_MEMORY_DB.get(request_data.token, {}).get, + ["status", "login", "code"], + ) + IN_MEMORY_DB.pop(request_data.token, None) + + if ( + token_status != "confirmed" + or token_confirmation_code != request_data.confirmation_code + ): + return validation_error( + [ + { + "loc": ["confirmation_code"], + "msg": "Confirmation code is invalid", + } + ] + ) + + user_id = await what_id(login=token_login) + user = await get_user(user_id=user_id) + + except ValidationError as e: + return validation_error(e.errors()) + + for k, v in ( + UserSession( + login=token_login, + id=user.id, + acl=user.acl, + lang=request.accept_languages.best_match(defaults.ACCEPT_LANGUAGES), + profile=user.profile, + ) + .dict() + .items() + ): + session[k] = v + + return "", 200, {"HX-Redirect": "/profile", "HX-Refresh": False} + + +# Generate login options for webauthn authentication +@blueprint.route("/login/webauthn/options", methods=["POST"]) +async def login_webauthn_options(): + try: + user_id = await what_id(login=request.form_parsed.get("login")) + user = await get_user(user_id=user_id) + if not user.credentials: + raise ValidationError + except (ValidationError, ValueError): + return validation_error([{"loc": ["login"], "msg": f"User is not available"}]) + + allow_credentials = [ + PublicKeyCredentialDescriptor(id=bytes.fromhex(c)) + for c in user.credentials.keys() + ] + + options = generate_authentication_options( + rp_id=defaults.WEBAUTHN_RP_ID, + timeout=defaults.WEBAUTHN_CHALLENGE_TIMEOUT * 1000, + allow_credentials=allow_credentials, + user_verification=UserVerificationRequirement.REQUIRED, + ) + + session["webauthn_challenge_id"] = token_urlsafe() + + IN_MEMORY_DB[session["webauthn_challenge_id"]] = { + "challenge": b64encode(options.challenge), + "login": user.login, + } + current_app.add_background_task( + expire_key, + IN_MEMORY_DB, + session["webauthn_challenge_id"], + defaults.WEBAUTHN_CHALLENGE_TIMEOUT, + ) + + return "", 204, {"HX-Trigger": json.dumps({"startAuth": options_to_json(options)})} + + +@blueprint.route("/register/token", methods=["POST"]) +async def register_token(): + try: + request_data = AuthToken.parse_obj(request.form_parsed) + token = request_data.token + IN_MEMORY_DB[token] = { + "intention": f"Register user: {request_data.login}", + "status": "awaiting", + "token_type": "cli_confirmation", + "login": request_data.login, + } + current_app.add_background_task( + expire_key, + IN_MEMORY_DB, + token, + 120, + ) + await ws_htmx( + "_system", + "beforeend", + f'
', + ) + + except ValidationError as e: + return validation_error(e.errors()) + + return await render_template( + "auth/register/token.html", + token=token, + ) + return template + + +@blueprint.route("/register/webauthn/options", methods=["POST"]) +async def register_webauthn_options(): + if "token" in request.form_parsed: + try: + request_data = TokenConfirmation.parse_obj(request.form_parsed) + except ValidationError as e: + return validation_error(e.errors()) + + token_status, token_login, token_confirmation_code = map( + IN_MEMORY_DB.get(request_data.token, {}).get, + ["status", "login", "code"], + ) + IN_MEMORY_DB.pop(request_data.token, None) + + if ( + token_status != "confirmed" + or token_confirmation_code != request_data.confirmation_code + ): + return validation_error( + [ + { + "loc": ["confirmation_code"], + "msg": "Confirmation code is invalid", + } + ] + ) + + exclude_credentials = [] + user_id = str(uuid4()) + login = token_login + appending_passkey = False + else: + if not session.get("id"): + return trigger_notification( + level="error", + response_code=409, + title="Registration failed", + message="Something went wrong", + ) + + user = await get_user(user_id=session["id"]) + + exclude_credentials = [ + PublicKeyCredentialDescriptor(id=bytes.fromhex(c)) + for c in user.credentials.keys() + ] + + user_id = session["id"] + login = session["login"] + appending_passkey = True + + options = generate_registration_options( + rp_name=defaults.WEBAUTHN_RP_NAME, + rp_id=defaults.WEBAUTHN_RP_ID, + user_id=user_id.encode("ascii"), + timeout=defaults.WEBAUTHN_CHALLENGE_TIMEOUT * 1000, + exclude_credentials=exclude_credentials, + user_name=login, + attestation=AttestationConveyancePreference.DIRECT, + authenticator_selection=AuthenticatorSelectionCriteria( + user_verification=UserVerificationRequirement.REQUIRED, + resident_key=ResidentKeyRequirement.REQUIRED, + ), + ) + + session["webauthn_challenge_id"] = token_urlsafe() + + IN_MEMORY_DB[session["webauthn_challenge_id"]] = { + "challenge": b64encode(options.challenge), + "login": login, + "user_id": user_id, + "appending_passkey": appending_passkey, + } + current_app.add_background_task( + expire_key, + IN_MEMORY_DB, + session["webauthn_challenge_id"], + defaults.WEBAUTHN_CHALLENGE_TIMEOUT, + ) + + return "", 204, {"HX-Trigger": json.dumps({"startReg": options_to_json(options)})} + + +@blueprint.route("/register/webauthn", methods=["POST"]) +async def register_webauthn(): + json_body = await request.json + + webauthn_challenge_id = session.get("webauthn_challenge_id") + session["webauthn_challenge_id"] = None + + challenge, login, user_id, appending_passkey = map( + IN_MEMORY_DB.get(webauthn_challenge_id, {}).get, + ["challenge", "login", "user_id", "appending_passkey"], + ) + IN_MEMORY_DB.pop(webauthn_challenge_id, None) + + if not challenge: + return trigger_notification( + level="error", + response_code=409, + title="Registration session invalid", + message="Registration session invalid", + additional_triggers={"authRegFailed": "register"}, + ) + + try: + credential = parse_registration_credential_json(json_body) + verification = verify_registration_response( + credential=credential, + expected_challenge=b64decode(challenge), + expected_rp_id=defaults.WEBAUTHN_RP_ID, + expected_origin=f"https://{defaults.WEBAUTHN_RP_ORIGIN}", + require_user_verification=True, + ) + except Exception as e: + return trigger_notification( + level="error", + response_code=409, + title="Registration failed", + message="An error occured while verifying the credential", + additional_triggers={"authRegFailed": "register"}, + ) + + credential_data = { + "id": verification.credential_id, + "public_key": verification.credential_public_key, + "sign_count": verification.sign_count, + "friendly_name": "Key Anú Reeves", + "transports": json_body.get("transports", []), + } + + try: + async with ClusterLock(["users", "credentials"], current_app): + if not appending_passkey: + user_id = await create_user(data={"login": login}) + + await create_credential( + user_id=user_id, + data={ + "id": verification.credential_id, + "public_key": verification.credential_public_key, + "sign_count": verification.sign_count, + "transports": json_body.get("transports", []), + }, + ) + + except Exception as e: + return trigger_notification( + level="error", + response_code=409, + title="Registration failed", + message="An error occured verifying the registration", + additional_triggers={"authRegFailed": "register"}, + ) + + if appending_passkey: + await ws_htmx( + session["login"], + "beforeend", + f'
', + ) + return trigger_notification( + level="success", + response_code=204, + title="New token registered", + message="A new token was appended to your account and can now be used to login", + ) + + return trigger_notification( + level="success", + response_code=204, + title="Welcome on board 👋", + message="Your account was created, you can now log in", + additional_triggers={"regCompleted": login}, + ) + + +@blueprint.route("/login/webauthn", methods=["POST"]) +async def auth_login_verify(): + json_body = await request.json + + try: + webauthn_challenge_id = session.get("webauthn_challenge_id") + challenge, login = map( + IN_MEMORY_DB.get(webauthn_challenge_id, {}).get, + ["challenge", "login"], + ) + IN_MEMORY_DB.pop(webauthn_challenge_id, None) + session["webauthn_challenge_id"] = None + + if not all([webauthn_challenge_id, challenge, login]): + return trigger_notification( + level="error", + response_code=409, + title="Verification failed", + message="Verification process timed out", + additional_triggers={"authRegFailed": "authenticate"}, + ) + + auth_challenge = b64decode(challenge) + + user_id = await what_id(login=login) + user = await get_user(user_id=user_id) + + credential = parse_authentication_credential_json(json_body) + + matched_user_credential = None + for k, v in user.credentials.items(): + if bytes.fromhex(k) == credential.raw_id: + matched_user_credential = v + + if not matched_user_credential: + return trigger_notification( + level="error", + response_code=409, + title="Verification failed", + message="No such credential in user realm", + additional_triggers={"authRegFailed": "authenticate"}, + ) + + verification = verify_authentication_response( + credential=credential, + expected_challenge=auth_challenge, + expected_rp_id=defaults.WEBAUTHN_RP_ORIGIN, + expected_origin=f"https://{defaults.WEBAUTHN_RP_ORIGIN}", + credential_public_key=matched_user_credential.public_key, + credential_current_sign_count=matched_user_credential.sign_count, + require_user_verification=True, + ) + + matched_user_credential.last_login = utc_now_as_str() + if matched_user_credential.sign_count != 0: + matched_user_credential.sign_count = verification.new_sign_count + + async with ClusterLock("credentials", current_app): + user_id = await what_id(login=login) + await patch_credential( + user_id=user_id, + hex_id=credential.raw_id.hex(), + data=matched_user_credential.model_dump(mode="json"), + ) + + except Exception as e: + return trigger_notification( + level="error", + response_code=409, + title="Verification failed", + message="An error occured verifying the credential", + additional_triggers={"authRegFailed": "authenticate"}, + ) + + request_token = session.get("request_token") + + if request_token: + """ + Not setting session login and id for device that is confirming the proxy authentication + Gracing 10s for the awaiting party to catch up an almost expired key + """ + IN_MEMORY_DB[request_token].update( + { + "status": "confirmed", + "credential_id": credential.raw_id.hex(), + } + ) + current_app.add_background_task( + expire_key, + IN_MEMORY_DB, + request_token, + 10, + ) + session["request_token"] = None + + return "", 204, {"HX-Trigger": "proxyAuthSuccess"} + + for k, v in ( + UserSession( + login=user.login, + id=user.id, + acl=user.acl, + cred_id=credential.raw_id.hex(), + lang=request.accept_languages.best_match(defaults.ACCEPT_LANGUAGES), + profile=user.profile, + ) + .dict() + .items() + ): + session[k] = v + + return "", 200, {"HX-Redirect": "/profile", "HX-Refresh": False} diff --git a/components/web/blueprints/objects.py b/components/web/blueprints/objects.py new file mode 100644 index 0000000..4c0afc0 --- /dev/null +++ b/components/web/blueprints/objects.py @@ -0,0 +1,265 @@ +import components.objects + +from components.models.objects import model_classes, uuid4 +from components.utils import batch, ensure_list +from components.web.utils import * + +blueprint = Blueprint("objects", __name__, url_prefix="/objects") + + +@blueprint.context_processor +async def load_context(): + context = dict() + context["system_fields"] = { + object_type: system_fields + for object_type, system_fields in model_classes["system_fields"].items() + } + context["unique_fields"] = { + object_type: unique_fields + for object_type, unique_fields in model_classes["unique_fields"].items() + } + context["schemas"] = { + object_type: v.model_json_schema() + for object_type, v in model_classes["forms"].items() + } + return context + + +@blueprint.before_request +async def objects_before_request(): + if "object_type" in request.view_args: + if request.view_args["object_type"] not in model_classes["types"]: + if "Hx-Request" in request.headers: + return trigger_notification( + level="error", + response_code=204, + title="Object type error", + message="Object type is unknown", + ) + else: + return (f"

Object type error

Object type is unknown

", 409) + + +@blueprint.route("//") +@acl("user") +@formoptions(["emailusers", "domains", "keypairs", "users"]) +async def get_object(object_type: str, object_id: str): + try: + object_data = await components.objects.get( + object_id=object_id, object_type=object_type + ) + + if not object_data: + return (f"

Object not found

Object is unknown

", 404) + + """ + Inject form options not provided by user permission. + A user will not be able to access detailed information about objects + with inherited permission nor will they be able to remove assignments. + """ + if object_type == "addresses": + object_domain = { + "name": object_data.details.assigned_domain.name, + "value": object_data.details.assigned_domain.id, + } + if object_domain not in request.form_options["domains"]: + object_domain["name"] = f"{object_data.details.assigned_domain.name} ⚠️" + request.form_options["domains"].append(object_domain) + + for object_emailuser in object_data.details.assigned_emailusers: + if object_emailuser: + u = { + "name": object_emailuser.name, + "value": object_emailuser.id, + } + if u not in request.form_options["emailusers"]: + u["name"] = f"{object_emailuser.name} ⚠️" + request.form_options["emailusers"].append(u) + + elif object_type == "domains": + object_keypair_injections = [] + for attr in ["assigned_dkim_keypair", "assigned_arc_keypair"]: + object_keypair_details = getattr(object_data.details, attr) + if hasattr(object_keypair_details, "id"): + object_keypair = { + "name": object_keypair_details.name, + "value": object_keypair_details.id, + } + if ( + object_keypair not in request.form_options["keypairs"] + and object_keypair not in object_keypair_injections + ): + object_keypair_injections.append(object_keypair) + for keypair in object_keypair_injections: + keypair["name"] = keypair["name"] + " ⚠️" + request.form_options["keypairs"].append(keypair) + + except ValidationError as e: + return validation_error(e.errors()) + except ValueError as e: + name, message = e.args + return validation_error([{"loc": [name], "msg": message}]) + + return await render_or_json( + "objects/object.html", request.headers, object=object_data + ) + + +@blueprint.route("/") +@blueprint.route("//search", methods=["POST"]) +@acl("user") +@formoptions(["domains"]) +async def get_objects(object_type: str): + try: + ( + search_model, + page, + page_size, + sort_attr, + sort_reverse, + filters, + ) = table_search_helper( + request.form_parsed, object_type, default_sort_attr="name" + ) + except ValidationError as e: + return validation_error(e.errors()) + + if request.method == "POST": + try: + match_any = { + "key_name": search_model.q, + "domain": search_model.q, + "local_part": search_model.q, + "username": search_model.q, + "assigned_domain": search_model.q, + } + match_all = ( + {"assigned_users": [session["id"]]} + if not "system" in session["acl"] + else {} + ) + matched_objects = await components.objects.search( + object_type=object_type, + match_any=match_any, + fully_resolve=True, + match_all=filters | match_all, + ) + except ValidationError as e: + return validation_error(e.errors()) + + object_pages = [ + m + for m in batch( + sorted( + matched_objects, + key=lambda x: getattr(x, sort_attr), + reverse=sort_reverse, + ), + page_size, + ) + ] + + try: + object_pages[page - 1] + except IndexError: + page = len(object_pages) + + objects = object_pages[page - 1] if page else object_pages + + return await render_template( + "objects/includes/objects/table_body.html", + data={ + "objects": objects, + "page_size": page_size, + "page": page, + "pages": len(object_pages), + "elements": len(matched_objects), + }, + ) + else: + return await render_template( + "objects/objects.html", data={"object_type": object_type} + ) + + +@blueprint.route("/", methods=["POST"]) +@acl("user") +async def create_object(object_type: str): + try: + async with ClusterLock(object_type, current_app): + object_id = await components.objects.create( + object_type=object_type, data=request.form_parsed + ) + except ValidationError as e: + return validation_error(e.errors()) + except ValueError as e: + name, message = e.args + return validation_error([{"loc": [name], "msg": message}]) + + return trigger_notification( + level="success", + response_code=204, + title="Object created", + message=f"Object {object_id} created", + ) + + +@blueprint.route("//delete", methods=["POST"]) +@blueprint.route("//", methods=["DELETE"]) +@acl("user") +async def delete_object(object_type: str, object_id: str | None = None): + if request.method == "POST": + object_id = request.form_parsed.get("id") + try: + object_ids = ensure_list(object_id) + async with ClusterLock(object_type, current_app): + deleted_objects = await components.objects.delete( + object_id=object_ids, object_type=object_type + ) + + except ValidationError as e: + return validation_error(e.errors()) + except ValueError as e: + name, message = e.args + return validation_error([{"loc": [name], "msg": message}]) + + return trigger_notification( + level="success", + response_code=204, + title="Object removed", + message=f"{len(deleted_objects)} object{'s' if len(deleted_objects) > 1 else ''} removed", + ) + + +@blueprint.route("//patch", methods=["POST"]) +@blueprint.route("//", methods=["PATCH"]) +@acl("user") +async def patch_object(object_type: str, object_id: str | None = None): + if request.method == "POST": + object_id = request.form_parsed.get("id") + try: + async with ClusterLock(object_type, current_app): + patched_objects = await components.objects.patch( + object_id=object_id, object_type=object_type, data=request.form_parsed + ) + + except ValidationError as e: + return validation_error(e.errors()) + except ValueError as e: + name, message = e.args + return validation_error([{"loc": [name], "msg": message}]) + + await ws_htmx( + "_user", + "beforeend", + f'
', + f"/objects/{object_type}/{object_id}", + exclude_self=True, + ) + + return trigger_notification( + level="success" if len(patched_objects) > 0 else "warning", + response_code=204, + title="Patch completed", + message=f"{len(patched_objects)} object{'s' if (len(patched_objects) > 1 or len(patched_objects) == 0) else ''} modified", + ) diff --git a/components/web/blueprints/profile.py b/components/web/blueprints/profile.py new file mode 100644 index 0000000..b5be9a3 --- /dev/null +++ b/components/web/blueprints/profile.py @@ -0,0 +1,103 @@ +import components.users + +from components.models.users import UserProfile +from components.web.utils import * + +blueprint = Blueprint("profile", __name__, url_prefix="/profile") + + +@blueprint.context_processor +def load_context(): + context = dict() + context["schemas"] = {"user_profile": UserProfile.model_json_schema()} + return context + + +@blueprint.route("/") +@acl("any") +async def user_profile_get(): + try: + user = await components.users.get(user_id=session["id"]) + except ValidationError: + session_clear() + return redirect(url_for("root.root")) + except ValueError as e: + name, message = e.args + return validation_error([{"loc": [name], "msg": message}]) + + return await render_template( + "profile/profile.html", + data={ + "user": user.dict(), + "keypair": None, + "credentials": user.credentials, + }, + ) + + +@blueprint.route("/edit", methods=["PATCH"]) +@acl("any") +async def user_profile_patch(): + try: + async with ClusterLock("users", current_app): + await components.users.patch_profile( + user_id=session["id"], data=request.form_parsed + ) + + user = await components.users.get(user_id=session["id"]) + + except ValidationError as e: + return validation_error(e.errors()) + except ValueError as e: + name, message = e.args + return validation_error([{"loc": [name], "msg": message}]) + + session.pop("profile", None) + session["profile"] = user.profile.dict() + + return trigger_notification( + level="success", + response_code=204, + title="Profile updated", + message="Your profile was updated", + ) + + +@blueprint.route("/credential/", methods=["PATCH"]) +@acl("any") +async def patch_credential(credential_hex_id: str): + try: + async with ClusterLock("credentials", current_app): + await components.users.patch_credential( + user_id=session["id"], + hex_id=credential_hex_id, + data=request.form_parsed, + ) + except ValidationError as e: + return validation_error(e.errors()) + + return trigger_notification( + level="success", + response_code=204, + title="Credential modified", + message="Credential was modified", + ) + + +@blueprint.route("/credential/", methods=["DELETE"]) +@acl("any") +async def delete_credential(credential_hex_id: str): + try: + async with ClusterLock(["credentials", "users"], current_app): + await components.users.delete_credential( + user_id=session["id"], hex_id=credential_hex_id + ) + except ValidationError as e: + return validation_error(e.errors()) + + return trigger_notification( + level="success", + response_code=204, + title="Credential deleted", + message="Credential was removed", + ) diff --git a/components/web/blueprints/root.py b/components/web/blueprints/root.py new file mode 100644 index 0000000..07930b7 --- /dev/null +++ b/components/web/blueprints/root.py @@ -0,0 +1,46 @@ +import asyncio +import json +import os + +from components.web.utils import * +from components.utils import is_path_within_cwd +from components.utils.datetimes import ( + last_modified_http, + parse_last_modified_http, + ntime_utc_now, +) + +blueprint = Blueprint("main", __name__, url_prefix="/") + + +@blueprint.route("/") +async def root(): + if session.get("id"): + return redirect(url_for("profile.user_profile_get")) + + session_clear() + return await render_template("auth/authenticate.html") + + +@blueprint.route("/logout", methods=["POST", "GET"]) +async def logout(): + session_clear() + return ("", 200, {"HX-Redirect": "/"}) + + +@blueprint.websocket("/ws") +@websocket_acl("any") +async def ws(): + while True: + await websocket.send( + f'
🟢
' + ) + data = await websocket.receive() + try: + data_dict = json.loads(data) + if "path" in data_dict: + IN_MEMORY_DB["WS_CONNECTIONS"][session["login"]][ + websocket._get_current_object() + ]["path"] = data_dict["path"] + except: + pass diff --git a/components/web/blueprints/system.py b/components/web/blueprints/system.py new file mode 100644 index 0000000..e9b9668 --- /dev/null +++ b/components/web/blueprints/system.py @@ -0,0 +1,302 @@ +import asyncio +import components.system +import fileinput +import json +import os + +from components.models.system import SystemSettings, UpdateSystemSettings +from components.utils import batch, expire_key +from components.utils.datetimes import datetime, ntime_utc_now +from components.web.utils import * + +blueprint = Blueprint("system", __name__, url_prefix="/system") +log_lock = asyncio.Lock() + + +@blueprint.context_processor +def load_context(): + from components.cluster.cluster import cluster + + context = dict() + context["schemas"] = {"system_settings": SystemSettings.model_json_schema()} + + return context + + +@blueprint.route("/cluster/update-status", methods=["POST"]) +@acl("system") +async def cluster_status_update(): + ticket, receivers = await cluster.send_command("STATUS", "*") + async with cluster.receiving: + await cluster.await_receivers(ticket, receivers, raise_err=False) + + return await status() + + +@blueprint.route("/cluster/db/enforce-updates", methods=["POST"]) +@acl("system") +async def cluster_db_enforce_updates(): + toggle = request.form_parsed.get("toggle", "off") + if toggle == "on": + if not IN_MEMORY_DB.get("ENFORCE_DBUPDATE", False): + IN_MEMORY_DB["ENFORCE_DBUPDATE"] = ntime_utc_now() + current_app.add_background_task( + expire_key, + IN_MEMORY_DB, + "ENFORCE_DBUPDATE", + defaults.CLUSTER_ENFORCE_DBUPDATE_TIMEOUT, + ) + await ws_htmx( + "_system", + "beforeend", + """""", + ) + + return trigger_notification( + level="success", + response_code=204, + title="Activated", + message="Enforced database updates are enabled", + ) + else: + return trigger_notification( + level="warning", + response_code=409, + title="Already active", + message="Enforced database updates are already enabled", + ) + elif toggle == "off": + IN_MEMORY_DB["ENFORCE_DBUPDATE"] = False + await ws_htmx( + "_system", + "beforeend", + '", + ) + return trigger_notification( + level="success", + response_code=204, + title="Deactivated", + message="Enforced transaction mode is disabled", + ) + + +@blueprint.route("/status", methods=["GET"]) +@acl("system") +async def status(): + status = { + "ENFORCE_DBUPDATE": IN_MEMORY_DB.get("ENFORCE_DBUPDATE", False), + "CLUSTER_PEERS_REMOTE_PEERS": cluster.peers.remotes, + "CLUSTER_PEERS_LOCAL": cluster.peers.local, + } + return await render_template("system/status.html", data={"status": status}) + + +@blueprint.route("/settings", methods=["PATCH"]) +@acl("system") +async def write_settings(): + try: + UpdateSystemSettingsModel = UpdateSystemSettings.parse_obj(request.form_parsed) + except ValidationError as e: + return validation_error(e.errors()) + + async with TinyDB(**TINYDB_PARAMS) as db: + db.table("system_settings").remove(doc_ids=[1]) + db.table("system_settings").insert( + Document(UpdateSystemSettingsModel.dict(), doc_id=1) + ) + + return trigger_notification( + level="success", + response_code=204, + title="Settings updated", + message="System settings were updated", + ) + + +@blueprint.route("/settings") +@acl("system") +async def settings(): + try: + settings = await components.system.get_system_settings() + except ValidationError as e: + return validation_error(e.errors()) + + return await render_template("system/settings.html", settings=settings) + + +@blueprint.route("/logs") +@blueprint.route("/logs/search", methods=["POST"]) +@acl("system") +async def cluster_logs(): + try: + ( + search_model, + page, + page_size, + sort_attr, + sort_reverse, + filters, + ) = table_search_helper( + request.form_parsed, + "system_logs", + default_sort_attr="record.time.repr", + default_sort_reverse=True, + ) + except ValidationError as e: + return validation_error(e.errors()) + + if request.method == "POST": + _logs = [] + async with log_lock: + parser_failed = False + + with fileinput.input( + components.system.list_application_log_files(), encoding="utf-8" + ) as f: + for line in f: + if search_model.q in line: + try: + _logs.append(json.loads(line.strip())) + except json.decoder.JSONDecodeError: + parser_failed = True + os.unlink(f.filename()) + f.nextfile() + + if parser_failed: + return trigger_notification( + level="user", + response_code=204, + title="Full refresh", + message="Logs rotated, requesting full refresh...", + additional_triggers={"forceRefresh": ""}, + duration=1000, + ) + + def system_logs_sort_func(sort_attr): + if sort_attr == "text": + return lambda d: ( + d["text"], + datetime.fromisoformat(d["record"]["time"]["repr"]).timestamp(), + ) + elif sort_attr == "record.level.no": + return lambda d: ( + d["record"]["level"]["no"], + datetime.fromisoformat(d["record"]["time"]["repr"]).timestamp(), + ) + else: # fallback to "record.time.repr" + return lambda d: datetime.fromisoformat( + d["record"]["time"]["repr"] + ).timestamp() + + log_pages = [ + m + for m in batch( + sorted( + _logs, + key=system_logs_sort_func(sort_attr), + reverse=sort_reverse, + ), + page_size, + ) + ] + + try: + log_pages[page - 1] + except IndexError: + page = len(log_pages) + + system_logs = log_pages[page - 1] if page else log_pages + + return await render_template( + "system/includes/logs/table_body.html", + data={ + "logs": system_logs, + "page_size": page_size, + "page": page, + "pages": len(log_pages), + "elements": len(_logs), + }, + ) + else: + return await render_template("system/logs.html") + + +@blueprint.route("/logs/refresh") +@acl("system") +async def refresh_cluster_logs(): + await ws_htmx( + session["login"], + "beforeend", + '", + "/system/logs", + ) + + if not IN_MEMORY_DB.get("application_logs_refresh") or request.args.get("force"): + IN_MEMORY_DB["application_logs_refresh"] = ntime_utc_now() + + current_app.add_background_task( + expire_key, + IN_MEMORY_DB, + "application_logs_refresh", + defaults.CLUSTER_LOGS_REFRESH_AFTER, + ) + + async with log_lock: + async with ClusterLock("files", current_app): + for peer in cluster.peers.get_established(): + if not peer in IN_MEMORY_DB["APP_LOGS_FULL_PULL"]: + IN_MEMORY_DB["APP_LOGS_FULL_PULL"][peer] = True + current_app.add_background_task( + expire_key, + IN_MEMORY_DB["APP_LOGS_FULL_PULL"], + peer, + 36000, + ) + start = 0 + else: + start = -1 + file_path = f"peer_files/{peer}/logs/application.log" + if os.path.exists(file_path) and os.path.getsize(file_path) > ( + 5 * 1024 * 1024 + ): + start = 0 + + await cluster.request_files("logs/application.log", peer, start, -1) + + missing_peers = ", ".join(cluster.peers.get_offline_peers()) + + if missing_peers: + await ws_htmx( + session["login"], + "beforeend", + '", + "/system/logs", + ) + + refresh_ago = round(ntime_utc_now() - IN_MEMORY_DB["application_logs_refresh"]) + + await ws_htmx( + session["login"], + "beforeend", + '', + "/system/logs", + ) + + return "", 204 diff --git a/components/web/blueprints/users.py b/components/web/blueprints/users.py new file mode 100644 index 0000000..a4b3afe --- /dev/null +++ b/components/web/blueprints/users.py @@ -0,0 +1,211 @@ +import components.users +from components.utils import batch, ensure_list +from components.database import IN_MEMORY_DB +from components.models.users import UserGroups +from components.web.utils import * + + +blueprint = Blueprint("users", __name__, url_prefix="/system/users") + + +@blueprint.context_processor +def load_context(): + from components.models.users import UserProfile + + context = dict() + context["schemas"] = {"user_profile": UserProfile.model_json_schema()} + return context + + +@blueprint.route("/groups", methods=["PATCH"]) +@acl("system") +async def user_group(): + try: + request_data = UserGroups.parse_obj(request.form_parsed) + + assigned_to = [ + u + for u in await components.users.search(name="", join_credentials=False) + if request_data.name in u.groups + ] + + assign_to = [] + for user_id in request_data.members: + assign_to.append( + await components.users.get(user_id=user_id, join_credentials=False) + ) + + _all = assigned_to + assign_to + + async with ClusterLock(["users", "credentials"], current_app): + for user in _all: + user_dict = user.model_dump(mode="json") + if request_data.name in user_dict["groups"]: + user_dict["groups"].remove(request_data.name) + + if ( + request_data.new_name not in user_dict["groups"] + and user in assign_to + ): + user_dict["groups"].append(request_data.new_name) + + await components.users.patch(user_id=user.id, data=user_dict) + + return "", 204 + + except ValidationError as e: + return validation_error(e.errors()) + except ValueError as e: + name, message = e.args + return validation_error([{"loc": [name], "msg": message}]) + + +@blueprint.route("/") +@acl("system") +async def get_user(user_id: str): + try: + user = await components.users.get(user_id=user_id) + except ValidationError as e: + return validation_error(e.errors()) + except ValueError as e: + name, message = e.args + return validation_error([{"loc": [name], "msg": message}]) + + return await render_or_json( + "system/includes/users/row.html", request.headers, user=user.dict() + ) + + +@blueprint.route("/") +@blueprint.route("/search", methods=["POST"]) +@acl("system") +@formoptions(["users"]) +async def get_users(): + try: + ( + search_model, + page, + page_size, + sort_attr, + sort_reverse, + filters, + ) = table_search_helper(request.form_parsed, "users", default_sort_attr="login") + except ValidationError as e: + return validation_error(e.errors()) + + if request.method == "POST": + matched_users = [ + m.dict() for m in await components.users.search(name=search_model.q) + ] + + user_pages = [ + m + for m in batch( + sorted( + matched_users, + key=lambda x: x.get(sort_attr, "id"), + reverse=sort_reverse, + ), + page_size, + ) + ] + + try: + user_pages[page - 1] + except IndexError: + page = len(user_pages) + + users = user_pages[page - 1] if page else user_pages + + return await render_template( + "system/includes/users/table_body.html", + data={ + "users": users, + "page_size": page_size, + "page": page, + "pages": len(user_pages), + "elements": len(matched_users), + }, + ) + else: + return await render_template("system/users.html", data={}) + + +@blueprint.route("/delete", methods=["POST"]) +@blueprint.route("/", methods=["DELETE"]) +@acl("system") +async def delete_user(user_id: str | None = None): + if request.method == "POST": + user_ids = request.form_parsed.get("id") + + try: + async with ClusterLock(["users", "credentials"], current_app): + for user_id in ensure_list(user_ids): + await components.users.delete(user_id=user_id) + + except ValidationError as e: + return validation_error(e.errors()) + except ValueError as e: + name, message = e.args + return validation_error([{"loc": [name], "msg": message}]) + + return trigger_notification( + level="success", + response_code=204, + title="User removed", + message=f"{len(ensure_list(user_ids))} user{'s' if len(ensure_list(user_ids)) > 1 else ''} removed", + ) + + +@blueprint.route("//credential/", methods=["PATCH"]) +@acl("system") +async def patch_user_credential(user_id: str, hex_id: str): + try: + async with ClusterLock("credentials", current_app): + await components.users.patch_credential( + user_id=user_id, + hex_id=hex_id, + data=request.form_parsed, + ) + except ValidationError as e: + return validation_error(e.errors()) + except ValueError as e: + name, message = e.args + return validation_error([{"loc": [name], "msg": message}]) + + return trigger_notification( + level="success", + response_code=204, + title="Credential modified", + message="Credential was modified", + ) + + +@blueprint.route("/patch", methods=["POST"]) +@blueprint.route("/", methods=["PATCH"]) +@acl("system") +async def patch_user(user_id: str | None = None): + try: + if not user_id: + user_id = request.form_parsed.get("id") + + async with ClusterLock(["users", "credentials"], current_app): + await components.users.patch(user_id=user_id, data=request.form_parsed) + await components.users.patch_profile( + user_id=user_id, data=request.form_parsed.get("profile", {}) + ) + + except ValidationError as e: + return validation_error(e.errors()) + except ValueError as e: + name, message = e.args + return validation_error([{"loc": [name], "msg": message}]) + + IN_MEMORY_DB["SESSION_VALIDATED"].pop(user_id, None) + + return trigger_notification( + level="success", + response_code=204, + title="User modified", + message=f"User was updated", + ) diff --git a/components/web/static_files/.gitignore b/components/web/static_files/.gitignore new file mode 100644 index 0000000..c97f963 --- /dev/null +++ b/components/web/static_files/.gitignore @@ -0,0 +1 @@ +*.sh diff --git a/components/web/static_files/css/.gitignore b/components/web/static_files/css/.gitignore new file mode 100644 index 0000000..ae47b83 --- /dev/null +++ b/components/web/static_files/css/.gitignore @@ -0,0 +1,4 @@ +* +!pico-custom.scss +!pico-custom.css +!.gitignore diff --git a/components/web/static_files/css/pico-custom.css b/components/web/static_files/css/pico-custom.css new file mode 100644 index 0000000..446e510 --- /dev/null +++ b/components/web/static_files/css/pico-custom.css @@ -0,0 +1,9363 @@ +@charset "UTF-8"; +/*! + * Pico CSS ✨ v2.0.6 (https://picocss.com) + * Copyright 2019-2024 - Licensed under MIT + */ +/** + * Styles + */ +:root { + --pico-font-family-emoji: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --pico-font-family-sans-serif: system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, Helvetica, Arial, "Helvetica Neue", sans-serif, var(--pico-font-family-emoji); + --pico-font-family-monospace: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace, var(--pico-font-family-emoji); + --pico-font-family: var(--pico-font-family-sans-serif); + --pico-line-height: 1.5; + --pico-font-weight: 400; + --pico-font-size: 100%; + --pico-text-underline-offset: 0.1rem; + --pico-border-radius: 0.25rem; + --pico-border-width: 0.0625rem; + --pico-outline-width: 0.125rem; + --pico-transition: 0.2s ease-in-out; + --pico-spacing: 1rem; + --pico-typography-spacing-vertical: 1rem; + --pico-block-spacing-vertical: var(--pico-spacing); + --pico-block-spacing-horizontal: var(--pico-spacing); + --pico-grid-column-gap: var(--pico-spacing); + --pico-grid-row-gap: var(--pico-spacing); + --pico-form-element-spacing-vertical: 0.75rem; + --pico-form-element-spacing-horizontal: 1rem; + --pico-group-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-group-box-shadow-focus-with-button: 0 0 0 var(--pico-outline-width) var(--pico-primary-focus); + --pico-group-box-shadow-focus-with-input: 0 0 0 0.0625rem var(--pico-form-element-border-color); + --pico-modal-overlay-backdrop-filter: blur(0.375rem); + --pico-nav-element-spacing-vertical: 1rem; + --pico-nav-element-spacing-horizontal: 0.5rem; + --pico-nav-link-spacing-vertical: 0.5rem; + --pico-nav-link-spacing-horizontal: 0.5rem; + --pico-nav-breadcrumb-divider: ">"; + --pico-icon-checkbox: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); + --pico-icon-minus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E"); + --pico-icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); + --pico-icon-date: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E"); + --pico-icon-time: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E"); + --pico-icon-search: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E"); + --pico-icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E"); + --pico-icon-loading: url("data:image/svg+xml,%3Csvg fill='none' height='24' width='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' %3E%3Cstyle%3E g %7B animation: rotate 2s linear infinite; transform-origin: center center; %7D circle %7B stroke-dasharray: 75,100; stroke-dashoffset: -5; animation: dash 1.5s ease-in-out infinite; stroke-linecap: round; %7D @keyframes rotate %7B 0%25 %7B transform: rotate(0deg); %7D 100%25 %7B transform: rotate(360deg); %7D %7D @keyframes dash %7B 0%25 %7B stroke-dasharray: 1,100; stroke-dashoffset: 0; %7D 50%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -17.5; %7D 100%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -62; %7D %7D %3C/style%3E%3Cg%3E%3Ccircle cx='12' cy='12' r='10' fill='none' stroke='rgb(136, 145, 164)' stroke-width='4' /%3E%3C/g%3E%3C/svg%3E"); +} + +@media (min-width: 576px) { + body > header, + body > main, + body > footer, + section { + --pico-block-spacing-vertical: calc(var(--pico-spacing) * 1.25); + } +} +@media (min-width: 768px) { + body > header, + body > main, + body > footer, + section { + --pico-block-spacing-vertical: calc(var(--pico-spacing) * 1.5); + } +} +@media (min-width: 1024px) { + body > header, + body > main, + body > footer, + section { + --pico-block-spacing-vertical: calc(var(--pico-spacing) * 1.75); + } +} +@media (min-width: 1280px) { + body > header, + body > main, + body > footer, + section { + --pico-block-spacing-vertical: calc(var(--pico-spacing) * 2); + } +} +@media (min-width: 1536px) { + body > header, + body > main, + body > footer, + section { + --pico-block-spacing-vertical: calc(var(--pico-spacing) * 2.25); + } +} + +@media (min-width: 576px) { + article { + --pico-block-spacing-vertical: calc(var(--pico-spacing) * 1.25); + --pico-block-spacing-horizontal: calc(var(--pico-spacing) * 1.25); + } +} +@media (min-width: 768px) { + article { + --pico-block-spacing-vertical: calc(var(--pico-spacing) * 1.5); + --pico-block-spacing-horizontal: calc(var(--pico-spacing) * 1.5); + } +} +@media (min-width: 1024px) { + article { + --pico-block-spacing-vertical: calc(var(--pico-spacing) * 1.75); + --pico-block-spacing-horizontal: calc(var(--pico-spacing) * 1.75); + } +} +@media (min-width: 1280px) { + article { + --pico-block-spacing-vertical: calc(var(--pico-spacing) * 2); + --pico-block-spacing-horizontal: calc(var(--pico-spacing) * 2); + } +} +@media (min-width: 1536px) { + article { + --pico-block-spacing-vertical: calc(var(--pico-spacing) * 2.25); + --pico-block-spacing-horizontal: calc(var(--pico-spacing) * 2.25); + } +} + +a { + --pico-text-decoration: underline; +} +a.secondary, a.contrast { + --pico-text-decoration: underline; +} + +small { + --pico-font-size: 0.875em; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + --pico-font-weight: 700; +} + +h1 { + --pico-font-size: 2rem; + --pico-line-height: 1.125; + --pico-typography-spacing-top: 3rem; +} + +h2 { + --pico-font-size: 1.75rem; + --pico-line-height: 1.15; + --pico-typography-spacing-top: 2.625rem; +} + +h3 { + --pico-font-size: 1.5rem; + --pico-line-height: 1.175; + --pico-typography-spacing-top: 2.25rem; +} + +h4 { + --pico-font-size: 1.25rem; + --pico-line-height: 1.2; + --pico-typography-spacing-top: 1.874rem; +} + +h5 { + --pico-font-size: 1.125rem; + --pico-line-height: 1.225; + --pico-typography-spacing-top: 1.6875rem; +} + +h6 { + --pico-font-size: 1rem; + --pico-line-height: 1.25; + --pico-typography-spacing-top: 1.5rem; +} + +thead th, +thead td, +tfoot th, +tfoot td { + --pico-font-weight: 600; + --pico-border-width: 0.1875rem; +} + +pre, +code, +kbd, +samp { + --pico-font-family: var(--pico-font-family-monospace); +} + +kbd { + --pico-font-weight: bolder; +} + +input:not([type=submit], +[type=button], +[type=reset], +[type=checkbox], +[type=radio], +[type=file]), +:where(select, textarea) { + --pico-outline-width: 0.0625rem; +} + +[type=search] { + --pico-border-radius: 5rem; +} + +[type=checkbox], +[type=radio] { + --pico-border-width: 0.125rem; +} + +[type=checkbox][role=switch] { + --pico-border-width: 0.1875rem; +} + +details.dropdown summary:not([role=button]) { + --pico-outline-width: 0.0625rem; +} + +nav details.dropdown summary:focus-visible { + --pico-outline-width: 0.125rem; +} + +[role=search] { + --pico-border-radius: 5rem; +} + +[role=search]:has(button.secondary:focus, +[type=submit].secondary:focus, +[type=button].secondary:focus, +[role=button].secondary:focus), +[role=group]:has(button.secondary:focus, +[type=submit].secondary:focus, +[type=button].secondary:focus, +[role=button].secondary:focus) { + --pico-group-box-shadow-focus-with-button: 0 0 0 var(--pico-outline-width) var(--pico-secondary-focus); +} +[role=search]:has(button.contrast:focus, +[type=submit].contrast:focus, +[type=button].contrast:focus, +[role=button].contrast:focus), +[role=group]:has(button.contrast:focus, +[type=submit].contrast:focus, +[type=button].contrast:focus, +[role=button].contrast:focus) { + --pico-group-box-shadow-focus-with-button: 0 0 0 var(--pico-outline-width) var(--pico-contrast-focus); +} +[role=search] button, +[role=search] [type=submit], +[role=search] [type=button], +[role=search] [role=button], +[role=group] button, +[role=group] [type=submit], +[role=group] [type=button], +[role=group] [role=button] { + --pico-form-element-spacing-horizontal: 2rem; +} + +details summary[role=button]:not(.outline)::after { + filter: brightness(0) invert(1); +} + +[aria-busy=true]:not(input, select, textarea):is(button, [type=submit], [type=button], [type=reset], [role=button]):not(.outline)::before { + filter: brightness(0) invert(1); +} + +/** + * Color schemes + */ +[data-theme=light], +:root:not([data-theme=dark]) { + --pico-background-color: #fff; + --pico-color: #373c44; + --pico-text-selection-color: rgba(129, 145, 181, 0.25); + --pico-muted-color: #646b79; + --pico-muted-border-color: #e7eaf0; + --pico-primary: #5d6b89; + --pico-primary-background: #525f7a; + --pico-primary-border: var(--pico-primary-background); + --pico-primary-underline: rgba(93, 107, 137, 0.5); + --pico-primary-hover: #48536b; + --pico-primary-hover-background: #48536b; + --pico-primary-hover-border: var(--pico-primary-hover-background); + --pico-primary-hover-underline: var(--pico-primary-hover); + --pico-primary-focus: rgba(129, 145, 181, 0.5); + --pico-primary-inverse: #fff; + --pico-secondary: #5d6b89; + --pico-secondary-background: #525f7a; + --pico-secondary-border: var(--pico-secondary-background); + --pico-secondary-underline: rgba(93, 107, 137, 0.5); + --pico-secondary-hover: #48536b; + --pico-secondary-hover-background: #48536b; + --pico-secondary-hover-border: var(--pico-secondary-hover-background); + --pico-secondary-hover-underline: var(--pico-secondary-hover); + --pico-secondary-focus: rgba(93, 107, 137, 0.25); + --pico-secondary-inverse: #fff; + --pico-contrast: #181c25; + --pico-contrast-background: #181c25; + --pico-contrast-border: var(--pico-contrast-background); + --pico-contrast-underline: rgba(24, 28, 37, 0.5); + --pico-contrast-hover: #000; + --pico-contrast-hover-background: #000; + --pico-contrast-hover-border: var(--pico-contrast-hover-background); + --pico-contrast-hover-underline: var(--pico-secondary-hover); + --pico-contrast-focus: rgba(93, 107, 137, 0.25); + --pico-contrast-inverse: #fff; + --pico-box-shadow: 0.0145rem 0.029rem 0.174rem rgba(129, 145, 181, 0.01698), 0.0335rem 0.067rem 0.402rem rgba(129, 145, 181, 0.024), 0.0625rem 0.125rem 0.75rem rgba(129, 145, 181, 0.03), 0.1125rem 0.225rem 1.35rem rgba(129, 145, 181, 0.036), 0.2085rem 0.417rem 2.502rem rgba(129, 145, 181, 0.04302), 0.5rem 1rem 6rem rgba(129, 145, 181, 0.06), 0 0 0 0.0625rem rgba(129, 145, 181, 0.015); + --pico-h1-color: #2d3138; + --pico-h2-color: #373c44; + --pico-h3-color: #424751; + --pico-h4-color: #4d535e; + --pico-h5-color: #5c6370; + --pico-h6-color: #646b79; + --pico-mark-background-color: #fde7c0; + --pico-mark-color: #0f1114; + --pico-ins-color: #1d6a54; + --pico-del-color: #883935; + --pico-blockquote-border-color: var(--pico-muted-border-color); + --pico-blockquote-footer-color: var(--pico-muted-color); + --pico-button-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-table-border-color: var(--pico-muted-border-color); + --pico-table-row-stripped-background-color: rgba(111, 120, 135, 0.0375); + --pico-code-background-color: #f3f5f7; + --pico-code-color: #646b79; + --pico-code-kbd-background-color: var(--pico-color); + --pico-code-kbd-color: var(--pico-background-color); + --pico-form-element-background-color: #fbfcfc; + --pico-form-element-selected-background-color: #dfe3eb; + --pico-form-element-border-color: #cfd5e2; + --pico-form-element-color: #23262c; + --pico-form-element-placeholder-color: var(--pico-muted-color); + --pico-form-element-active-background-color: #fff; + --pico-form-element-active-border-color: var(--pico-primary-border); + --pico-form-element-focus-color: var(--pico-primary-border); + --pico-form-element-disabled-opacity: 0.5; + --pico-form-element-invalid-border-color: #b86a6b; + --pico-form-element-invalid-active-border-color: #c84f48; + --pico-form-element-invalid-focus-color: var(--pico-form-element-invalid-active-border-color); + --pico-form-element-valid-border-color: #4c9b8a; + --pico-form-element-valid-active-border-color: #279977; + --pico-form-element-valid-focus-color: var(--pico-form-element-valid-active-border-color); + --pico-switch-background-color: #bfc7d9; + --pico-switch-checked-background-color: var(--pico-primary-background); + --pico-switch-color: #fff; + --pico-switch-thumb-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-range-border-color: #dfe3eb; + --pico-range-active-border-color: #bfc7d9; + --pico-range-thumb-border-color: var(--pico-background-color); + --pico-range-thumb-color: var(--pico-secondary-background); + --pico-range-thumb-active-color: var(--pico-primary-background); + --pico-accordion-border-color: var(--pico-muted-border-color); + --pico-accordion-active-summary-color: var(--pico-primary-hover); + --pico-accordion-close-summary-color: var(--pico-color); + --pico-accordion-open-summary-color: var(--pico-muted-color); + --pico-card-background-color: var(--pico-background-color); + --pico-card-border-color: var(--pico-muted-border-color); + --pico-card-box-shadow: var(--pico-box-shadow); + --pico-card-sectioning-background-color: #fbfcfc; + --pico-dropdown-background-color: #fff; + --pico-dropdown-border-color: #eff1f4; + --pico-dropdown-box-shadow: var(--pico-box-shadow); + --pico-dropdown-color: var(--pico-color); + --pico-dropdown-hover-background-color: #eff1f4; + --pico-loading-spinner-opacity: 0.5; + --pico-modal-overlay-background-color: rgba(232, 234, 237, 0.75); + --pico-progress-background-color: #dfe3eb; + --pico-progress-color: var(--pico-primary-background); + --pico-tooltip-background-color: var(--pico-contrast-background); + --pico-tooltip-color: var(--pico-contrast-inverse); + --pico-icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(76, 155, 138)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); + --pico-icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(200, 79, 72)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); + color-scheme: light; +} +[data-theme=light] input:is([type=submit], +[type=button], +[type=reset], +[type=checkbox], +[type=radio], +[type=file]), +:root:not([data-theme=dark]) input:is([type=submit], +[type=button], +[type=reset], +[type=checkbox], +[type=radio], +[type=file]) { + --pico-form-element-focus-color: var(--pico-primary-focus); +} + +@media only screen and (prefers-color-scheme: dark) { + :root:not([data-theme]) { + --pico-background-color: #13171f; + --pico-color: #c2c7d0; + --pico-text-selection-color: rgba(144, 158, 190, 0.1875); + --pico-muted-color: #7b8495; + --pico-muted-border-color: #202632; + --pico-primary: #909ebe; + --pico-primary-background: #525f7a; + --pico-primary-border: var(--pico-primary-background); + --pico-primary-underline: rgba(144, 158, 190, 0.5); + --pico-primary-hover: #b0b9d0; + --pico-primary-hover-background: #5d6b89; + --pico-primary-hover-border: var(--pico-primary-hover-background); + --pico-primary-hover-underline: var(--pico-primary-hover); + --pico-primary-focus: rgba(144, 158, 190, 0.375); + --pico-primary-inverse: #fff; + --pico-secondary: #969eaf; + --pico-secondary-background: #525f7a; + --pico-secondary-border: var(--pico-secondary-background); + --pico-secondary-underline: rgba(150, 158, 175, 0.5); + --pico-secondary-hover: #b3b9c5; + --pico-secondary-hover-background: #5d6b89; + --pico-secondary-hover-border: var(--pico-secondary-hover-background); + --pico-secondary-hover-underline: var(--pico-secondary-hover); + --pico-secondary-focus: rgba(144, 158, 190, 0.25); + --pico-secondary-inverse: #fff; + --pico-contrast: #dfe3eb; + --pico-contrast-background: #eff1f4; + --pico-contrast-border: var(--pico-contrast-background); + --pico-contrast-underline: rgba(223, 227, 235, 0.5); + --pico-contrast-hover: #fff; + --pico-contrast-hover-background: #fff; + --pico-contrast-hover-border: var(--pico-contrast-hover-background); + --pico-contrast-hover-underline: var(--pico-contrast-hover); + --pico-contrast-focus: rgba(207, 213, 226, 0.25); + --pico-contrast-inverse: #000; + --pico-box-shadow: 0.0145rem 0.029rem 0.174rem rgba(7, 9, 12, 0.01698), 0.0335rem 0.067rem 0.402rem rgba(7, 9, 12, 0.024), 0.0625rem 0.125rem 0.75rem rgba(7, 9, 12, 0.03), 0.1125rem 0.225rem 1.35rem rgba(7, 9, 12, 0.036), 0.2085rem 0.417rem 2.502rem rgba(7, 9, 12, 0.04302), 0.5rem 1rem 6rem rgba(7, 9, 12, 0.06), 0 0 0 0.0625rem rgba(7, 9, 12, 0.015); + --pico-h1-color: #f0f1f3; + --pico-h2-color: #e0e3e7; + --pico-h3-color: #c2c7d0; + --pico-h4-color: #b3b9c5; + --pico-h5-color: #a4acba; + --pico-h6-color: #8891a4; + --pico-mark-background-color: #014063; + --pico-mark-color: #fff; + --pico-ins-color: #62af9a; + --pico-del-color: #ce7e7b; + --pico-blockquote-border-color: var(--pico-muted-border-color); + --pico-blockquote-footer-color: var(--pico-muted-color); + --pico-button-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-table-border-color: var(--pico-muted-border-color); + --pico-table-row-stripped-background-color: rgba(111, 120, 135, 0.0375); + --pico-code-background-color: #1a1f28; + --pico-code-color: #8891a4; + --pico-code-kbd-background-color: var(--pico-color); + --pico-code-kbd-color: var(--pico-background-color); + --pico-form-element-background-color: #1c212c; + --pico-form-element-selected-background-color: #2a3140; + --pico-form-element-border-color: #2a3140; + --pico-form-element-color: #e0e3e7; + --pico-form-element-placeholder-color: #8891a4; + --pico-form-element-active-background-color: #1a1f28; + --pico-form-element-active-border-color: var(--pico-primary-border); + --pico-form-element-focus-color: var(--pico-primary-border); + --pico-form-element-disabled-opacity: 0.5; + --pico-form-element-invalid-border-color: #964a50; + --pico-form-element-invalid-active-border-color: #b7403b; + --pico-form-element-invalid-focus-color: var(--pico-form-element-invalid-active-border-color); + --pico-form-element-valid-border-color: #2a7b6f; + --pico-form-element-valid-active-border-color: #16896a; + --pico-form-element-valid-focus-color: var(--pico-form-element-valid-active-border-color); + --pico-switch-background-color: #333c4e; + --pico-switch-checked-background-color: var(--pico-primary-background); + --pico-switch-color: #fff; + --pico-switch-thumb-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-range-border-color: #202632; + --pico-range-active-border-color: #2a3140; + --pico-range-thumb-border-color: var(--pico-background-color); + --pico-range-thumb-color: var(--pico-secondary-background); + --pico-range-thumb-active-color: var(--pico-primary-background); + --pico-accordion-border-color: var(--pico-muted-border-color); + --pico-accordion-active-summary-color: var(--pico-primary-hover); + --pico-accordion-close-summary-color: var(--pico-color); + --pico-accordion-open-summary-color: var(--pico-muted-color); + --pico-card-background-color: #181c25; + --pico-card-border-color: var(--pico-card-background-color); + --pico-card-box-shadow: var(--pico-box-shadow); + --pico-card-sectioning-background-color: #1a1f28; + --pico-dropdown-background-color: #181c25; + --pico-dropdown-border-color: #202632; + --pico-dropdown-box-shadow: var(--pico-box-shadow); + --pico-dropdown-color: var(--pico-color); + --pico-dropdown-hover-background-color: #202632; + --pico-loading-spinner-opacity: 0.5; + --pico-modal-overlay-background-color: rgba(8, 9, 10, 0.75); + --pico-progress-background-color: #202632; + --pico-progress-color: var(--pico-primary-background); + --pico-tooltip-background-color: var(--pico-contrast-background); + --pico-tooltip-color: var(--pico-contrast-inverse); + --pico-icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); + --pico-icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(150, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); + color-scheme: dark; + } + :root:not([data-theme]) input:is([type=submit], + [type=button], + [type=reset], + [type=checkbox], + [type=radio], + [type=file]) { + --pico-form-element-focus-color: var(--pico-primary-focus); + } + :root:not([data-theme]) details summary[role=button].contrast:not(.outline)::after { + filter: brightness(0); + } + :root:not([data-theme]) [aria-busy=true]:not(input, select, textarea).contrast:is(button, + [type=submit], + [type=button], + [type=reset], + [role=button]):not(.outline)::before { + filter: brightness(0); + } +} +[data-theme=dark] { + --pico-background-color: #13171f; + --pico-color: #c2c7d0; + --pico-text-selection-color: rgba(144, 158, 190, 0.1875); + --pico-muted-color: #7b8495; + --pico-muted-border-color: #202632; + --pico-primary: #909ebe; + --pico-primary-background: #525f7a; + --pico-primary-border: var(--pico-primary-background); + --pico-primary-underline: rgba(144, 158, 190, 0.5); + --pico-primary-hover: #b0b9d0; + --pico-primary-hover-background: #5d6b89; + --pico-primary-hover-border: var(--pico-primary-hover-background); + --pico-primary-hover-underline: var(--pico-primary-hover); + --pico-primary-focus: rgba(144, 158, 190, 0.375); + --pico-primary-inverse: #fff; + --pico-secondary: #969eaf; + --pico-secondary-background: #525f7a; + --pico-secondary-border: var(--pico-secondary-background); + --pico-secondary-underline: rgba(150, 158, 175, 0.5); + --pico-secondary-hover: #b3b9c5; + --pico-secondary-hover-background: #5d6b89; + --pico-secondary-hover-border: var(--pico-secondary-hover-background); + --pico-secondary-hover-underline: var(--pico-secondary-hover); + --pico-secondary-focus: rgba(144, 158, 190, 0.25); + --pico-secondary-inverse: #fff; + --pico-contrast: #dfe3eb; + --pico-contrast-background: #eff1f4; + --pico-contrast-border: var(--pico-contrast-background); + --pico-contrast-underline: rgba(223, 227, 235, 0.5); + --pico-contrast-hover: #fff; + --pico-contrast-hover-background: #fff; + --pico-contrast-hover-border: var(--pico-contrast-hover-background); + --pico-contrast-hover-underline: var(--pico-contrast-hover); + --pico-contrast-focus: rgba(207, 213, 226, 0.25); + --pico-contrast-inverse: #000; + --pico-box-shadow: 0.0145rem 0.029rem 0.174rem rgba(7, 9, 12, 0.01698), 0.0335rem 0.067rem 0.402rem rgba(7, 9, 12, 0.024), 0.0625rem 0.125rem 0.75rem rgba(7, 9, 12, 0.03), 0.1125rem 0.225rem 1.35rem rgba(7, 9, 12, 0.036), 0.2085rem 0.417rem 2.502rem rgba(7, 9, 12, 0.04302), 0.5rem 1rem 6rem rgba(7, 9, 12, 0.06), 0 0 0 0.0625rem rgba(7, 9, 12, 0.015); + --pico-h1-color: #f0f1f3; + --pico-h2-color: #e0e3e7; + --pico-h3-color: #c2c7d0; + --pico-h4-color: #b3b9c5; + --pico-h5-color: #a4acba; + --pico-h6-color: #8891a4; + --pico-mark-background-color: #014063; + --pico-mark-color: #fff; + --pico-ins-color: #62af9a; + --pico-del-color: #ce7e7b; + --pico-blockquote-border-color: var(--pico-muted-border-color); + --pico-blockquote-footer-color: var(--pico-muted-color); + --pico-button-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-table-border-color: var(--pico-muted-border-color); + --pico-table-row-stripped-background-color: rgba(111, 120, 135, 0.0375); + --pico-code-background-color: #1a1f28; + --pico-code-color: #8891a4; + --pico-code-kbd-background-color: var(--pico-color); + --pico-code-kbd-color: var(--pico-background-color); + --pico-form-element-background-color: #1c212c; + --pico-form-element-selected-background-color: #2a3140; + --pico-form-element-border-color: #2a3140; + --pico-form-element-color: #e0e3e7; + --pico-form-element-placeholder-color: #8891a4; + --pico-form-element-active-background-color: #1a1f28; + --pico-form-element-active-border-color: var(--pico-primary-border); + --pico-form-element-focus-color: var(--pico-primary-border); + --pico-form-element-disabled-opacity: 0.5; + --pico-form-element-invalid-border-color: #964a50; + --pico-form-element-invalid-active-border-color: #b7403b; + --pico-form-element-invalid-focus-color: var(--pico-form-element-invalid-active-border-color); + --pico-form-element-valid-border-color: #2a7b6f; + --pico-form-element-valid-active-border-color: #16896a; + --pico-form-element-valid-focus-color: var(--pico-form-element-valid-active-border-color); + --pico-switch-background-color: #333c4e; + --pico-switch-checked-background-color: var(--pico-primary-background); + --pico-switch-color: #fff; + --pico-switch-thumb-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-range-border-color: #202632; + --pico-range-active-border-color: #2a3140; + --pico-range-thumb-border-color: var(--pico-background-color); + --pico-range-thumb-color: var(--pico-secondary-background); + --pico-range-thumb-active-color: var(--pico-primary-background); + --pico-accordion-border-color: var(--pico-muted-border-color); + --pico-accordion-active-summary-color: var(--pico-primary-hover); + --pico-accordion-close-summary-color: var(--pico-color); + --pico-accordion-open-summary-color: var(--pico-muted-color); + --pico-card-background-color: #181c25; + --pico-card-border-color: var(--pico-card-background-color); + --pico-card-box-shadow: var(--pico-box-shadow); + --pico-card-sectioning-background-color: #1a1f28; + --pico-dropdown-background-color: #181c25; + --pico-dropdown-border-color: #202632; + --pico-dropdown-box-shadow: var(--pico-box-shadow); + --pico-dropdown-color: var(--pico-color); + --pico-dropdown-hover-background-color: #202632; + --pico-loading-spinner-opacity: 0.5; + --pico-modal-overlay-background-color: rgba(8, 9, 10, 0.75); + --pico-progress-background-color: #202632; + --pico-progress-color: var(--pico-primary-background); + --pico-tooltip-background-color: var(--pico-contrast-background); + --pico-tooltip-color: var(--pico-contrast-inverse); + --pico-icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); + --pico-icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(150, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); + color-scheme: dark; +} +[data-theme=dark] input:is([type=submit], +[type=button], +[type=reset], +[type=checkbox], +[type=radio], +[type=file]) { + --pico-form-element-focus-color: var(--pico-primary-focus); +} +[data-theme=dark] details summary[role=button].contrast:not(.outline)::after { + filter: brightness(0); +} +[data-theme=dark] [aria-busy=true]:not(input, select, textarea).contrast:is(button, +[type=submit], +[type=button], +[type=reset], +[role=button]):not(.outline)::before { + filter: brightness(0); +} + +progress, +[type=checkbox], +[type=radio], +[type=range] { + accent-color: var(--pico-primary); +} + +/** + * Document + * Content-box & Responsive typography + */ +*, +*::before, +*::after { + box-sizing: border-box; + background-repeat: no-repeat; +} + +::before, +::after { + text-decoration: inherit; + vertical-align: inherit; +} + +:where(:root) { + -webkit-tap-highlight-color: transparent; + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; + background-color: var(--pico-background-color); + color: var(--pico-color); + font-weight: var(--pico-font-weight); + font-size: var(--pico-font-size); + line-height: var(--pico-line-height); + font-family: var(--pico-font-family); + text-underline-offset: var(--pico-text-underline-offset); + text-rendering: optimizeLegibility; + overflow-wrap: break-word; + tab-size: 4; +} + +/** + * Landmarks + */ +body { + width: 100%; + margin: 0; +} + +main { + display: block; +} + +body > header, +body > main, +body > footer { + width: 100%; + margin-right: auto; + margin-left: auto; + padding: var(--pico-block-spacing-vertical) var(--pico-block-spacing-horizontal); +} +@media (min-width: 576px) { + body > header, + body > main, + body > footer { + max-width: 95%; + padding-right: 0; + padding-left: 0; + } +} +@media (min-width: 768px) { + body > header, + body > main, + body > footer { + max-width: 95%; + } +} +@media (min-width: 1024px) { + body > header, + body > main, + body > footer { + max-width: 90%; + } +} +@media (min-width: 1280px) { + body > header, + body > main, + body > footer { + max-width: 90%; + } +} +@media (min-width: 1536px) { + body > header, + body > main, + body > footer { + max-width: 85%; + } +} + +/** + * Section + */ +section { + margin-bottom: var(--pico-block-spacing-vertical); +} + +/** + * Container + */ +.container, +.container-fluid { + width: 100%; + margin-right: auto; + margin-left: auto; + padding-right: var(--pico-spacing); + padding-left: var(--pico-spacing); +} + +@media (min-width: 576px) { + .container { + max-width: 95%; + padding-right: 0; + padding-left: 0; + } +} +@media (min-width: 768px) { + .container { + max-width: 95%; + } +} +@media (min-width: 1024px) { + .container { + max-width: 90%; + } +} +@media (min-width: 1280px) { + .container { + max-width: 90%; + } +} +@media (min-width: 1536px) { + .container { + max-width: 85%; + } +} + +/** + * Grid + * Minimal grid system with auto-layout columns + */ +.grid { + grid-column-gap: var(--pico-grid-column-gap); + grid-row-gap: var(--pico-grid-row-gap); + display: grid; + grid-template-columns: 1fr; +} +@media (min-width: 768px) { + .grid { + grid-template-columns: repeat(auto-fit, minmax(0%, 1fr)); + } +} +.grid > * { + min-width: 0; +} + +/** + * Overflow auto + */ +.overflow-auto { + overflow: auto; +} + +/** + * Typography + */ +b, +strong { + font-weight: bolder; +} + +sub, +sup { + position: relative; + font-size: 0.75em; + line-height: 0; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +address, +blockquote, +dl, +ol, +p, +pre, +table, +ul { + margin-top: 0; + margin-bottom: var(--pico-typography-spacing-vertical); + color: var(--pico-color); + font-style: normal; + font-weight: var(--pico-font-weight); +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin-top: 0; + margin-bottom: var(--pico-typography-spacing-vertical); + color: var(--pico-color); + font-weight: var(--pico-font-weight); + font-size: var(--pico-font-size); + line-height: var(--pico-line-height); + font-family: var(--pico-font-family); +} + +h1 { + --pico-color: var(--pico-h1-color); +} + +h2 { + --pico-color: var(--pico-h2-color); +} + +h3 { + --pico-color: var(--pico-h3-color); +} + +h4 { + --pico-color: var(--pico-h4-color); +} + +h5 { + --pico-color: var(--pico-h5-color); +} + +h6 { + --pico-color: var(--pico-h6-color); +} + +:where(article, address, blockquote, dl, figure, form, ol, p, pre, table, ul) ~ :is(h1, h2, h3, h4, h5, h6) { + margin-top: var(--pico-typography-spacing-top); +} + +p { + margin-bottom: var(--pico-typography-spacing-vertical); +} + +hgroup { + margin-bottom: var(--pico-typography-spacing-vertical); +} +hgroup > * { + margin-top: 0; + margin-bottom: 0; +} +hgroup > *:not(:first-child):last-child { + --pico-color: var(--pico-muted-color); + --pico-font-weight: unset; + font-size: 1rem; +} + +:where(ol, ul) li { + margin-bottom: calc(var(--pico-typography-spacing-vertical) * 0.25); +} + +:where(dl, ol, ul) :where(dl, ol, ul) { + margin: 0; + margin-top: calc(var(--pico-typography-spacing-vertical) * 0.25); +} + +ul li { + list-style: square; +} + +mark { + padding: 0.125rem 0.25rem; + background-color: var(--pico-mark-background-color); + color: var(--pico-mark-color); + vertical-align: baseline; +} + +blockquote { + display: block; + margin: var(--pico-typography-spacing-vertical) 0; + padding: var(--pico-spacing); + border-right: none; + border-left: 0.25rem solid var(--pico-blockquote-border-color); + border-inline-start: 0.25rem solid var(--pico-blockquote-border-color); + border-inline-end: none; +} +blockquote footer { + margin-top: calc(var(--pico-typography-spacing-vertical) * 0.5); + color: var(--pico-blockquote-footer-color); +} + +abbr[title] { + border-bottom: 1px dotted; + text-decoration: none; + cursor: help; +} + +ins { + color: var(--pico-ins-color); + text-decoration: none; +} + +del { + color: var(--pico-del-color); +} + +::selection { + background-color: var(--pico-text-selection-color); +} + +/** + * Link + */ +:where(a:not([role=button])), +[role=link] { + --pico-color: var(--pico-primary); + --pico-background-color: transparent; + --pico-underline: var(--pico-primary-underline); + outline: none; + background-color: var(--pico-background-color); + color: var(--pico-color); + text-decoration: var(--pico-text-decoration); + text-decoration-color: var(--pico-underline); + text-underline-offset: 0.125em; + transition: background-color var(--pico-transition), color var(--pico-transition), text-decoration var(--pico-transition), box-shadow var(--pico-transition); +} +:where(a:not([role=button])):is([aria-current]:not([aria-current=false]), :hover, :active, :focus), +[role=link]:is([aria-current]:not([aria-current=false]), :hover, :active, :focus) { + --pico-color: var(--pico-primary-hover); + --pico-underline: var(--pico-primary-hover-underline); + --pico-text-decoration: underline; +} +:where(a:not([role=button])):focus-visible, +[role=link]:focus-visible { + box-shadow: 0 0 0 var(--pico-outline-width) var(--pico-primary-focus); +} +:where(a:not([role=button])).secondary, +[role=link].secondary { + --pico-color: var(--pico-secondary); + --pico-underline: var(--pico-secondary-underline); +} +:where(a:not([role=button])).secondary:is([aria-current]:not([aria-current=false]), :hover, :active, :focus), +[role=link].secondary:is([aria-current]:not([aria-current=false]), :hover, :active, :focus) { + --pico-color: var(--pico-secondary-hover); + --pico-underline: var(--pico-secondary-hover-underline); +} +:where(a:not([role=button])).contrast, +[role=link].contrast { + --pico-color: var(--pico-contrast); + --pico-underline: var(--pico-contrast-underline); +} +:where(a:not([role=button])).contrast:is([aria-current]:not([aria-current=false]), :hover, :active, :focus), +[role=link].contrast:is([aria-current]:not([aria-current=false]), :hover, :active, :focus) { + --pico-color: var(--pico-contrast-hover); + --pico-underline: var(--pico-contrast-hover-underline); +} + +a[role=button] { + display: inline-block; +} + +/** + * Button + */ +button { + margin: 0; + overflow: visible; + font-family: inherit; + text-transform: none; +} + +button, +[type=submit], +[type=reset], +[type=button] { + -webkit-appearance: button; +} + +button, +[type=submit], +[type=reset], +[type=button], +[type=file]::file-selector-button, +[role=button] { + --pico-background-color: var(--pico-primary-background); + --pico-border-color: var(--pico-primary-border); + --pico-color: var(--pico-primary-inverse); + --pico-box-shadow: var(--pico-button-box-shadow, 0 0 0 rgba(0, 0, 0, 0)); + padding: var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal); + border: var(--pico-border-width) solid var(--pico-border-color); + border-radius: var(--pico-border-radius); + outline: none; + background-color: var(--pico-background-color); + box-shadow: var(--pico-box-shadow); + color: var(--pico-color); + font-weight: var(--pico-font-weight); + font-size: 1rem; + line-height: var(--pico-line-height); + text-align: center; + text-decoration: none; + cursor: pointer; + user-select: none; + transition: background-color var(--pico-transition), border-color var(--pico-transition), color var(--pico-transition), box-shadow var(--pico-transition); +} +button:is([aria-current]:not([aria-current=false])), button:is(:hover, :active, :focus), +[type=submit]:is([aria-current]:not([aria-current=false])), +[type=submit]:is(:hover, :active, :focus), +[type=reset]:is([aria-current]:not([aria-current=false])), +[type=reset]:is(:hover, :active, :focus), +[type=button]:is([aria-current]:not([aria-current=false])), +[type=button]:is(:hover, :active, :focus), +[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])), +[type=file]::file-selector-button:is(:hover, :active, :focus), +[role=button]:is([aria-current]:not([aria-current=false])), +[role=button]:is(:hover, :active, :focus) { + --pico-background-color: var(--pico-primary-hover-background); + --pico-border-color: var(--pico-primary-hover-border); + --pico-box-shadow: var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)); + --pico-color: var(--pico-primary-inverse); +} +button:focus, button:is([aria-current]:not([aria-current=false])):focus, +[type=submit]:focus, +[type=submit]:is([aria-current]:not([aria-current=false])):focus, +[type=reset]:focus, +[type=reset]:is([aria-current]:not([aria-current=false])):focus, +[type=button]:focus, +[type=button]:is([aria-current]:not([aria-current=false])):focus, +[type=file]::file-selector-button:focus, +[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])):focus, +[role=button]:focus, +[role=button]:is([aria-current]:not([aria-current=false])):focus { + --pico-box-shadow: var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)), 0 0 0 var(--pico-outline-width) var(--pico-primary-focus); +} + +[type=submit], +[type=reset], +[type=button] { + margin-bottom: var(--pico-spacing); +} + +:is(button, [type=submit], [type=button], [role=button]).secondary, +[type=reset], +[type=file]::file-selector-button { + --pico-background-color: var(--pico-secondary-background); + --pico-border-color: var(--pico-secondary-border); + --pico-color: var(--pico-secondary-inverse); + cursor: pointer; +} +:is(button, [type=submit], [type=button], [role=button]).secondary:is([aria-current]:not([aria-current=false]), :hover, :active, :focus), +[type=reset]:is([aria-current]:not([aria-current=false]), :hover, :active, :focus), +[type=file]::file-selector-button:is([aria-current]:not([aria-current=false]), :hover, :active, :focus) { + --pico-background-color: var(--pico-secondary-hover-background); + --pico-border-color: var(--pico-secondary-hover-border); + --pico-color: var(--pico-secondary-inverse); +} +:is(button, [type=submit], [type=button], [role=button]).secondary:focus, :is(button, [type=submit], [type=button], [role=button]).secondary:is([aria-current]:not([aria-current=false])):focus, +[type=reset]:focus, +[type=reset]:is([aria-current]:not([aria-current=false])):focus, +[type=file]::file-selector-button:focus, +[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])):focus { + --pico-box-shadow: var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)), 0 0 0 var(--pico-outline-width) var(--pico-secondary-focus); +} + +:is(button, [type=submit], [type=button], [role=button]).contrast { + --pico-background-color: var(--pico-contrast-background); + --pico-border-color: var(--pico-contrast-border); + --pico-color: var(--pico-contrast-inverse); +} +:is(button, [type=submit], [type=button], [role=button]).contrast:is([aria-current]:not([aria-current=false]), :hover, :active, :focus) { + --pico-background-color: var(--pico-contrast-hover-background); + --pico-border-color: var(--pico-contrast-hover-border); + --pico-color: var(--pico-contrast-inverse); +} +:is(button, [type=submit], [type=button], [role=button]).contrast:focus, :is(button, [type=submit], [type=button], [role=button]).contrast:is([aria-current]:not([aria-current=false])):focus { + --pico-box-shadow: var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)), 0 0 0 var(--pico-outline-width) var(--pico-contrast-focus); +} + +:is(button, [type=submit], [type=button], [role=button]).outline, +[type=reset].outline { + --pico-background-color: transparent; + --pico-color: var(--pico-primary); + --pico-border-color: var(--pico-primary); +} +:is(button, [type=submit], [type=button], [role=button]).outline:is([aria-current]:not([aria-current=false]), :hover, :active, :focus), +[type=reset].outline:is([aria-current]:not([aria-current=false]), :hover, :active, :focus) { + --pico-background-color: transparent; + --pico-color: var(--pico-primary-hover); + --pico-border-color: var(--pico-primary-hover); +} + +:is(button, [type=submit], [type=button], [role=button]).outline.secondary, +[type=reset].outline { + --pico-color: var(--pico-secondary); + --pico-border-color: var(--pico-secondary); +} +:is(button, [type=submit], [type=button], [role=button]).outline.secondary:is([aria-current]:not([aria-current=false]), :hover, :active, :focus), +[type=reset].outline:is([aria-current]:not([aria-current=false]), :hover, :active, :focus) { + --pico-color: var(--pico-secondary-hover); + --pico-border-color: var(--pico-secondary-hover); +} + +:is(button, [type=submit], [type=button], [role=button]).outline.contrast { + --pico-color: var(--pico-contrast); + --pico-border-color: var(--pico-contrast); +} +:is(button, [type=submit], [type=button], [role=button]).outline.contrast:is([aria-current]:not([aria-current=false]), :hover, :active, :focus) { + --pico-color: var(--pico-contrast-hover); + --pico-border-color: var(--pico-contrast-hover); +} + +:where(button, [type=submit], [type=reset], [type=button], [role=button])[disabled], +:where(fieldset[disabled]) :is(button, [type=submit], [type=button], [type=reset], [role=button]) { + opacity: 0.5; + pointer-events: none; +} + +/** + * Table + */ +:where(table) { + width: 100%; + border-collapse: collapse; + border-spacing: 0; + text-indent: 0; +} + +th, +td { + padding: calc(var(--pico-spacing) / 2) var(--pico-spacing); + border-bottom: var(--pico-border-width) solid var(--pico-table-border-color); + background-color: var(--pico-background-color); + color: var(--pico-color); + font-weight: var(--pico-font-weight); + text-align: left; + text-align: start; +} + +tfoot th, +tfoot td { + border-top: var(--pico-border-width) solid var(--pico-table-border-color); + border-bottom: 0; +} + +table.striped tbody tr:nth-child(odd) th, +table.striped tbody tr:nth-child(odd) td { + background-color: var(--pico-table-row-stripped-background-color); +} + +/** + * Embedded content + */ +:where(audio, canvas, iframe, img, svg, video) { + vertical-align: middle; +} + +audio, +video { + display: inline-block; +} + +audio:not([controls]) { + display: none; + height: 0; +} + +:where(iframe) { + border-style: none; +} + +img { + max-width: 100%; + height: auto; + border-style: none; +} + +:where(svg:not([fill])) { + fill: currentColor; +} + +svg:not(:root) { + overflow: hidden; +} + +/** + * Code + */ +pre, +code, +kbd, +samp { + font-size: 0.875em; + font-family: var(--pico-font-family); +} + +pre code { + font-size: inherit; + font-family: inherit; +} + +pre { + -ms-overflow-style: scrollbar; + overflow: auto; +} + +pre, +code, +kbd { + border-radius: var(--pico-border-radius); + background: var(--pico-code-background-color); + color: var(--pico-code-color); + font-weight: var(--pico-font-weight); + line-height: initial; +} + +code, +kbd { + display: inline-block; + padding: 0.375rem; +} + +pre { + display: block; + margin-bottom: var(--pico-spacing); + overflow-x: auto; +} +pre > code { + display: block; + padding: var(--pico-spacing); + background: none; + line-height: var(--pico-line-height); +} + +kbd { + background-color: var(--pico-code-kbd-background-color); + color: var(--pico-code-kbd-color); + vertical-align: baseline; +} + +/** + * Figure + */ +figure { + display: block; + margin: 0; + padding: 0; +} +figure figcaption { + padding: calc(var(--pico-spacing) * 0.5) 0; + color: var(--pico-muted-color); +} + +/** + * Miscs + */ +hr { + height: 0; + margin: var(--pico-typography-spacing-vertical) 0; + border: 0; + border-top: 1px solid var(--pico-muted-border-color); + color: inherit; +} + +[hidden], +template { + display: none !important; +} + +canvas { + display: inline-block; +} + +/** + * Basics form elements + */ +input, +optgroup, +select, +textarea { + margin: 0; + font-size: 1rem; + line-height: var(--pico-line-height); + font-family: inherit; + letter-spacing: inherit; +} + +input { + overflow: visible; +} + +select { + text-transform: none; +} + +legend { + max-width: 100%; + padding: 0; + color: inherit; + white-space: normal; +} + +textarea { + overflow: auto; +} + +[type=checkbox], +[type=radio] { + padding: 0; +} + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +[type=search] { + -webkit-appearance: textfield; + outline-offset: -2px; +} + +[type=search]::-webkit-search-decoration { + -webkit-appearance: none; +} + +::-webkit-file-upload-button { + -webkit-appearance: button; + font: inherit; +} + +::-moz-focus-inner { + padding: 0; + border-style: none; +} + +:-moz-focusring { + outline: none; +} + +:-moz-ui-invalid { + box-shadow: none; +} + +::-ms-expand { + display: none; +} + +[type=file], +[type=range] { + padding: 0; + border-width: 0; +} + +input:not([type=checkbox], [type=radio], [type=range]) { + height: calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2); +} + +fieldset { + width: 100%; + margin: 0; + margin-bottom: var(--pico-spacing); + padding: 0; + border: 0; +} + +label, +fieldset legend { + display: block; + margin-bottom: calc(var(--pico-spacing) * 0.375); + color: var(--pico-color); + font-weight: var(--pico-form-label-font-weight, var(--pico-font-weight)); +} + +fieldset legend { + margin-bottom: calc(var(--pico-spacing) * 0.5); +} + +input:not([type=checkbox], [type=radio]), +button[type=submit], +select, +textarea { + width: 100%; +} + +input:not([type=checkbox], [type=radio], [type=range], [type=file]), +select, +textarea { + appearance: none; + padding: var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal); +} + +input, +select, +textarea { + --pico-background-color: var(--pico-form-element-background-color); + --pico-border-color: var(--pico-form-element-border-color); + --pico-color: var(--pico-form-element-color); + --pico-box-shadow: none; + border: var(--pico-border-width) solid var(--pico-border-color); + border-radius: var(--pico-border-radius); + outline: none; + background-color: var(--pico-background-color); + box-shadow: var(--pico-box-shadow); + color: var(--pico-color); + font-weight: var(--pico-font-weight); + transition: background-color var(--pico-transition), border-color var(--pico-transition), color var(--pico-transition), box-shadow var(--pico-transition); +} + +input:not([type=submit], +[type=button], +[type=reset], +[type=checkbox], +[type=radio], +[readonly]):is(:active, :focus), +:where(select, textarea):not([readonly]):is(:active, :focus) { + --pico-background-color: var(--pico-form-element-active-background-color); +} + +input:not([type=submit], [type=button], [type=reset], [role=switch], [readonly]):is(:active, :focus), +:where(select, textarea):not([readonly]):is(:active, :focus) { + --pico-border-color: var(--pico-form-element-active-border-color); +} + +input:not([type=submit], +[type=button], +[type=reset], +[type=range], +[type=file], +[readonly]):focus, +:where(select, textarea):not([readonly]):focus { + --pico-box-shadow: 0 0 0 var(--pico-outline-width) var(--pico-form-element-focus-color); +} + +input:not([type=submit], [type=button], [type=reset])[disabled], +select[disabled], +textarea[disabled], +label[aria-disabled=true], +:where(fieldset[disabled]) :is(input:not([type=submit], [type=button], [type=reset]), select, textarea) { + opacity: var(--pico-form-element-disabled-opacity); + pointer-events: none; +} + +label[aria-disabled=true] input[disabled] { + opacity: 1; +} + +:where(input, select, textarea):not([type=checkbox], +[type=radio], +[type=date], +[type=datetime-local], +[type=month], +[type=time], +[type=week], +[type=range])[aria-invalid] { + padding-right: calc(var(--pico-form-element-spacing-horizontal) + 1.5rem) !important; + padding-left: var(--pico-form-element-spacing-horizontal); + padding-inline-start: var(--pico-form-element-spacing-horizontal) !important; + padding-inline-end: calc(var(--pico-form-element-spacing-horizontal) + 1.5rem) !important; + background-position: center right 0.75rem; + background-size: 1rem auto; + background-repeat: no-repeat; +} +:where(input, select, textarea):not([type=checkbox], +[type=radio], +[type=date], +[type=datetime-local], +[type=month], +[type=time], +[type=week], +[type=range])[aria-invalid=false]:not(select) { + background-image: var(--pico-icon-valid); +} +:where(input, select, textarea):not([type=checkbox], +[type=radio], +[type=date], +[type=datetime-local], +[type=month], +[type=time], +[type=week], +[type=range])[aria-invalid=true]:not(select) { + background-image: var(--pico-icon-invalid); +} +:where(input, select, textarea)[aria-invalid=false] { + --pico-border-color: var(--pico-form-element-valid-border-color); +} +:where(input, select, textarea)[aria-invalid=false]:is(:active, :focus) { + --pico-border-color: var(--pico-form-element-valid-active-border-color) !important; +} +:where(input, select, textarea)[aria-invalid=false]:is(:active, :focus):not([type=checkbox], [type=radio]) { + --pico-box-shadow: 0 0 0 var(--pico-outline-width) var(--pico-form-element-valid-focus-color) !important; +} +:where(input, select, textarea)[aria-invalid=true] { + --pico-border-color: var(--pico-form-element-invalid-border-color); +} +:where(input, select, textarea)[aria-invalid=true]:is(:active, :focus) { + --pico-border-color: var(--pico-form-element-invalid-active-border-color) !important; +} +:where(input, select, textarea)[aria-invalid=true]:is(:active, :focus):not([type=checkbox], [type=radio]) { + --pico-box-shadow: 0 0 0 var(--pico-outline-width) var(--pico-form-element-invalid-focus-color) !important; +} + +[dir=rtl] :where(input, select, textarea):not([type=checkbox], [type=radio]):is([aria-invalid], [aria-invalid=true], [aria-invalid=false]) { + background-position: center left 0.75rem; +} + +input::placeholder, +input::-webkit-input-placeholder, +textarea::placeholder, +textarea::-webkit-input-placeholder, +select:invalid { + color: var(--pico-form-element-placeholder-color); + opacity: 1; +} + +input:not([type=checkbox], [type=radio]), +select, +textarea { + margin-bottom: var(--pico-spacing); +} + +select::-ms-expand { + border: 0; + background-color: transparent; +} +select:not([multiple], [size]) { + padding-right: calc(var(--pico-form-element-spacing-horizontal) + 1.5rem); + padding-left: var(--pico-form-element-spacing-horizontal); + padding-inline-start: var(--pico-form-element-spacing-horizontal); + padding-inline-end: calc(var(--pico-form-element-spacing-horizontal) + 1.5rem); + background-image: var(--pico-icon-chevron); + background-position: center right 0.75rem; + background-size: 1rem auto; + background-repeat: no-repeat; +} +select[multiple] option:checked { + background: var(--pico-form-element-selected-background-color); + color: var(--pico-form-element-color); +} + +[dir=rtl] select:not([multiple], [size]) { + background-position: center left 0.75rem; +} + +textarea { + display: block; + resize: vertical; +} +textarea[aria-invalid] { + --pico-icon-height: calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2); + background-position: top right 0.75rem !important; + background-size: 1rem var(--pico-icon-height) !important; +} + +:where(input, select, textarea, fieldset, .grid) + small { + display: block; + width: 100%; + margin-top: calc(var(--pico-spacing) * -0.75); + margin-bottom: var(--pico-spacing); + color: var(--pico-muted-color); +} +:where(input, select, textarea, fieldset, .grid)[aria-invalid=false] + small { + color: var(--pico-ins-color); +} +:where(input, select, textarea, fieldset, .grid)[aria-invalid=true] + small { + color: var(--pico-del-color); +} + +label > :where(input, select, textarea) { + margin-top: calc(var(--pico-spacing) * 0.25); +} + +/** + * Checkboxes, Radios and Switches + */ +label:has([type=checkbox], [type=radio]) { + width: fit-content; + cursor: pointer; +} + +[type=checkbox], +[type=radio] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + width: 1.25em; + height: 1.25em; + margin-top: -0.125em; + margin-inline-end: 0.5em; + border-width: var(--pico-border-width); + vertical-align: middle; + cursor: pointer; +} +[type=checkbox]::-ms-check, +[type=radio]::-ms-check { + display: none; +} +[type=checkbox]:checked, [type=checkbox]:checked:active, [type=checkbox]:checked:focus, +[type=radio]:checked, +[type=radio]:checked:active, +[type=radio]:checked:focus { + --pico-background-color: var(--pico-primary-background); + --pico-border-color: var(--pico-primary-border); + background-image: var(--pico-icon-checkbox); + background-position: center; + background-size: 0.75em auto; + background-repeat: no-repeat; +} +[type=checkbox] ~ label, +[type=radio] ~ label { + display: inline-block; + margin-bottom: 0; + cursor: pointer; +} +[type=checkbox] ~ label:not(:last-of-type), +[type=radio] ~ label:not(:last-of-type) { + margin-inline-end: 1em; +} + +[type=checkbox]:indeterminate { + --pico-background-color: var(--pico-primary-background); + --pico-border-color: var(--pico-primary-border); + background-image: var(--pico-icon-minus); + background-position: center; + background-size: 0.75em auto; + background-repeat: no-repeat; +} + +[type=radio] { + border-radius: 50%; +} +[type=radio]:checked, [type=radio]:checked:active, [type=radio]:checked:focus { + --pico-background-color: var(--pico-primary-inverse); + border-width: 0.35em; + background-image: none; +} + +[type=checkbox][role=switch] { + --pico-background-color: var(--pico-switch-background-color); + --pico-color: var(--pico-switch-color); + width: 2.25em; + height: 1.25em; + border: var(--pico-border-width) solid var(--pico-border-color); + border-radius: 1.25em; + background-color: var(--pico-background-color); + line-height: 1.25em; +} +[type=checkbox][role=switch]:not([aria-invalid]) { + --pico-border-color: var(--pico-switch-background-color); +} +[type=checkbox][role=switch]:before { + display: block; + aspect-ratio: 1; + height: 100%; + border-radius: 50%; + background-color: var(--pico-color); + box-shadow: var(--pico-switch-thumb-box-shadow); + content: ""; + transition: margin 0.1s ease-in-out; +} +[type=checkbox][role=switch]:focus { + --pico-background-color: var(--pico-switch-background-color); + --pico-border-color: var(--pico-switch-background-color); +} +[type=checkbox][role=switch]:checked { + --pico-background-color: var(--pico-switch-checked-background-color); + --pico-border-color: var(--pico-switch-checked-background-color); + background-image: none; +} +[type=checkbox][role=switch]:checked::before { + margin-inline-start: calc(2.25em - 1.25em); +} +[type=checkbox][role=switch][disabled] { + --pico-background-color: var(--pico-border-color); +} + +[type=checkbox][aria-invalid=false]:checked, [type=checkbox][aria-invalid=false]:checked:active, [type=checkbox][aria-invalid=false]:checked:focus, +[type=checkbox][role=switch][aria-invalid=false]:checked, +[type=checkbox][role=switch][aria-invalid=false]:checked:active, +[type=checkbox][role=switch][aria-invalid=false]:checked:focus { + --pico-background-color: var(--pico-form-element-valid-border-color); +} +[type=checkbox]:checked[aria-invalid=true], [type=checkbox]:checked:active[aria-invalid=true], [type=checkbox]:checked:focus[aria-invalid=true], +[type=checkbox][role=switch]:checked[aria-invalid=true], +[type=checkbox][role=switch]:checked:active[aria-invalid=true], +[type=checkbox][role=switch]:checked:focus[aria-invalid=true] { + --pico-background-color: var(--pico-form-element-invalid-border-color); +} + +[type=checkbox][aria-invalid=false]:checked, [type=checkbox][aria-invalid=false]:checked:active, [type=checkbox][aria-invalid=false]:checked:focus, +[type=radio][aria-invalid=false]:checked, +[type=radio][aria-invalid=false]:checked:active, +[type=radio][aria-invalid=false]:checked:focus, +[type=checkbox][role=switch][aria-invalid=false]:checked, +[type=checkbox][role=switch][aria-invalid=false]:checked:active, +[type=checkbox][role=switch][aria-invalid=false]:checked:focus { + --pico-border-color: var(--pico-form-element-valid-border-color); +} +[type=checkbox]:checked[aria-invalid=true], [type=checkbox]:checked:active[aria-invalid=true], [type=checkbox]:checked:focus[aria-invalid=true], +[type=radio]:checked[aria-invalid=true], +[type=radio]:checked:active[aria-invalid=true], +[type=radio]:checked:focus[aria-invalid=true], +[type=checkbox][role=switch]:checked[aria-invalid=true], +[type=checkbox][role=switch]:checked:active[aria-invalid=true], +[type=checkbox][role=switch]:checked:focus[aria-invalid=true] { + --pico-border-color: var(--pico-form-element-invalid-border-color); +} + +/** + * Input type color + */ +[type=color]::-webkit-color-swatch-wrapper { + padding: 0; +} +[type=color]::-moz-focus-inner { + padding: 0; +} +[type=color]::-webkit-color-swatch { + border: 0; + border-radius: calc(var(--pico-border-radius) * 0.5); +} +[type=color]::-moz-color-swatch { + border: 0; + border-radius: calc(var(--pico-border-radius) * 0.5); +} + +/** + * Input type datetime + */ +input:not([type=checkbox], [type=radio], [type=range], [type=file]):is([type=date], [type=datetime-local], [type=month], [type=time], [type=week]) { + --pico-icon-position: 0.75rem; + --pico-icon-width: 1rem; + padding-right: calc(var(--pico-icon-width) + var(--pico-icon-position)); + background-image: var(--pico-icon-date); + background-position: center right var(--pico-icon-position); + background-size: var(--pico-icon-width) auto; + background-repeat: no-repeat; +} +input:not([type=checkbox], [type=radio], [type=range], [type=file])[type=time] { + background-image: var(--pico-icon-time); +} + +[type=date]::-webkit-calendar-picker-indicator, +[type=datetime-local]::-webkit-calendar-picker-indicator, +[type=month]::-webkit-calendar-picker-indicator, +[type=time]::-webkit-calendar-picker-indicator, +[type=week]::-webkit-calendar-picker-indicator { + width: var(--pico-icon-width); + margin-right: calc(var(--pico-icon-width) * -1); + margin-left: var(--pico-icon-position); + opacity: 0; +} + +@-moz-document url-prefix() { + [type=date], + [type=datetime-local], + [type=month], + [type=time], + [type=week] { + padding-right: var(--pico-form-element-spacing-horizontal) !important; + background-image: none !important; + } +} +[dir=rtl] :is([type=date], [type=datetime-local], [type=month], [type=time], [type=week]) { + text-align: right; +} + +/** + * Input type file + */ +[type=file] { + --pico-color: var(--pico-muted-color); + margin-left: calc(var(--pico-outline-width) * -1); + padding: calc(var(--pico-form-element-spacing-vertical) * 0.5) 0; + padding-left: var(--pico-outline-width); + border: 0; + border-radius: 0; + background: none; +} +[type=file]::file-selector-button { + margin-right: calc(var(--pico-spacing) / 2); + padding: calc(var(--pico-form-element-spacing-vertical) * 0.5) var(--pico-form-element-spacing-horizontal); +} +[type=file]:is(:hover, :active, :focus)::file-selector-button { + --pico-background-color: var(--pico-secondary-hover-background); + --pico-border-color: var(--pico-secondary-hover-border); +} +[type=file]:focus::file-selector-button { + --pico-box-shadow: var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)), 0 0 0 var(--pico-outline-width) var(--pico-secondary-focus); +} + +/** + * Input type range + */ +[type=range] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + width: 100%; + height: 1.25rem; + background: none; +} +[type=range]::-webkit-slider-runnable-track { + width: 100%; + height: 0.375rem; + border-radius: var(--pico-border-radius); + background-color: var(--pico-range-border-color); + transition: background-color var(--pico-transition), box-shadow var(--pico-transition); +} +[type=range]::-moz-range-track { + width: 100%; + height: 0.375rem; + border-radius: var(--pico-border-radius); + background-color: var(--pico-range-border-color); + transition: background-color var(--pico-transition), box-shadow var(--pico-transition); +} +[type=range]::-ms-track { + width: 100%; + height: 0.375rem; + border-radius: var(--pico-border-radius); + background-color: var(--pico-range-border-color); + transition: background-color var(--pico-transition), box-shadow var(--pico-transition); +} +[type=range]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 1.25rem; + height: 1.25rem; + margin-top: -0.4375rem; + border: 2px solid var(--pico-range-thumb-border-color); + border-radius: 50%; + background-color: var(--pico-range-thumb-color); + cursor: pointer; + transition: background-color var(--pico-transition), transform var(--pico-transition); +} +[type=range]::-moz-range-thumb { + -webkit-appearance: none; + width: 1.25rem; + height: 1.25rem; + margin-top: -0.4375rem; + border: 2px solid var(--pico-range-thumb-border-color); + border-radius: 50%; + background-color: var(--pico-range-thumb-color); + cursor: pointer; + transition: background-color var(--pico-transition), transform var(--pico-transition); +} +[type=range]::-ms-thumb { + -webkit-appearance: none; + width: 1.25rem; + height: 1.25rem; + margin-top: -0.4375rem; + border: 2px solid var(--pico-range-thumb-border-color); + border-radius: 50%; + background-color: var(--pico-range-thumb-color); + cursor: pointer; + transition: background-color var(--pico-transition), transform var(--pico-transition); +} +[type=range]:active, [type=range]:focus-within { + --pico-range-border-color: var(--pico-range-active-border-color); + --pico-range-thumb-color: var(--pico-range-thumb-active-color); +} +[type=range]:active::-webkit-slider-thumb { + transform: scale(1.25); +} +[type=range]:active::-moz-range-thumb { + transform: scale(1.25); +} +[type=range]:active::-ms-thumb { + transform: scale(1.25); +} + +/** + * Input type search + */ +input:not([type=checkbox], [type=radio], [type=range], [type=file])[type=search] { + padding-inline-start: calc(var(--pico-form-element-spacing-horizontal) + 1.75rem); + background-image: var(--pico-icon-search); + background-position: center left calc(var(--pico-form-element-spacing-horizontal) + 0.125rem); + background-size: 1rem auto; + background-repeat: no-repeat; +} +input:not([type=checkbox], [type=radio], [type=range], [type=file])[type=search][aria-invalid] { + padding-inline-start: calc(var(--pico-form-element-spacing-horizontal) + 1.75rem) !important; + background-position: center left 1.125rem, center right 0.75rem; +} +input:not([type=checkbox], [type=radio], [type=range], [type=file])[type=search][aria-invalid=false] { + background-image: var(--pico-icon-search), var(--pico-icon-valid); +} +input:not([type=checkbox], [type=radio], [type=range], [type=file])[type=search][aria-invalid=true] { + background-image: var(--pico-icon-search), var(--pico-icon-invalid); +} + +[dir=rtl] :where(input):not([type=checkbox], [type=radio], [type=range], [type=file])[type=search] { + background-position: center right 1.125rem; +} +[dir=rtl] :where(input):not([type=checkbox], [type=radio], [type=range], [type=file])[type=search][aria-invalid] { + background-position: center right 1.125rem, center left 0.75rem; +} + +/** + * Accordion (
) + */ +details { + display: block; + margin-bottom: var(--pico-spacing); +} +details summary { + line-height: 1rem; + list-style-type: none; + cursor: pointer; + transition: color var(--pico-transition); +} +details summary:not([role]) { + color: var(--pico-accordion-close-summary-color); +} +details summary::-webkit-details-marker { + display: none; +} +details summary::marker { + display: none; +} +details summary::-moz-list-bullet { + list-style-type: none; +} +details summary::after { + display: block; + width: 1rem; + height: 1rem; + margin-inline-start: calc(var(--pico-spacing, 1rem) * 0.5); + float: right; + transform: rotate(-90deg); + background-image: var(--pico-icon-chevron); + background-position: right center; + background-size: 1rem auto; + background-repeat: no-repeat; + content: ""; + transition: transform var(--pico-transition); +} +details summary:focus { + outline: none; +} +details summary:focus:not([role]) { + color: var(--pico-accordion-active-summary-color); +} +details summary:focus-visible:not([role]) { + outline: var(--pico-outline-width) solid var(--pico-primary-focus); + outline-offset: calc(var(--pico-spacing, 1rem) * 0.5); + color: var(--pico-primary); +} +details summary[role=button] { + width: 100%; + text-align: left; +} +details summary[role=button]::after { + height: calc(1rem * var(--pico-line-height, 1.5)); +} +details[open] > summary { + margin-bottom: var(--pico-spacing); +} +details[open] > summary:not([role]):not(:focus) { + color: var(--pico-accordion-open-summary-color); +} +details[open] > summary::after { + transform: rotate(0); +} + +[dir=rtl] details summary { + text-align: right; +} +[dir=rtl] details summary::after { + float: left; + background-position: left center; +} + +/** + * Card (
) + */ +article { + margin-bottom: var(--pico-block-spacing-vertical); + padding: var(--pico-block-spacing-vertical) var(--pico-block-spacing-horizontal); + border-radius: var(--pico-border-radius); + background: var(--pico-card-background-color); + box-shadow: var(--pico-card-box-shadow); +} +article > header, +article > footer { + margin-right: calc(var(--pico-block-spacing-horizontal) * -1); + margin-left: calc(var(--pico-block-spacing-horizontal) * -1); + padding: calc(var(--pico-block-spacing-vertical) * 0.66) var(--pico-block-spacing-horizontal); + background-color: var(--pico-card-sectioning-background-color); +} +article > header { + margin-top: calc(var(--pico-block-spacing-vertical) * -1); + margin-bottom: var(--pico-block-spacing-vertical); + border-bottom: var(--pico-border-width) solid var(--pico-card-border-color); + border-top-right-radius: var(--pico-border-radius); + border-top-left-radius: var(--pico-border-radius); +} +article > footer { + margin-top: var(--pico-block-spacing-vertical); + margin-bottom: calc(var(--pico-block-spacing-vertical) * -1); + border-top: var(--pico-border-width) solid var(--pico-card-border-color); + border-bottom-right-radius: var(--pico-border-radius); + border-bottom-left-radius: var(--pico-border-radius); +} + +/** + * Dropdown (details.dropdown) + */ +details.dropdown { + position: relative; + border-bottom: none; +} +details.dropdown summary::after, +details.dropdown > button::after, +details.dropdown > a::after { + display: block; + width: 1rem; + height: calc(1rem * var(--pico-line-height, 1.5)); + margin-inline-start: 0.25rem; + float: right; + transform: rotate(0deg) translateX(0.2rem); + background-image: var(--pico-icon-chevron); + background-position: right center; + background-size: 1rem auto; + background-repeat: no-repeat; + content: ""; +} + +nav details.dropdown { + margin-bottom: 0; +} + +details.dropdown summary:not([role]) { + height: calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2); + padding: var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal); + border: var(--pico-border-width) solid var(--pico-form-element-border-color); + border-radius: var(--pico-border-radius); + background-color: var(--pico-form-element-background-color); + color: var(--pico-form-element-placeholder-color); + line-height: inherit; + cursor: pointer; + user-select: none; + transition: background-color var(--pico-transition), border-color var(--pico-transition), color var(--pico-transition), box-shadow var(--pico-transition); +} +details.dropdown summary:not([role]):active, details.dropdown summary:not([role]):focus { + border-color: var(--pico-form-element-active-border-color); + background-color: var(--pico-form-element-active-background-color); +} +details.dropdown summary:not([role]):focus { + box-shadow: 0 0 0 var(--pico-outline-width) var(--pico-form-element-focus-color); +} +details.dropdown summary:not([role]):focus-visible { + outline: none; +} +details.dropdown summary:not([role])[aria-invalid=false] { + --pico-form-element-border-color: var(--pico-form-element-valid-border-color); + --pico-form-element-active-border-color: var(--pico-form-element-valid-focus-color); + --pico-form-element-focus-color: var(--pico-form-element-valid-focus-color); +} +details.dropdown summary:not([role])[aria-invalid=true] { + --pico-form-element-border-color: var(--pico-form-element-invalid-border-color); + --pico-form-element-active-border-color: var(--pico-form-element-invalid-focus-color); + --pico-form-element-focus-color: var(--pico-form-element-invalid-focus-color); +} + +nav details.dropdown { + display: inline; + margin: calc(var(--pico-nav-element-spacing-vertical) * -1) 0; +} +nav details.dropdown summary::after { + transform: rotate(0deg) translateX(0rem); +} +nav details.dropdown summary:not([role]) { + height: calc(1rem * var(--pico-line-height) + var(--pico-nav-link-spacing-vertical) * 2); + padding: calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) var(--pico-nav-link-spacing-horizontal); +} +nav details.dropdown summary:not([role]):focus-visible { + box-shadow: 0 0 0 var(--pico-outline-width) var(--pico-primary-focus); +} + +details.dropdown summary + ul { + display: flex; + z-index: 99; + position: absolute; + left: 0; + flex-direction: column; + width: 100%; + min-width: fit-content; + margin: 0; + margin-top: var(--pico-outline-width); + padding: 0; + border: var(--pico-border-width) solid var(--pico-dropdown-border-color); + border-radius: var(--pico-border-radius); + background-color: var(--pico-dropdown-background-color); + box-shadow: var(--pico-dropdown-box-shadow); + color: var(--pico-dropdown-color); + white-space: nowrap; + opacity: 0; + transition: opacity var(--pico-transition), transform 0s ease-in-out 1s; +} +details.dropdown summary + ul[dir=rtl] { + right: 0; + left: auto; +} +details.dropdown summary + ul li { + width: 100%; + margin-bottom: 0; + padding: calc(var(--pico-form-element-spacing-vertical) * 0.5) var(--pico-form-element-spacing-horizontal); + list-style: none; +} +details.dropdown summary + ul li:first-of-type { + margin-top: calc(var(--pico-form-element-spacing-vertical) * 0.5); +} +details.dropdown summary + ul li:last-of-type { + margin-bottom: calc(var(--pico-form-element-spacing-vertical) * 0.5); +} +details.dropdown summary + ul li a { + display: block; + margin: calc(var(--pico-form-element-spacing-vertical) * -0.5) calc(var(--pico-form-element-spacing-horizontal) * -1); + padding: calc(var(--pico-form-element-spacing-vertical) * 0.5) var(--pico-form-element-spacing-horizontal); + overflow: hidden; + border-radius: 0; + color: var(--pico-dropdown-color); + text-decoration: none; + text-overflow: ellipsis; +} +details.dropdown summary + ul li a:hover, details.dropdown summary + ul li a:focus, details.dropdown summary + ul li a:active, details.dropdown summary + ul li a:focus-visible, details.dropdown summary + ul li a[aria-current]:not([aria-current=false]) { + background-color: var(--pico-dropdown-hover-background-color); +} +details.dropdown summary + ul li label { + width: 100%; +} +details.dropdown summary + ul li:has(label):hover { + background-color: var(--pico-dropdown-hover-background-color); +} + +details.dropdown[open] summary { + margin-bottom: 0; +} + +details.dropdown[open] summary + ul { + transform: scaleY(1); + opacity: 1; + transition: opacity var(--pico-transition), transform 0s ease-in-out 0s; +} + +details.dropdown[open] summary::before { + display: block; + z-index: 1; + position: fixed; + width: 100vw; + height: 100vh; + inset: 0; + background: none; + content: ""; + cursor: default; +} + +label > details.dropdown { + margin-top: calc(var(--pico-spacing) * 0.25); +} + +/** + * Group ([role="group"], [role="search"]) + */ +[role=search], +[role=group] { + display: inline-flex; + position: relative; + width: 100%; + margin-bottom: var(--pico-spacing); + border-radius: var(--pico-border-radius); + box-shadow: var(--pico-group-box-shadow, 0 0 0 rgba(0, 0, 0, 0)); + vertical-align: middle; + transition: box-shadow var(--pico-transition); +} +[role=search] > *, +[role=search] input:not([type=checkbox], [type=radio]), +[role=search] select, +[role=group] > *, +[role=group] input:not([type=checkbox], [type=radio]), +[role=group] select { + position: relative; + flex: 1 1 auto; + margin-bottom: 0; +} +[role=search] > *:not(:first-child), +[role=search] input:not([type=checkbox], [type=radio]):not(:first-child), +[role=search] select:not(:first-child), +[role=group] > *:not(:first-child), +[role=group] input:not([type=checkbox], [type=radio]):not(:first-child), +[role=group] select:not(:first-child) { + margin-left: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +[role=search] > *:not(:last-child), +[role=search] input:not([type=checkbox], [type=radio]):not(:last-child), +[role=search] select:not(:last-child), +[role=group] > *:not(:last-child), +[role=group] input:not([type=checkbox], [type=radio]):not(:last-child), +[role=group] select:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +[role=search] > *:focus, +[role=search] input:not([type=checkbox], [type=radio]):focus, +[role=search] select:focus, +[role=group] > *:focus, +[role=group] input:not([type=checkbox], [type=radio]):focus, +[role=group] select:focus { + z-index: 2; +} +[role=search] button:not(:first-child), +[role=search] [type=submit]:not(:first-child), +[role=search] [type=reset]:not(:first-child), +[role=search] [type=button]:not(:first-child), +[role=search] [role=button]:not(:first-child), +[role=search] input:not([type=checkbox], [type=radio]):not(:first-child), +[role=search] select:not(:first-child), +[role=group] button:not(:first-child), +[role=group] [type=submit]:not(:first-child), +[role=group] [type=reset]:not(:first-child), +[role=group] [type=button]:not(:first-child), +[role=group] [role=button]:not(:first-child), +[role=group] input:not([type=checkbox], [type=radio]):not(:first-child), +[role=group] select:not(:first-child) { + margin-left: calc(var(--pico-border-width) * -1); +} +[role=search] button, +[role=search] [type=submit], +[role=search] [type=reset], +[role=search] [type=button], +[role=search] [role=button], +[role=group] button, +[role=group] [type=submit], +[role=group] [type=reset], +[role=group] [type=button], +[role=group] [role=button] { + width: auto; +} +@supports selector(:has(*)) { + [role=search]:has(button:focus, [type=submit]:focus, [type=button]:focus, [role=button]:focus), + [role=group]:has(button:focus, [type=submit]:focus, [type=button]:focus, [role=button]:focus) { + --pico-group-box-shadow: var(--pico-group-box-shadow-focus-with-button); + } + [role=search]:has(button:focus, [type=submit]:focus, [type=button]:focus, [role=button]:focus) input:not([type=checkbox], [type=radio]), + [role=search]:has(button:focus, [type=submit]:focus, [type=button]:focus, [role=button]:focus) select, + [role=group]:has(button:focus, [type=submit]:focus, [type=button]:focus, [role=button]:focus) input:not([type=checkbox], [type=radio]), + [role=group]:has(button:focus, [type=submit]:focus, [type=button]:focus, [role=button]:focus) select { + border-color: transparent; + } + [role=search]:has(input:not([type=submit], [type=button]):focus, select:focus), + [role=group]:has(input:not([type=submit], [type=button]):focus, select:focus) { + --pico-group-box-shadow: var(--pico-group-box-shadow-focus-with-input); + } + [role=search]:has(input:not([type=submit], [type=button]):focus, select:focus) button, + [role=search]:has(input:not([type=submit], [type=button]):focus, select:focus) [type=submit], + [role=search]:has(input:not([type=submit], [type=button]):focus, select:focus) [type=button], + [role=search]:has(input:not([type=submit], [type=button]):focus, select:focus) [role=button], + [role=group]:has(input:not([type=submit], [type=button]):focus, select:focus) button, + [role=group]:has(input:not([type=submit], [type=button]):focus, select:focus) [type=submit], + [role=group]:has(input:not([type=submit], [type=button]):focus, select:focus) [type=button], + [role=group]:has(input:not([type=submit], [type=button]):focus, select:focus) [role=button] { + --pico-button-box-shadow: 0 0 0 var(--pico-border-width) var(--pico-primary-border); + --pico-button-hover-box-shadow: 0 0 0 var(--pico-border-width) var(--pico-primary-hover-border); + } + [role=search] button:focus, + [role=search] [type=submit]:focus, + [role=search] [type=reset]:focus, + [role=search] [type=button]:focus, + [role=search] [role=button]:focus, + [role=group] button:focus, + [role=group] [type=submit]:focus, + [role=group] [type=reset]:focus, + [role=group] [type=button]:focus, + [role=group] [role=button]:focus { + box-shadow: none; + } +} + +[role=search] > *:first-child { + border-top-left-radius: 5rem; + border-bottom-left-radius: 5rem; +} +[role=search] > *:last-child { + border-top-right-radius: 5rem; + border-bottom-right-radius: 5rem; +} + +/** + * Loading ([aria-busy=true]) + */ +[aria-busy=true]:not(input, select, textarea, html) { + white-space: nowrap; +} +[aria-busy=true]:not(input, select, textarea, html)::before { + display: inline-block; + width: 1em; + height: 1em; + background-image: var(--pico-icon-loading); + background-size: 1em auto; + background-repeat: no-repeat; + content: ""; + vertical-align: -0.125em; +} +[aria-busy=true]:not(input, select, textarea, html):not(:empty)::before { + margin-inline-end: calc(var(--pico-spacing) * 0.5); +} +[aria-busy=true]:not(input, select, textarea, html):empty { + text-align: center; +} + +button[aria-busy=true], +[type=submit][aria-busy=true], +[type=button][aria-busy=true], +[type=reset][aria-busy=true], +[role=button][aria-busy=true], +a[aria-busy=true] { + pointer-events: none; +} + +/** + * Modal () + */ +:root { + --pico-scrollbar-width: 0px; +} + +dialog { + display: flex; + z-index: 999; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + align-items: center; + justify-content: center; + width: inherit; + min-width: 100%; + height: inherit; + min-height: 100%; + padding: 0; + border: 0; + backdrop-filter: var(--pico-modal-overlay-backdrop-filter); + background-color: var(--pico-modal-overlay-background-color); + color: var(--pico-color); +} +dialog article { + width: 100%; + max-height: calc(100vh - var(--pico-spacing) * 2); + margin: var(--pico-spacing); + overflow: auto; +} +@media (min-width: 576px) { + dialog article { + max-width: 95%; + } +} +@media (min-width: 768px) { + dialog article { + max-width: 95%; + } +} +dialog article > header > * { + margin-bottom: 0; +} +dialog article > header .close, dialog article > header :is(a, button)[rel=prev] { + margin: 0; + margin-left: var(--pico-spacing); + padding: 0; + float: right; +} +dialog article > footer { + text-align: right; +} +dialog article > footer button, +dialog article > footer [role=button] { + margin-bottom: 0; +} +dialog article > footer button:not(:first-of-type), +dialog article > footer [role=button]:not(:first-of-type) { + margin-left: calc(var(--pico-spacing) * 0.5); +} +dialog article .close, dialog article :is(a, button)[rel=prev] { + display: block; + width: 1rem; + height: 1rem; + margin-top: calc(var(--pico-spacing) * -1); + margin-bottom: var(--pico-spacing); + margin-left: auto; + border: none; + background-image: var(--pico-icon-close); + background-position: center; + background-size: auto 1rem; + background-repeat: no-repeat; + background-color: transparent; + opacity: 0.5; + transition: opacity var(--pico-transition); +} +dialog article .close:is([aria-current]:not([aria-current=false]), :hover, :active, :focus), dialog article :is(a, button)[rel=prev]:is([aria-current]:not([aria-current=false]), :hover, :active, :focus) { + opacity: 1; +} +dialog:not([open]), dialog[open=false] { + display: none; +} + +.modal-is-open { + padding-right: var(--pico-scrollbar-width, 0px); + overflow: hidden; + pointer-events: none; + touch-action: none; +} +.modal-is-open dialog { + pointer-events: auto; + touch-action: auto; +} + +:where(.modal-is-opening, .modal-is-closing) dialog, +:where(.modal-is-opening, .modal-is-closing) dialog > article { + animation-duration: 0.2s; + animation-timing-function: ease-in-out; + animation-fill-mode: both; +} +:where(.modal-is-opening, .modal-is-closing) dialog { + animation-duration: 0.8s; + animation-name: modal-overlay; +} +:where(.modal-is-opening, .modal-is-closing) dialog > article { + animation-delay: 0.2s; + animation-name: modal; +} + +.modal-is-closing dialog, +.modal-is-closing dialog > article { + animation-delay: 0s; + animation-direction: reverse; +} + +@keyframes modal-overlay { + from { + backdrop-filter: none; + background-color: transparent; + } +} +@keyframes modal { + from { + transform: translateY(-100%); + opacity: 0; + } +} +/** + * Nav + */ +:where(nav li)::before { + float: left; + content: "​"; +} + +nav, +nav ul { + display: flex; +} + +nav { + justify-content: space-between; + overflow: visible; +} +nav ol, +nav ul { + align-items: center; + margin-bottom: 0; + padding: 0; + list-style: none; +} +nav ol:first-of-type, +nav ul:first-of-type { + margin-left: calc(var(--pico-nav-element-spacing-horizontal) * -1); +} +nav ol:last-of-type, +nav ul:last-of-type { + margin-right: calc(var(--pico-nav-element-spacing-horizontal) * -1); +} +nav li { + display: inline-block; + margin: 0; + padding: var(--pico-nav-element-spacing-vertical) var(--pico-nav-element-spacing-horizontal); +} +nav li :where(a, [role=link]) { + display: inline-block; + margin: calc(var(--pico-nav-link-spacing-vertical) * -1) calc(var(--pico-nav-link-spacing-horizontal) * -1); + padding: var(--pico-nav-link-spacing-vertical) var(--pico-nav-link-spacing-horizontal); + border-radius: var(--pico-border-radius); +} +nav li :where(a, [role=link]):not(:hover) { + text-decoration: none; +} +nav li button, +nav li [role=button], +nav li [type=button], +nav li input:not([type=checkbox], [type=radio], [type=range], [type=file]), +nav li select { + height: auto; + margin-right: inherit; + margin-bottom: 0; + margin-left: inherit; + padding: calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) var(--pico-nav-link-spacing-horizontal); +} +nav[aria-label=breadcrumb] { + align-items: center; + justify-content: start; +} +nav[aria-label=breadcrumb] ul li:not(:first-child) { + margin-inline-start: var(--pico-nav-link-spacing-horizontal); +} +nav[aria-label=breadcrumb] ul li a { + margin: calc(var(--pico-nav-link-spacing-vertical) * -1) 0; + margin-inline-start: calc(var(--pico-nav-link-spacing-horizontal) * -1); +} +nav[aria-label=breadcrumb] ul li:not(:last-child)::after { + display: inline-block; + position: absolute; + width: calc(var(--pico-nav-link-spacing-horizontal) * 4); + margin: 0 calc(var(--pico-nav-link-spacing-horizontal) * -1); + content: var(--pico-nav-breadcrumb-divider); + color: var(--pico-muted-color); + text-align: center; + text-decoration: none; + white-space: nowrap; +} +nav[aria-label=breadcrumb] a[aria-current]:not([aria-current=false]) { + background-color: transparent; + color: inherit; + text-decoration: none; + pointer-events: none; +} + +aside nav, +aside ol, +aside ul, +aside li { + display: block; +} +aside li { + padding: calc(var(--pico-nav-element-spacing-vertical) * 0.5) var(--pico-nav-element-spacing-horizontal); +} +aside li a { + display: block; +} +aside li [role=button] { + margin: inherit; +} + +[dir=rtl] nav[aria-label=breadcrumb] ul li:not(:last-child) ::after { + content: "\\"; +} + +/** + * Progress + */ +progress { + display: inline-block; + vertical-align: baseline; +} + +progress { + -webkit-appearance: none; + -moz-appearance: none; + display: inline-block; + appearance: none; + width: 100%; + height: 0.5rem; + margin-bottom: calc(var(--pico-spacing) * 0.5); + overflow: hidden; + border: 0; + border-radius: var(--pico-border-radius); + background-color: var(--pico-progress-background-color); + color: var(--pico-progress-color); +} +progress::-webkit-progress-bar { + border-radius: var(--pico-border-radius); + background: none; +} +progress[value]::-webkit-progress-value { + background-color: var(--pico-progress-color); + transition: inline-size var(--pico-transition); +} +progress::-moz-progress-bar { + background-color: var(--pico-progress-color); +} +@media (prefers-reduced-motion: no-preference) { + progress:indeterminate { + background: var(--pico-progress-background-color) linear-gradient(to right, var(--pico-progress-color) 30%, var(--pico-progress-background-color) 30%) top left/150% 150% no-repeat; + animation: progress-indeterminate 1s linear infinite; + } + progress:indeterminate[value]::-webkit-progress-value { + background-color: transparent; + } + progress:indeterminate::-moz-progress-bar { + background-color: transparent; + } +} + +@media (prefers-reduced-motion: no-preference) { + [dir=rtl] progress:indeterminate { + animation-direction: reverse; + } +} + +@keyframes progress-indeterminate { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} +/** + * Tooltip ([data-tooltip]) + */ +[data-tooltip] { + position: relative; +} +[data-tooltip]:not(a, button, input) { + border-bottom: 1px dotted; + text-decoration: none; + cursor: help; +} +[data-tooltip][data-placement=top]::before, [data-tooltip][data-placement=top]::after, [data-tooltip]::before, [data-tooltip]::after { + display: block; + z-index: 99; + position: absolute; + bottom: 100%; + left: 50%; + padding: 0.25rem 0.5rem; + overflow: hidden; + transform: translate(-50%, -0.25rem); + border-radius: var(--pico-border-radius); + background: var(--pico-tooltip-background-color); + content: attr(data-tooltip); + color: var(--pico-tooltip-color); + font-style: normal; + font-weight: var(--pico-font-weight); + font-size: 0.875rem; + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; + opacity: 0; + pointer-events: none; +} +[data-tooltip][data-placement=top]::after, [data-tooltip]::after { + padding: 0; + transform: translate(-50%, 0rem); + border-top: 0.3rem solid; + border-right: 0.3rem solid transparent; + border-left: 0.3rem solid transparent; + border-radius: 0; + background-color: transparent; + content: ""; + color: var(--pico-tooltip-background-color); +} +[data-tooltip][data-placement=bottom]::before, [data-tooltip][data-placement=bottom]::after { + top: 100%; + bottom: auto; + transform: translate(-50%, 0.25rem); +} +[data-tooltip][data-placement=bottom]:after { + transform: translate(-50%, -0.3rem); + border: 0.3rem solid transparent; + border-bottom: 0.3rem solid; +} +[data-tooltip][data-placement=left]::before, [data-tooltip][data-placement=left]::after { + top: 50%; + right: 100%; + bottom: auto; + left: auto; + transform: translate(-0.25rem, -50%); +} +[data-tooltip][data-placement=left]:after { + transform: translate(0.3rem, -50%); + border: 0.3rem solid transparent; + border-left: 0.3rem solid; +} +[data-tooltip][data-placement=right]::before, [data-tooltip][data-placement=right]::after { + top: 50%; + right: auto; + bottom: auto; + left: 100%; + transform: translate(0.25rem, -50%); +} +[data-tooltip][data-placement=right]:after { + transform: translate(-0.3rem, -50%); + border: 0.3rem solid transparent; + border-right: 0.3rem solid; +} +[data-tooltip]:focus::before, [data-tooltip]:focus::after, [data-tooltip]:hover::before, [data-tooltip]:hover::after { + opacity: 1; +} +@media (hover: hover) and (pointer: fine) { + [data-tooltip]:focus::before, [data-tooltip]:focus::after, [data-tooltip]:hover::before, [data-tooltip]:hover::after { + --pico-tooltip-slide-to: translate(-50%, -0.25rem); + transform: translate(-50%, 0.75rem); + animation-duration: 0.2s; + animation-fill-mode: forwards; + animation-name: tooltip-slide; + opacity: 0; + } + [data-tooltip]:focus::after, [data-tooltip]:hover::after { + --pico-tooltip-caret-slide-to: translate(-50%, 0rem); + transform: translate(-50%, -0.25rem); + animation-name: tooltip-caret-slide; + } + [data-tooltip][data-placement=bottom]:focus::before, [data-tooltip][data-placement=bottom]:focus::after, [data-tooltip][data-placement=bottom]:hover::before, [data-tooltip][data-placement=bottom]:hover::after { + --pico-tooltip-slide-to: translate(-50%, 0.25rem); + transform: translate(-50%, -0.75rem); + animation-name: tooltip-slide; + } + [data-tooltip][data-placement=bottom]:focus::after, [data-tooltip][data-placement=bottom]:hover::after { + --pico-tooltip-caret-slide-to: translate(-50%, -0.3rem); + transform: translate(-50%, -0.5rem); + animation-name: tooltip-caret-slide; + } + [data-tooltip][data-placement=left]:focus::before, [data-tooltip][data-placement=left]:focus::after, [data-tooltip][data-placement=left]:hover::before, [data-tooltip][data-placement=left]:hover::after { + --pico-tooltip-slide-to: translate(-0.25rem, -50%); + transform: translate(0.75rem, -50%); + animation-name: tooltip-slide; + } + [data-tooltip][data-placement=left]:focus::after, [data-tooltip][data-placement=left]:hover::after { + --pico-tooltip-caret-slide-to: translate(0.3rem, -50%); + transform: translate(0.05rem, -50%); + animation-name: tooltip-caret-slide; + } + [data-tooltip][data-placement=right]:focus::before, [data-tooltip][data-placement=right]:focus::after, [data-tooltip][data-placement=right]:hover::before, [data-tooltip][data-placement=right]:hover::after { + --pico-tooltip-slide-to: translate(0.25rem, -50%); + transform: translate(-0.75rem, -50%); + animation-name: tooltip-slide; + } + [data-tooltip][data-placement=right]:focus::after, [data-tooltip][data-placement=right]:hover::after { + --pico-tooltip-caret-slide-to: translate(-0.3rem, -50%); + transform: translate(-0.05rem, -50%); + animation-name: tooltip-caret-slide; + } +} +@keyframes tooltip-slide { + to { + transform: var(--pico-tooltip-slide-to); + opacity: 1; + } +} +@keyframes tooltip-caret-slide { + 50% { + opacity: 0; + } + to { + transform: var(--pico-tooltip-caret-slide-to); + opacity: 1; + } +} + +/** + * Accessibility & User interaction + */ +[aria-controls] { + cursor: pointer; +} + +[aria-disabled=true], +[disabled] { + cursor: not-allowed; +} + +[aria-hidden=false][hidden] { + display: initial; +} + +[aria-hidden=false][hidden]:not(:focus) { + clip: rect(0, 0, 0, 0); + position: absolute; +} + +a, +area, +button, +input, +label, +select, +summary, +textarea, +[tabindex] { + -ms-touch-action: manipulation; +} + +[dir=rtl] { + direction: rtl; +} + +/** + * Reduce Motion Features + */ +@media (prefers-reduced-motion: reduce) { + *:not([aria-busy=true]), + :not([aria-busy=true])::before, + :not([aria-busy=true])::after { + background-attachment: initial !important; + animation-duration: 1ms !important; + animation-delay: -1ms !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + transition-delay: 0s !important; + transition-duration: 0s !important; + } +} +@font-face { + font-family: go; + src: local("Go Regular"), url(../fonts/Go-Regular.woff2) format("woff2"); + font-style: normal; + font-weight: 400; +} +@font-face { + font-family: go; + src: local("Go Italic"), url(../fonts/Go-Italic.woff2) format("woff2"); + font-style: italic; + font-weight: 400; +} +@font-face { + font-family: go medium; + src: local("Go Medium"), url(../fonts/Go-Medium.woff2) format("woff2"); + font-style: normal; + font-weight: 500; +} +@font-face { + font-family: go medium; + src: local("Go Medium Italic"), url(../fonts/Go-Medium-Italic.woff2) format("woff2"); + font-style: italic; + font-weight: 500; +} +@font-face { + font-family: go; + src: local("Go Bold"), url(../fonts/Go-Bold.woff2) format("woff2"); + font-style: normal; + font-weight: 600; +} +@font-face { + font-family: go; + src: local("Go Bold Italic"), url(../fonts/Go-Bold-Italic.woff2) format("woff2"); + font-style: italic; + font-weight: 600; +} +@font-face { + font-family: go smallcaps; + src: local("Go Smallcaps"), url(../fonts/Go-Smallcaps.woff2) format("woff2"); + font-style: normal; + font-weight: 400; +} +@font-face { + font-family: go smallcaps; + src: local("Go Smallcaps Italic"), url(../fonts/Go-Smallcaps-Italic.woff2) format("woff2"); + font-style: italic; + font-weight: 400; +} +@font-face { + font-family: go mono; + src: local("Go Mono"), url(../fonts/Go-Mono.woff2) format("woff2"); + font-style: normal; + font-weight: 400; +} +@font-face { + font-family: go mono; + src: local("Go Mono Italic"), url(../fonts/Go-Mono-Italic.woff2) format("woff2"); + font-style: italic; + font-weight: 400; +} +@font-face { + font-family: go mono; + src: local("Go Mono Bold"), url(../fonts/Go-Mono-Bold.woff2) format("woff2"); + font-style: normal; + font-weight: 600; +} +@font-face { + font-family: go mono; + src: local("Go Mono Bold Italic"), url(../fonts/Go-Mono-Bold-Italic.woff2) format("woff2"); + font-style: italic; + font-weight: 600; +} +:root { + --pico-font-family-sans-serif: "Go", system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, var(--pico-font-family-emoji); + --pico-form-element-spacing-vertical: .5rem; + --pico-form-element-spacing-horizontal: .5rem; + --pico-border-radius: 0; + --pico-font-size: 12pt; + --pico-line-height: 1.4; +} + +small { + font-size: 0.875rem; + color: var(--pico-muted-color); +} + +mark { + margin-block: calc(var(--pico-spacing) / 4); + display: inline-block; + border-radius: 0.25rem; +} + +pre { + padding: calc(var(--pico-spacing) / 2); +} + +#nav-theme-toggle { + cursor: pointer !important; +} + +.dark { + filter: grayscale(100%); +} + +.hi, .hi a { + font-size: 1.1rem; + --pico-text-decoration: none; +} + +table td article { + margin-bottom: var(--pico-spacing); +} + +table tr { + white-space: nowrap; +} + +table td.created-modified { + background-image: var(--pico-icon-date); + background-position: center right var(--pico-form-element-spacing-vertical); + background-size: 1rem; + padding-inline-end: 2rem; +} + +table td.created-modified, table th.created-modified { + text-align: right; +} + +.no-text-decoration { + text-decoration: none !important; + border-bottom: none !important; +} + +[data-loading] { + display: none; +} + +.help { + cursor: help; +} + +.pointer { + cursor: pointer; +} + +[contenteditable] { + display: inline-block; + border-bottom: 1px dotted #a0acc7; + text-decoration: none; +} + +[contenteditable]:focus { + padding: calc(var(--pico-spacing) / 2); + background: var(--pico-contrast); + color: var(--pico-contrast-inverse); +} + +[role=group] { + --pico-group-box-shadow: none !important; +} + +/* +* Table navigation +*/ +.table-navigation { + user-select: none; +} + +.table-navigation .paging:not(.disabled) { + padding: var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal); + color: var(--pico-color); +} + +.table-navigation button.sorting { + font-size: 0.875rem; + border: 0; +} + +.table-navigation div.filters { + white-space: wrap; +} + +.table-navigation div.filter-buttons button { + font-size: 0.875rem; + padding: calc(var(--pico-form-element-spacing-vertical) / 2) calc(var(--pico-form-element-spacing-horizontal) / 2); + border-color: var(--pico-form-element-border-color); + --pico-border-radius: .5rem; +} + +.table-navigation .paging.disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* +# Notifications +*/ +#notification-container { + position: fixed; + left: 0.25rem; + bottom: 0.25rem; + z-index: 999; + width: fit-content; +} + +.notification:not(:last-child) { + margin-bottom: 0.25rem; +} + +.notification-text { + white-space: pre-line; +} + +.notification { + cursor: pointer; + padding: var(--pico-form-element-spacing-vertical) 2rem; + color: var(--pico-color); + background-color: var(--pico-background-color); + background-image: var(--pico-icon); + background-position: center left var(--pico-form-element-spacing-vertical); + background-size: calc(var(--pico-spacing) * 1.5); + background-blend-mode: color-burn; + padding-left: calc(var(--pico-form-element-spacing-vertical) * 2 + var(--pico-spacing) * 1.5); +} + +.notification-error { + --pico-background-color: #af291d; + --pico-icon: var(--pico-icon-invalid); + --pico-color: #faeeeb; +} + +.notification-warning { + --pico-background-color: #fdf1b4; + --pico-icon: var(--pico-icon-invalid); + --pico-color: #1f1c02; +} + +.notification-success { + --pico-background-color: #33790f; + --pico-icon: var(--pico-icon-valid); + --pico-color: #eff1f4; +} + +.notification-user { + --pico-background-color: #018cd4; + --pico-icon: var(--pico-icon-chevron); + --pico-color: #eff1f4; +} + +.notification-title { + font-weight: bold; +} + +.notification-system > .notification-title:before { + content: "📢 Broadcast: "; +} + +.notification-system { + --pico-background-color: #5c0d41; + --pico-icon: var(--pico-icon-chevron); + --pico-color: #f9daea; +} + +.login-grid { + display: grid; + grid-template-columns: 20% 60% 20%; + grid-template-rows: 1fr; +} + +.login-register { + grid-column-start: 2; +} + +thead th, thead td, tfoot th, tfoot td { + --pico-font-weight: 400; +} + +button, a { + touch-action: manipulation; +} + +button[type=submit] { + width: auto; +} + +dialog article { + max-width: 65%; +} + +.no-text-wrap { + text-wrap: nowrap; + white-space: nowrap; +} + +.text-wrap { + text-wrap: balance; + white-space: break-spaces; +} + +.split-grid.grid > article:first-child { + grid-column: span 1; +} + +.split-grid.grid > article { + grid-column: span 3; +} + +.grid-end { + display: grid; + justify-content: end; + grid-auto-columns: max-content; + grid-auto-flow: column; + gap: calc(var(--pico-spacing) / 2); + align-items: baseline; + white-space: nowrap; +} + +.grid-space-between { + display: grid; + justify-content: space-between; + grid-auto-columns: max-content; + grid-auto-flow: column; + gap: calc(var(--pico-spacing) / 2); + white-space: nowrap; + align-items: baseline; + margin: calc(var(--pico-spacing) / 2) auto; +} + +.grid-3-cols { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: calc(var(--pico-spacing) / 2); +} + +.grid-3-cols .span-3 { + grid-column: span 3; +} + +.grid-3-cols > article { + margin-bottom: 0; + padding-bottom: var(--pico-form-element-spacing-vertical); + --pico-border-radius: .5rem; +} + +.table-select { + background-color: rgba(128, 128, 128, 0.1); + backdrop-filter: blur(1px); + position: sticky; + bottom: var(--pico-form-element-spacing-vertical); + padding: var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal); +} + +nav details.dropdown { + width: max-content; +} + +fieldset.vault-unlock { + padding: var(--pico-spacing) 0; +} + +@media only screen and (max-width: 600px) { + #notification-container { + left: 0; + bottom: 0; + width: 100%; + } + dialog article { + max-width: 95%; + } + .grid-3-cols { + display: grid; + grid-template-columns: repeat(1, 1fr); + gap: calc(var(--pico-spacing) / 2); + } + .grid-3-cols .span-3 { + grid-column: span 1; + } + .grid-3-cols > article { + margin-bottom: 0; + --pico-border-radius: .5rem; + } +} +@media (max-width: 1024px) { + .split-grid.grid { + gap: calc(var(--pico-spacing) / 2); + } + .split-grid.grid > article { + padding: 0; + background-color: transparent; + box-shadow: none; + } + th, td { + --pico-spacing: 0.75rem; + } +} +.group { + opacity: 0; +} + +textarea.dns-data { + border: 0; + background: transparent; + font-family: monospace; +} + +fieldset.system-field label:before { + content: "🔒 "; +} + +fieldset.keypair { + border: 1px solid var(--pico-form-element-border-color); + padding: var(--pico-spacing); +} + +.color-red-950 { + color: #1c0d06 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-red-950, +[type=reset].button-red-950 { + color: #dfe3eb; + border-color: #1c0d06; + background-color: #1c0d06; +} + +:is(a).color-red-950 { + text-decoration-color: #1c0d06 !important; +} + +.color-red-900 { + color: #30130a !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-red-900, +[type=reset].button-red-900 { + color: #dfe3eb; + border-color: #30130a; + background-color: #30130a; +} + +:is(a).color-red-900 { + text-decoration-color: #30130a !important; +} + +.color-red-850 { + color: #45150c !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-red-850, +[type=reset].button-red-850 { + color: #dfe3eb; + border-color: #45150c; + background-color: #45150c; +} + +:is(a).color-red-850 { + text-decoration-color: #45150c !important; +} + +.color-red-800 { + color: #5c160d !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-red-800, +[type=reset].button-red-800 { + color: #dfe3eb; + border-color: #5c160d; + background-color: #5c160d; +} + +:is(a).color-red-800 { + text-decoration-color: #5c160d !important; +} + +.color-red-750 { + color: #72170f !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-red-750, +[type=reset].button-red-750 { + color: #dfe3eb; + border-color: #72170f; + background-color: #72170f; +} + +:is(a).color-red-750 { + text-decoration-color: #72170f !important; +} + +.color-red-700 { + color: #861d13 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-red-700, +[type=reset].button-red-700 { + color: #dfe3eb; + border-color: #861d13; + background-color: #861d13; +} + +:is(a).color-red-700 { + text-decoration-color: #861d13 !important; +} + +.color-red-650 { + color: #9b2318 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-red-650, +[type=reset].button-red-650 { + color: #dfe3eb; + border-color: #9b2318; + background-color: #9b2318; +} + +:is(a).color-red-650 { + text-decoration-color: #9b2318 !important; +} + +.color-red-600 { + color: #af291d !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-red-600, +[type=reset].button-red-600 { + color: #dfe3eb; + border-color: #af291d; + background-color: #af291d; +} + +:is(a).color-red-600 { + text-decoration-color: #af291d !important; +} + +.color-red-550 { + color: #c52f21 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-red-550, +[type=reset].button-red-550 { + color: #dfe3eb; + border-color: #c52f21; + background-color: #c52f21; +} + +:is(a).color-red-550 { + text-decoration-color: #c52f21 !important; +} + +.color-red-500, .color-red { + color: #d93526 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-red-500, .button-red:is(button, [type=submit], [type=button], [role=button]), +[type=reset].button-red-500, +[type=reset].button-red { + color: #dfe3eb; + border-color: #d93526; + background-color: #d93526; +} + +:is(a).color-red-500, .color-red:is(a) { + text-decoration-color: #d93526 !important; +} + +.color-red-450 { + color: #ee402e !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-red-450, +[type=reset].button-red-450 { + color: #dfe3eb; + border-color: #ee402e; + background-color: #ee402e; +} + +:is(a).color-red-450 { + text-decoration-color: #ee402e !important; +} + +.color-red-400 { + color: #f06048 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-red-400, +[type=reset].button-red-400 { + color: #1b1b1b; + border-color: #f06048; + background-color: #f06048; +} + +:is(a).color-red-400 { + text-decoration-color: #f06048 !important; +} + +.color-red-350 { + color: #f17961 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-red-350, +[type=reset].button-red-350 { + color: #1b1b1b; + border-color: #f17961; + background-color: #f17961; +} + +:is(a).color-red-350 { + text-decoration-color: #f17961 !important; +} + +.color-red-300 { + color: #f38f79 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-red-300, +[type=reset].button-red-300 { + color: #1b1b1b; + border-color: #f38f79; + background-color: #f38f79; +} + +:is(a).color-red-300 { + text-decoration-color: #f38f79 !important; +} + +.color-red-250 { + color: #f5a390 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-red-250, +[type=reset].button-red-250 { + color: #1b1b1b; + border-color: #f5a390; + background-color: #f5a390; +} + +:is(a).color-red-250 { + text-decoration-color: #f5a390 !important; +} + +.color-red-200 { + color: #f5b7a8 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-red-200, +[type=reset].button-red-200 { + color: #1b1b1b; + border-color: #f5b7a8; + background-color: #f5b7a8; +} + +:is(a).color-red-200 { + text-decoration-color: #f5b7a8 !important; +} + +.color-red-150 { + color: #f6cabf !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-red-150, +[type=reset].button-red-150 { + color: #1b1b1b; + border-color: #f6cabf; + background-color: #f6cabf; +} + +:is(a).color-red-150 { + text-decoration-color: #f6cabf !important; +} + +.color-red-100 { + color: #f8dcd6 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-red-100, +[type=reset].button-red-100 { + color: #1b1b1b; + border-color: #f8dcd6; + background-color: #f8dcd6; +} + +:is(a).color-red-100 { + text-decoration-color: #f8dcd6 !important; +} + +.color-red-50 { + color: #faeeeb !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-red-50, +[type=reset].button-red-50 { + color: #1b1b1b; + border-color: #faeeeb; + background-color: #faeeeb; +} + +:is(a).color-red-50 { + text-decoration-color: #faeeeb !important; +} + +.color-red-main { + color: #c52f21 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-red-main, +[type=reset].button-red-main { + color: #dfe3eb; + border-color: #c52f21; + background-color: #c52f21; +} + +:is(a).color-red-main { + text-decoration-color: #c52f21 !important; +} + +.color-pink-950 { + color: #25060c !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pink-950, +[type=reset].button-pink-950 { + color: #dfe3eb; + border-color: #25060c; + background-color: #25060c; +} + +:is(a).color-pink-950 { + text-decoration-color: #25060c !important; +} + +.color-pink-900 { + color: #380916 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pink-900, +[type=reset].button-pink-900 { + color: #dfe3eb; + border-color: #380916; + background-color: #380916; +} + +:is(a).color-pink-900 { + text-decoration-color: #380916 !important; +} + +.color-pink-850 { + color: #4b0c1f !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pink-850, +[type=reset].button-pink-850 { + color: #dfe3eb; + border-color: #4b0c1f; + background-color: #4b0c1f; +} + +:is(a).color-pink-850 { + text-decoration-color: #4b0c1f !important; +} + +.color-pink-800 { + color: #5f0e28 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pink-800, +[type=reset].button-pink-800 { + color: #dfe3eb; + border-color: #5f0e28; + background-color: #5f0e28; +} + +:is(a).color-pink-800 { + text-decoration-color: #5f0e28 !important; +} + +.color-pink-750 { + color: #740f31 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pink-750, +[type=reset].button-pink-750 { + color: #dfe3eb; + border-color: #740f31; + background-color: #740f31; +} + +:is(a).color-pink-750 { + text-decoration-color: #740f31 !important; +} + +.color-pink-700 { + color: #88143b !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pink-700, +[type=reset].button-pink-700 { + color: #dfe3eb; + border-color: #88143b; + background-color: #88143b; +} + +:is(a).color-pink-700 { + text-decoration-color: #88143b !important; +} + +.color-pink-650 { + color: #9d1945 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pink-650, +[type=reset].button-pink-650 { + color: #dfe3eb; + border-color: #9d1945; + background-color: #9d1945; +} + +:is(a).color-pink-650 { + text-decoration-color: #9d1945 !important; +} + +.color-pink-600 { + color: #b21e4f !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pink-600, +[type=reset].button-pink-600 { + color: #dfe3eb; + border-color: #b21e4f; + background-color: #b21e4f; +} + +:is(a).color-pink-600 { + text-decoration-color: #b21e4f !important; +} + +.color-pink-550 { + color: #c72259 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pink-550, +[type=reset].button-pink-550 { + color: #dfe3eb; + border-color: #c72259; + background-color: #c72259; +} + +:is(a).color-pink-550 { + text-decoration-color: #c72259 !important; +} + +.color-pink-500, .color-pink { + color: #d92662 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pink-500, .button-pink:is(button, [type=submit], [type=button], [role=button]), +[type=reset].button-pink-500, +[type=reset].button-pink { + color: #dfe3eb; + border-color: #d92662; + background-color: #d92662; +} + +:is(a).color-pink-500, .color-pink:is(a) { + text-decoration-color: #d92662 !important; +} + +.color-pink-450 { + color: #f42c6f !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pink-450, +[type=reset].button-pink-450 { + color: #dfe3eb; + border-color: #f42c6f; + background-color: #f42c6f; +} + +:is(a).color-pink-450 { + text-decoration-color: #f42c6f !important; +} + +.color-pink-400 { + color: #f6547e !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pink-400, +[type=reset].button-pink-400 { + color: #1b1b1b; + border-color: #f6547e; + background-color: #f6547e; +} + +:is(a).color-pink-400 { + text-decoration-color: #f6547e !important; +} + +.color-pink-350 { + color: #f7708e !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pink-350, +[type=reset].button-pink-350 { + color: #1b1b1b; + border-color: #f7708e; + background-color: #f7708e; +} + +:is(a).color-pink-350 { + text-decoration-color: #f7708e !important; +} + +.color-pink-300 { + color: #f8889e !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pink-300, +[type=reset].button-pink-300 { + color: #1b1b1b; + border-color: #f8889e; + background-color: #f8889e; +} + +:is(a).color-pink-300 { + text-decoration-color: #f8889e !important; +} + +.color-pink-250 { + color: #f99eae !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pink-250, +[type=reset].button-pink-250 { + color: #1b1b1b; + border-color: #f99eae; + background-color: #f99eae; +} + +:is(a).color-pink-250 { + text-decoration-color: #f99eae !important; +} + +.color-pink-200 { + color: #f9b4be !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pink-200, +[type=reset].button-pink-200 { + color: #1b1b1b; + border-color: #f9b4be; + background-color: #f9b4be; +} + +:is(a).color-pink-200 { + text-decoration-color: #f9b4be !important; +} + +.color-pink-150 { + color: #f9c8ce !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pink-150, +[type=reset].button-pink-150 { + color: #1b1b1b; + border-color: #f9c8ce; + background-color: #f9c8ce; +} + +:is(a).color-pink-150 { + text-decoration-color: #f9c8ce !important; +} + +.color-pink-100 { + color: #f9dbdf !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pink-100, +[type=reset].button-pink-100 { + color: #1b1b1b; + border-color: #f9dbdf; + background-color: #f9dbdf; +} + +:is(a).color-pink-100 { + text-decoration-color: #f9dbdf !important; +} + +.color-pink-50 { + color: #fbedef !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pink-50, +[type=reset].button-pink-50 { + color: #1b1b1b; + border-color: #fbedef; + background-color: #fbedef; +} + +:is(a).color-pink-50 { + text-decoration-color: #fbedef !important; +} + +.color-pink-main { + color: #d92662 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pink-main, +[type=reset].button-pink-main { + color: #dfe3eb; + border-color: #d92662; + background-color: #d92662; +} + +:is(a).color-pink-main { + text-decoration-color: #d92662 !important; +} + +.color-fuchsia-950 { + color: #230518 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-fuchsia-950, +[type=reset].button-fuchsia-950 { + color: #dfe3eb; + border-color: #230518; + background-color: #230518; +} + +:is(a).color-fuchsia-950 { + text-decoration-color: #230518 !important; +} + +.color-fuchsia-900 { + color: #360925 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-fuchsia-900, +[type=reset].button-fuchsia-900 { + color: #dfe3eb; + border-color: #360925; + background-color: #360925; +} + +:is(a).color-fuchsia-900 { + text-decoration-color: #360925 !important; +} + +.color-fuchsia-850 { + color: #480b33 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-fuchsia-850, +[type=reset].button-fuchsia-850 { + color: #dfe3eb; + border-color: #480b33; + background-color: #480b33; +} + +:is(a).color-fuchsia-850 { + text-decoration-color: #480b33 !important; +} + +.color-fuchsia-800 { + color: #5c0d41 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-fuchsia-800, +[type=reset].button-fuchsia-800 { + color: #dfe3eb; + border-color: #5c0d41; + background-color: #5c0d41; +} + +:is(a).color-fuchsia-800 { + text-decoration-color: #5c0d41 !important; +} + +.color-fuchsia-750 { + color: #700e4f !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-fuchsia-750, +[type=reset].button-fuchsia-750 { + color: #dfe3eb; + border-color: #700e4f; + background-color: #700e4f; +} + +:is(a).color-fuchsia-750 { + text-decoration-color: #700e4f !important; +} + +.color-fuchsia-700 { + color: #84135e !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-fuchsia-700, +[type=reset].button-fuchsia-700 { + color: #dfe3eb; + border-color: #84135e; + background-color: #84135e; +} + +:is(a).color-fuchsia-700 { + text-decoration-color: #84135e !important; +} + +.color-fuchsia-650 { + color: #98176d !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-fuchsia-650, +[type=reset].button-fuchsia-650 { + color: #dfe3eb; + border-color: #98176d; + background-color: #98176d; +} + +:is(a).color-fuchsia-650 { + text-decoration-color: #98176d !important; +} + +.color-fuchsia-600 { + color: #ac1c7c !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-fuchsia-600, +[type=reset].button-fuchsia-600 { + color: #dfe3eb; + border-color: #ac1c7c; + background-color: #ac1c7c; +} + +:is(a).color-fuchsia-600 { + text-decoration-color: #ac1c7c !important; +} + +.color-fuchsia-550 { + color: #c1208b !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-fuchsia-550, +[type=reset].button-fuchsia-550 { + color: #dfe3eb; + border-color: #c1208b; + background-color: #c1208b; +} + +:is(a).color-fuchsia-550 { + text-decoration-color: #c1208b !important; +} + +.color-fuchsia-500, .color-fuchsia { + color: #d9269d !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-fuchsia-500, .button-fuchsia:is(button, [type=submit], [type=button], [role=button]), +[type=reset].button-fuchsia-500, +[type=reset].button-fuchsia { + color: #dfe3eb; + border-color: #d9269d; + background-color: #d9269d; +} + +:is(a).color-fuchsia-500, .color-fuchsia:is(a) { + text-decoration-color: #d9269d !important; +} + +.color-fuchsia-450 { + color: #ed2aac !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-fuchsia-450, +[type=reset].button-fuchsia-450 { + color: #dfe3eb; + border-color: #ed2aac; + background-color: #ed2aac; +} + +:is(a).color-fuchsia-450 { + text-decoration-color: #ed2aac !important; +} + +.color-fuchsia-400 { + color: #f748b7 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-fuchsia-400, +[type=reset].button-fuchsia-400 { + color: #1b1b1b; + border-color: #f748b7; + background-color: #f748b7; +} + +:is(a).color-fuchsia-400 { + text-decoration-color: #f748b7 !important; +} + +.color-fuchsia-350 { + color: #f869bf !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-fuchsia-350, +[type=reset].button-fuchsia-350 { + color: #1b1b1b; + border-color: #f869bf; + background-color: #f869bf; +} + +:is(a).color-fuchsia-350 { + text-decoration-color: #f869bf !important; +} + +.color-fuchsia-300 { + color: #f983c7 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-fuchsia-300, +[type=reset].button-fuchsia-300 { + color: #1b1b1b; + border-color: #f983c7; + background-color: #f983c7; +} + +:is(a).color-fuchsia-300 { + text-decoration-color: #f983c7 !important; +} + +.color-fuchsia-250 { + color: #fa9acf !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-fuchsia-250, +[type=reset].button-fuchsia-250 { + color: #1b1b1b; + border-color: #fa9acf; + background-color: #fa9acf; +} + +:is(a).color-fuchsia-250 { + text-decoration-color: #fa9acf !important; +} + +.color-fuchsia-200 { + color: #f9b1d8 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-fuchsia-200, +[type=reset].button-fuchsia-200 { + color: #1b1b1b; + border-color: #f9b1d8; + background-color: #f9b1d8; +} + +:is(a).color-fuchsia-200 { + text-decoration-color: #f9b1d8 !important; +} + +.color-fuchsia-150 { + color: #f9c6e1 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-fuchsia-150, +[type=reset].button-fuchsia-150 { + color: #1b1b1b; + border-color: #f9c6e1; + background-color: #f9c6e1; +} + +:is(a).color-fuchsia-150 { + text-decoration-color: #f9c6e1 !important; +} + +.color-fuchsia-100 { + color: #f9daea !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-fuchsia-100, +[type=reset].button-fuchsia-100 { + color: #1b1b1b; + border-color: #f9daea; + background-color: #f9daea; +} + +:is(a).color-fuchsia-100 { + text-decoration-color: #f9daea !important; +} + +.color-fuchsia-50 { + color: #fbedf4 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-fuchsia-50, +[type=reset].button-fuchsia-50 { + color: #1b1b1b; + border-color: #fbedf4; + background-color: #fbedf4; +} + +:is(a).color-fuchsia-50 { + text-decoration-color: #fbedf4 !important; +} + +.color-fuchsia-main { + color: #c1208b !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-fuchsia-main, +[type=reset].button-fuchsia-main { + color: #dfe3eb; + border-color: #c1208b; + background-color: #c1208b; +} + +:is(a).color-fuchsia-main { + text-decoration-color: #c1208b !important; +} + +.color-purple-950 { + color: #1e0820 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-purple-950, +[type=reset].button-purple-950 { + color: #dfe3eb; + border-color: #1e0820; + background-color: #1e0820; +} + +:is(a).color-purple-950 { + text-decoration-color: #1e0820 !important; +} + +.color-purple-900 { + color: #2d0f33 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-purple-900, +[type=reset].button-purple-900 { + color: #dfe3eb; + border-color: #2d0f33; + background-color: #2d0f33; +} + +:is(a).color-purple-900 { + text-decoration-color: #2d0f33 !important; +} + +.color-purple-850 { + color: #3d1545 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-purple-850, +[type=reset].button-purple-850 { + color: #dfe3eb; + border-color: #3d1545; + background-color: #3d1545; +} + +:is(a).color-purple-850 { + text-decoration-color: #3d1545 !important; +} + +.color-purple-800 { + color: #4d1a57 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-purple-800, +[type=reset].button-purple-800 { + color: #dfe3eb; + border-color: #4d1a57; + background-color: #4d1a57; +} + +:is(a).color-purple-800 { + text-decoration-color: #4d1a57 !important; +} + +.color-purple-750 { + color: #5e206b !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-purple-750, +[type=reset].button-purple-750 { + color: #dfe3eb; + border-color: #5e206b; + background-color: #5e206b; +} + +:is(a).color-purple-750 { + text-decoration-color: #5e206b !important; +} + +.color-purple-700 { + color: #6f277d !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-purple-700, +[type=reset].button-purple-700 { + color: #dfe3eb; + border-color: #6f277d; + background-color: #6f277d; +} + +:is(a).color-purple-700 { + text-decoration-color: #6f277d !important; +} + +.color-purple-650 { + color: #802e90 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-purple-650, +[type=reset].button-purple-650 { + color: #dfe3eb; + border-color: #802e90; + background-color: #802e90; +} + +:is(a).color-purple-650 { + text-decoration-color: #802e90 !important; +} + +.color-purple-600 { + color: #9236a4 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-purple-600, +[type=reset].button-purple-600 { + color: #dfe3eb; + border-color: #9236a4; + background-color: #9236a4; +} + +:is(a).color-purple-600 { + text-decoration-color: #9236a4 !important; +} + +.color-purple-550 { + color: #aa40bf !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-purple-550, +[type=reset].button-purple-550 { + color: #dfe3eb; + border-color: #aa40bf; + background-color: #aa40bf; +} + +:is(a).color-purple-550 { + text-decoration-color: #aa40bf !important; +} + +.color-purple-500, .color-purple { + color: #b645cd !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-purple-500, .button-purple:is(button, [type=submit], [type=button], [role=button]), +[type=reset].button-purple-500, +[type=reset].button-purple { + color: #dfe3eb; + border-color: #b645cd; + background-color: #b645cd; +} + +:is(a).color-purple-500, .color-purple:is(a) { + text-decoration-color: #b645cd !important; +} + +.color-purple-450 { + color: #c652dc !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-purple-450, +[type=reset].button-purple-450 { + color: #1b1b1b; + border-color: #c652dc; + background-color: #c652dc; +} + +:is(a).color-purple-450 { + text-decoration-color: #c652dc !important; +} + +.color-purple-400 { + color: #cd68e0 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-purple-400, +[type=reset].button-purple-400 { + color: #1b1b1b; + border-color: #cd68e0; + background-color: #cd68e0; +} + +:is(a).color-purple-400 { + text-decoration-color: #cd68e0 !important; +} + +.color-purple-350 { + color: #d47de4 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-purple-350, +[type=reset].button-purple-350 { + color: #1b1b1b; + border-color: #d47de4; + background-color: #d47de4; +} + +:is(a).color-purple-350 { + text-decoration-color: #d47de4 !important; +} + +.color-purple-300 { + color: #db90e8 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-purple-300, +[type=reset].button-purple-300 { + color: #1b1b1b; + border-color: #db90e8; + background-color: #db90e8; +} + +:is(a).color-purple-300 { + text-decoration-color: #db90e8 !important; +} + +.color-purple-250 { + color: #e2a3eb !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-purple-250, +[type=reset].button-purple-250 { + color: #1b1b1b; + border-color: #e2a3eb; + background-color: #e2a3eb; +} + +:is(a).color-purple-250 { + text-decoration-color: #e2a3eb !important; +} + +.color-purple-200 { + color: #e7b6ee !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-purple-200, +[type=reset].button-purple-200 { + color: #1b1b1b; + border-color: #e7b6ee; + background-color: #e7b6ee; +} + +:is(a).color-purple-200 { + text-decoration-color: #e7b6ee !important; +} + +.color-purple-150 { + color: #edc9f1 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-purple-150, +[type=reset].button-purple-150 { + color: #1b1b1b; + border-color: #edc9f1; + background-color: #edc9f1; +} + +:is(a).color-purple-150 { + text-decoration-color: #edc9f1 !important; +} + +.color-purple-100 { + color: #f2dcf4 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-purple-100, +[type=reset].button-purple-100 { + color: #1b1b1b; + border-color: #f2dcf4; + background-color: #f2dcf4; +} + +:is(a).color-purple-100 { + text-decoration-color: #f2dcf4 !important; +} + +.color-purple-50 { + color: #f8eef9 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-purple-50, +[type=reset].button-purple-50 { + color: #1b1b1b; + border-color: #f8eef9; + background-color: #f8eef9; +} + +:is(a).color-purple-50 { + text-decoration-color: #f8eef9 !important; +} + +.color-purple-main { + color: #9236a4 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-purple-main, +[type=reset].button-purple-main { + color: #dfe3eb; + border-color: #9236a4; + background-color: #9236a4; +} + +:is(a).color-purple-main { + text-decoration-color: #9236a4 !important; +} + +.color-violet-950 { + color: #190928 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-violet-950, +[type=reset].button-violet-950 { + color: #dfe3eb; + border-color: #190928; + background-color: #190928; +} + +:is(a).color-violet-950 { + text-decoration-color: #190928 !important; +} + +.color-violet-900 { + color: #251140 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-violet-900, +[type=reset].button-violet-900 { + color: #dfe3eb; + border-color: #251140; + background-color: #251140; +} + +:is(a).color-violet-900 { + text-decoration-color: #251140 !important; +} + +.color-violet-850 { + color: #321856 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-violet-850, +[type=reset].button-violet-850 { + color: #dfe3eb; + border-color: #321856; + background-color: #321856; +} + +:is(a).color-violet-850 { + text-decoration-color: #321856 !important; +} + +.color-violet-800 { + color: #3f1e6d !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-violet-800, +[type=reset].button-violet-800 { + color: #dfe3eb; + border-color: #3f1e6d; + background-color: #3f1e6d; +} + +:is(a).color-violet-800 { + text-decoration-color: #3f1e6d !important; +} + +.color-violet-750 { + color: #4d2585 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-violet-750, +[type=reset].button-violet-750 { + color: #dfe3eb; + border-color: #4d2585; + background-color: #4d2585; +} + +:is(a).color-violet-750 { + text-decoration-color: #4d2585 !important; +} + +.color-violet-700 { + color: #5b2d9c !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-violet-700, +[type=reset].button-violet-700 { + color: #dfe3eb; + border-color: #5b2d9c; + background-color: #5b2d9c; +} + +:is(a).color-violet-700 { + text-decoration-color: #5b2d9c !important; +} + +.color-violet-650 { + color: #6935b3 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-violet-650, +[type=reset].button-violet-650 { + color: #dfe3eb; + border-color: #6935b3; + background-color: #6935b3; +} + +:is(a).color-violet-650 { + text-decoration-color: #6935b3 !important; +} + +.color-violet-600 { + color: #7540bf !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-violet-600, +[type=reset].button-violet-600 { + color: #dfe3eb; + border-color: #7540bf; + background-color: #7540bf; +} + +:is(a).color-violet-600 { + text-decoration-color: #7540bf !important; +} + +.color-violet-550 { + color: #8352c5 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-violet-550, +[type=reset].button-violet-550 { + color: #dfe3eb; + border-color: #8352c5; + background-color: #8352c5; +} + +:is(a).color-violet-550 { + text-decoration-color: #8352c5 !important; +} + +.color-violet-500, .color-violet { + color: #9062ca !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-violet-500, .button-violet:is(button, [type=submit], [type=button], [role=button]), +[type=reset].button-violet-500, +[type=reset].button-violet { + color: #1b1b1b; + border-color: #9062ca; + background-color: #9062ca; +} + +:is(a).color-violet-500, .color-violet:is(a) { + text-decoration-color: #9062ca !important; +} + +.color-violet-450 { + color: #9b71cf !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-violet-450, +[type=reset].button-violet-450 { + color: #1b1b1b; + border-color: #9b71cf; + background-color: #9b71cf; +} + +:is(a).color-violet-450 { + text-decoration-color: #9b71cf !important; +} + +.color-violet-400 { + color: #a780d4 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-violet-400, +[type=reset].button-violet-400 { + color: #1b1b1b; + border-color: #a780d4; + background-color: #a780d4; +} + +:is(a).color-violet-400 { + text-decoration-color: #a780d4 !important; +} + +.color-violet-350 { + color: #b290d9 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-violet-350, +[type=reset].button-violet-350 { + color: #1b1b1b; + border-color: #b290d9; + background-color: #b290d9; +} + +:is(a).color-violet-350 { + text-decoration-color: #b290d9 !important; +} + +.color-violet-300 { + color: #bd9fdf !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-violet-300, +[type=reset].button-violet-300 { + color: #1b1b1b; + border-color: #bd9fdf; + background-color: #bd9fdf; +} + +:is(a).color-violet-300 { + text-decoration-color: #bd9fdf !important; +} + +.color-violet-250 { + color: #c9afe4 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-violet-250, +[type=reset].button-violet-250 { + color: #1b1b1b; + border-color: #c9afe4; + background-color: #c9afe4; +} + +:is(a).color-violet-250 { + text-decoration-color: #c9afe4 !important; +} + +.color-violet-200 { + color: #d3bfe8 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-violet-200, +[type=reset].button-violet-200 { + color: #1b1b1b; + border-color: #d3bfe8; + background-color: #d3bfe8; +} + +:is(a).color-violet-200 { + text-decoration-color: #d3bfe8 !important; +} + +.color-violet-150 { + color: #decfed !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-violet-150, +[type=reset].button-violet-150 { + color: #1b1b1b; + border-color: #decfed; + background-color: #decfed; +} + +:is(a).color-violet-150 { + text-decoration-color: #decfed !important; +} + +.color-violet-100 { + color: #e8dff2 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-violet-100, +[type=reset].button-violet-100 { + color: #1b1b1b; + border-color: #e8dff2; + background-color: #e8dff2; +} + +:is(a).color-violet-100 { + text-decoration-color: #e8dff2 !important; +} + +.color-violet-50 { + color: #f3eff7 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-violet-50, +[type=reset].button-violet-50 { + color: #1b1b1b; + border-color: #f3eff7; + background-color: #f3eff7; +} + +:is(a).color-violet-50 { + text-decoration-color: #f3eff7 !important; +} + +.color-violet-main { + color: #7540bf !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-violet-main, +[type=reset].button-violet-main { + color: #dfe3eb; + border-color: #7540bf; + background-color: #7540bf; +} + +:is(a).color-violet-main { + text-decoration-color: #7540bf !important; +} + +.color-indigo-950 { + color: #110b31 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-indigo-950, +[type=reset].button-indigo-950 { + color: #dfe3eb; + border-color: #110b31; + background-color: #110b31; +} + +:is(a).color-indigo-950 { + text-decoration-color: #110b31 !important; +} + +.color-indigo-900 { + color: #181546 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-indigo-900, +[type=reset].button-indigo-900 { + color: #dfe3eb; + border-color: #181546; + background-color: #181546; +} + +:is(a).color-indigo-900 { + text-decoration-color: #181546 !important; +} + +.color-indigo-850 { + color: #1f1e5e !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-indigo-850, +[type=reset].button-indigo-850 { + color: #dfe3eb; + border-color: #1f1e5e; + background-color: #1f1e5e; +} + +:is(a).color-indigo-850 { + text-decoration-color: #1f1e5e !important; +} + +.color-indigo-800 { + color: #272678 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-indigo-800, +[type=reset].button-indigo-800 { + color: #dfe3eb; + border-color: #272678; + background-color: #272678; +} + +:is(a).color-indigo-800 { + text-decoration-color: #272678 !important; +} + +.color-indigo-750 { + color: #2f2f92 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-indigo-750, +[type=reset].button-indigo-750 { + color: #dfe3eb; + border-color: #2f2f92; + background-color: #2f2f92; +} + +:is(a).color-indigo-750 { + text-decoration-color: #2f2f92 !important; +} + +.color-indigo-700 { + color: #3838ab !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-indigo-700, +[type=reset].button-indigo-700 { + color: #dfe3eb; + border-color: #3838ab; + background-color: #3838ab; +} + +:is(a).color-indigo-700 { + text-decoration-color: #3838ab !important; +} + +.color-indigo-650 { + color: #4040bf !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-indigo-650, +[type=reset].button-indigo-650 { + color: #dfe3eb; + border-color: #4040bf; + background-color: #4040bf; +} + +:is(a).color-indigo-650 { + text-decoration-color: #4040bf !important; +} + +.color-indigo-600 { + color: #524ed2 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-indigo-600, +[type=reset].button-indigo-600 { + color: #dfe3eb; + border-color: #524ed2; + background-color: #524ed2; +} + +:is(a).color-indigo-600 { + text-decoration-color: #524ed2 !important; +} + +.color-indigo-550 { + color: #655cd6 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-indigo-550, +[type=reset].button-indigo-550 { + color: #dfe3eb; + border-color: #655cd6; + background-color: #655cd6; +} + +:is(a).color-indigo-550 { + text-decoration-color: #655cd6 !important; +} + +.color-indigo-500, .color-indigo { + color: #7569da !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-indigo-500, .button-indigo:is(button, [type=submit], [type=button], [role=button]), +[type=reset].button-indigo-500, +[type=reset].button-indigo { + color: #1b1b1b; + border-color: #7569da; + background-color: #7569da; +} + +:is(a).color-indigo-500, .color-indigo:is(a) { + text-decoration-color: #7569da !important; +} + +.color-indigo-450 { + color: #8577dd !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-indigo-450, +[type=reset].button-indigo-450 { + color: #1b1b1b; + border-color: #8577dd; + background-color: #8577dd; +} + +:is(a).color-indigo-450 { + text-decoration-color: #8577dd !important; +} + +.color-indigo-400 { + color: #9486e1 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-indigo-400, +[type=reset].button-indigo-400 { + color: #1b1b1b; + border-color: #9486e1; + background-color: #9486e1; +} + +:is(a).color-indigo-400 { + text-decoration-color: #9486e1 !important; +} + +.color-indigo-350 { + color: #a294e5 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-indigo-350, +[type=reset].button-indigo-350 { + color: #1b1b1b; + border-color: #a294e5; + background-color: #a294e5; +} + +:is(a).color-indigo-350 { + text-decoration-color: #a294e5 !important; +} + +.color-indigo-300 { + color: #b0a3e8 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-indigo-300, +[type=reset].button-indigo-300 { + color: #1b1b1b; + border-color: #b0a3e8; + background-color: #b0a3e8; +} + +:is(a).color-indigo-300 { + text-decoration-color: #b0a3e8 !important; +} + +.color-indigo-250 { + color: #bdb2ec !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-indigo-250, +[type=reset].button-indigo-250 { + color: #1b1b1b; + border-color: #bdb2ec; + background-color: #bdb2ec; +} + +:is(a).color-indigo-250 { + text-decoration-color: #bdb2ec !important; +} + +.color-indigo-200 { + color: #cac1ee !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-indigo-200, +[type=reset].button-indigo-200 { + color: #1b1b1b; + border-color: #cac1ee; + background-color: #cac1ee; +} + +:is(a).color-indigo-200 { + text-decoration-color: #cac1ee !important; +} + +.color-indigo-150 { + color: #d8d0f1 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-indigo-150, +[type=reset].button-indigo-150 { + color: #1b1b1b; + border-color: #d8d0f1; + background-color: #d8d0f1; +} + +:is(a).color-indigo-150 { + text-decoration-color: #d8d0f1 !important; +} + +.color-indigo-100 { + color: #e5e0f4 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-indigo-100, +[type=reset].button-indigo-100 { + color: #1b1b1b; + border-color: #e5e0f4; + background-color: #e5e0f4; +} + +:is(a).color-indigo-100 { + text-decoration-color: #e5e0f4 !important; +} + +.color-indigo-50 { + color: #f2f0f9 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-indigo-50, +[type=reset].button-indigo-50 { + color: #1b1b1b; + border-color: #f2f0f9; + background-color: #f2f0f9; +} + +:is(a).color-indigo-50 { + text-decoration-color: #f2f0f9 !important; +} + +.color-indigo-main { + color: #524ed2 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-indigo-main, +[type=reset].button-indigo-main { + color: #dfe3eb; + border-color: #524ed2; + background-color: #524ed2; +} + +:is(a).color-indigo-main { + text-decoration-color: #524ed2 !important; +} + +.color-blue-950 { + color: #080f2d !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-blue-950, +[type=reset].button-blue-950 { + color: #dfe3eb; + border-color: #080f2d; + background-color: #080f2d; +} + +:is(a).color-blue-950 { + text-decoration-color: #080f2d !important; +} + +.color-blue-900 { + color: #0c1a41 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-blue-900, +[type=reset].button-blue-900 { + color: #dfe3eb; + border-color: #0c1a41; + background-color: #0c1a41; +} + +:is(a).color-blue-900 { + text-decoration-color: #0c1a41 !important; +} + +.color-blue-850 { + color: #0e2358 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-blue-850, +[type=reset].button-blue-850 { + color: #dfe3eb; + border-color: #0e2358; + background-color: #0e2358; +} + +:is(a).color-blue-850 { + text-decoration-color: #0e2358 !important; +} + +.color-blue-800 { + color: #0f2d70 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-blue-800, +[type=reset].button-blue-800 { + color: #dfe3eb; + border-color: #0f2d70; + background-color: #0f2d70; +} + +:is(a).color-blue-800 { + text-decoration-color: #0f2d70 !important; +} + +.color-blue-750 { + color: #0f3888 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-blue-750, +[type=reset].button-blue-750 { + color: #dfe3eb; + border-color: #0f3888; + background-color: #0f3888; +} + +:is(a).color-blue-750 { + text-decoration-color: #0f3888 !important; +} + +.color-blue-700 { + color: #1343a0 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-blue-700, +[type=reset].button-blue-700 { + color: #dfe3eb; + border-color: #1343a0; + background-color: #1343a0; +} + +:is(a).color-blue-700 { + text-decoration-color: #1343a0 !important; +} + +.color-blue-650 { + color: #184eb8 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-blue-650, +[type=reset].button-blue-650 { + color: #dfe3eb; + border-color: #184eb8; + background-color: #184eb8; +} + +:is(a).color-blue-650 { + text-decoration-color: #184eb8 !important; +} + +.color-blue-600 { + color: #1d59d0 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-blue-600, +[type=reset].button-blue-600 { + color: #dfe3eb; + border-color: #1d59d0; + background-color: #1d59d0; +} + +:is(a).color-blue-600 { + text-decoration-color: #1d59d0 !important; +} + +.color-blue-550 { + color: #2060df !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-blue-550, +[type=reset].button-blue-550 { + color: #dfe3eb; + border-color: #2060df; + background-color: #2060df; +} + +:is(a).color-blue-550 { + text-decoration-color: #2060df !important; +} + +.color-blue-500, .color-blue { + color: #3c71f7 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-blue-500, .button-blue:is(button, [type=submit], [type=button], [role=button]), +[type=reset].button-blue-500, +[type=reset].button-blue { + color: #dfe3eb; + border-color: #3c71f7; + background-color: #3c71f7; +} + +:is(a).color-blue-500, .color-blue:is(a) { + text-decoration-color: #3c71f7 !important; +} + +.color-blue-450 { + color: #5c7ef8 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-blue-450, +[type=reset].button-blue-450 { + color: #1b1b1b; + border-color: #5c7ef8; + background-color: #5c7ef8; +} + +:is(a).color-blue-450 { + text-decoration-color: #5c7ef8 !important; +} + +.color-blue-400 { + color: #748bf8 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-blue-400, +[type=reset].button-blue-400 { + color: #1b1b1b; + border-color: #748bf8; + background-color: #748bf8; +} + +:is(a).color-blue-400 { + text-decoration-color: #748bf8 !important; +} + +.color-blue-350 { + color: #8999f9 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-blue-350, +[type=reset].button-blue-350 { + color: #1b1b1b; + border-color: #8999f9; + background-color: #8999f9; +} + +:is(a).color-blue-350 { + text-decoration-color: #8999f9 !important; +} + +.color-blue-300 { + color: #9ca7fa !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-blue-300, +[type=reset].button-blue-300 { + color: #1b1b1b; + border-color: #9ca7fa; + background-color: #9ca7fa; +} + +:is(a).color-blue-300 { + text-decoration-color: #9ca7fa !important; +} + +.color-blue-250 { + color: #aeb5fb !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-blue-250, +[type=reset].button-blue-250 { + color: #1b1b1b; + border-color: #aeb5fb; + background-color: #aeb5fb; +} + +:is(a).color-blue-250 { + text-decoration-color: #aeb5fb !important; +} + +.color-blue-200 { + color: #bfc3fa !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-blue-200, +[type=reset].button-blue-200 { + color: #1b1b1b; + border-color: #bfc3fa; + background-color: #bfc3fa; +} + +:is(a).color-blue-200 { + text-decoration-color: #bfc3fa !important; +} + +.color-blue-150 { + color: #d0d2fa !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-blue-150, +[type=reset].button-blue-150 { + color: #1b1b1b; + border-color: #d0d2fa; + background-color: #d0d2fa; +} + +:is(a).color-blue-150 { + text-decoration-color: #d0d2fa !important; +} + +.color-blue-100 { + color: #e0e1fa !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-blue-100, +[type=reset].button-blue-100 { + color: #1b1b1b; + border-color: #e0e1fa; + background-color: #e0e1fa; +} + +:is(a).color-blue-100 { + text-decoration-color: #e0e1fa !important; +} + +.color-blue-50 { + color: #f0f0fb !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-blue-50, +[type=reset].button-blue-50 { + color: #1b1b1b; + border-color: #f0f0fb; + background-color: #f0f0fb; +} + +:is(a).color-blue-50 { + text-decoration-color: #f0f0fb !important; +} + +.color-blue-main { + color: #2060df !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-blue-main, +[type=reset].button-blue-main { + color: #dfe3eb; + border-color: #2060df; + background-color: #2060df; +} + +:is(a).color-blue-main { + text-decoration-color: #2060df !important; +} + +.color-azure-950 { + color: #04121d !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-azure-950, +[type=reset].button-azure-950 { + color: #dfe3eb; + border-color: #04121d; + background-color: #04121d; +} + +:is(a).color-azure-950 { + text-decoration-color: #04121d !important; +} + +.color-azure-900 { + color: #061e2f !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-azure-900, +[type=reset].button-azure-900 { + color: #dfe3eb; + border-color: #061e2f; + background-color: #061e2f; +} + +:is(a).color-azure-900 { + text-decoration-color: #061e2f !important; +} + +.color-azure-850 { + color: #052940 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-azure-850, +[type=reset].button-azure-850 { + color: #dfe3eb; + border-color: #052940; + background-color: #052940; +} + +:is(a).color-azure-850 { + text-decoration-color: #052940 !important; +} + +.color-azure-800 { + color: #033452 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-azure-800, +[type=reset].button-azure-800 { + color: #dfe3eb; + border-color: #033452; + background-color: #033452; +} + +:is(a).color-azure-800 { + text-decoration-color: #033452 !important; +} + +.color-azure-750 { + color: #014063 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-azure-750, +[type=reset].button-azure-750 { + color: #dfe3eb; + border-color: #014063; + background-color: #014063; +} + +:is(a).color-azure-750 { + text-decoration-color: #014063 !important; +} + +.color-azure-700 { + color: #014c75 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-azure-700, +[type=reset].button-azure-700 { + color: #dfe3eb; + border-color: #014c75; + background-color: #014c75; +} + +:is(a).color-azure-700 { + text-decoration-color: #014c75 !important; +} + +.color-azure-650 { + color: #015887 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-azure-650, +[type=reset].button-azure-650 { + color: #dfe3eb; + border-color: #015887; + background-color: #015887; +} + +:is(a).color-azure-650 { + text-decoration-color: #015887 !important; +} + +.color-azure-600 { + color: #02659a !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-azure-600, +[type=reset].button-azure-600 { + color: #dfe3eb; + border-color: #02659a; + background-color: #02659a; +} + +:is(a).color-azure-600 { + text-decoration-color: #02659a !important; +} + +.color-azure-550 { + color: #0172ad !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-azure-550, +[type=reset].button-azure-550 { + color: #dfe3eb; + border-color: #0172ad; + background-color: #0172ad; +} + +:is(a).color-azure-550 { + text-decoration-color: #0172ad !important; +} + +.color-azure-500, .color-azure { + color: #017fc0 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-azure-500, .button-azure:is(button, [type=submit], [type=button], [role=button]), +[type=reset].button-azure-500, +[type=reset].button-azure { + color: #dfe3eb; + border-color: #017fc0; + background-color: #017fc0; +} + +:is(a).color-azure-500, .color-azure:is(a) { + text-decoration-color: #017fc0 !important; +} + +.color-azure-450 { + color: #018cd4 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-azure-450, +[type=reset].button-azure-450 { + color: #1b1b1b; + border-color: #018cd4; + background-color: #018cd4; +} + +:is(a).color-azure-450 { + text-decoration-color: #018cd4 !important; +} + +.color-azure-400 { + color: #029ae8 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-azure-400, +[type=reset].button-azure-400 { + color: #1b1b1b; + border-color: #029ae8; + background-color: #029ae8; +} + +:is(a).color-azure-400 { + text-decoration-color: #029ae8 !important; +} + +.color-azure-350 { + color: #01aaff !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-azure-350, +[type=reset].button-azure-350 { + color: #1b1b1b; + border-color: #01aaff; + background-color: #01aaff; +} + +:is(a).color-azure-350 { + text-decoration-color: #01aaff !important; +} + +.color-azure-300 { + color: #51b4ff !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-azure-300, +[type=reset].button-azure-300 { + color: #1b1b1b; + border-color: #51b4ff; + background-color: #51b4ff; +} + +:is(a).color-azure-300 { + text-decoration-color: #51b4ff !important; +} + +.color-azure-250 { + color: #79c0ff !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-azure-250, +[type=reset].button-azure-250 { + color: #1b1b1b; + border-color: #79c0ff; + background-color: #79c0ff; +} + +:is(a).color-azure-250 { + text-decoration-color: #79c0ff !important; +} + +.color-azure-200 { + color: #9bccfd !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-azure-200, +[type=reset].button-azure-200 { + color: #1b1b1b; + border-color: #9bccfd; + background-color: #9bccfd; +} + +:is(a).color-azure-200 { + text-decoration-color: #9bccfd !important; +} + +.color-azure-150 { + color: #b7d9fc !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-azure-150, +[type=reset].button-azure-150 { + color: #1b1b1b; + border-color: #b7d9fc; + background-color: #b7d9fc; +} + +:is(a).color-azure-150 { + text-decoration-color: #b7d9fc !important; +} + +.color-azure-100 { + color: #d1e5fb !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-azure-100, +[type=reset].button-azure-100 { + color: #1b1b1b; + border-color: #d1e5fb; + background-color: #d1e5fb; +} + +:is(a).color-azure-100 { + text-decoration-color: #d1e5fb !important; +} + +.color-azure-50 { + color: #e9f2fc !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-azure-50, +[type=reset].button-azure-50 { + color: #1b1b1b; + border-color: #e9f2fc; + background-color: #e9f2fc; +} + +:is(a).color-azure-50 { + text-decoration-color: #e9f2fc !important; +} + +.color-azure-main { + color: #0172ad !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-azure-main, +[type=reset].button-azure-main { + color: #dfe3eb; + border-color: #0172ad; + background-color: #0172ad; +} + +:is(a).color-azure-main { + text-decoration-color: #0172ad !important; +} + +.color-cyan-950 { + color: #041413 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-cyan-950, +[type=reset].button-cyan-950 { + color: #dfe3eb; + border-color: #041413; + background-color: #041413; +} + +:is(a).color-cyan-950 { + text-decoration-color: #041413 !important; +} + +.color-cyan-900 { + color: #051f1f !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-cyan-900, +[type=reset].button-cyan-900 { + color: #dfe3eb; + border-color: #051f1f; + background-color: #051f1f; +} + +:is(a).color-cyan-900 { + text-decoration-color: #051f1f !important; +} + +.color-cyan-850 { + color: #052b2b !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-cyan-850, +[type=reset].button-cyan-850 { + color: #dfe3eb; + border-color: #052b2b; + background-color: #052b2b; +} + +:is(a).color-cyan-850 { + text-decoration-color: #052b2b !important; +} + +.color-cyan-800 { + color: #043737 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-cyan-800, +[type=reset].button-cyan-800 { + color: #dfe3eb; + border-color: #043737; + background-color: #043737; +} + +:is(a).color-cyan-800 { + text-decoration-color: #043737 !important; +} + +.color-cyan-750 { + color: #014343 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-cyan-750, +[type=reset].button-cyan-750 { + color: #dfe3eb; + border-color: #014343; + background-color: #014343; +} + +:is(a).color-cyan-750 { + text-decoration-color: #014343 !important; +} + +.color-cyan-700 { + color: #015050 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-cyan-700, +[type=reset].button-cyan-700 { + color: #dfe3eb; + border-color: #015050; + background-color: #015050; +} + +:is(a).color-cyan-700 { + text-decoration-color: #015050 !important; +} + +.color-cyan-650 { + color: #025d5d !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-cyan-650, +[type=reset].button-cyan-650 { + color: #dfe3eb; + border-color: #025d5d; + background-color: #025d5d; +} + +:is(a).color-cyan-650 { + text-decoration-color: #025d5d !important; +} + +.color-cyan-600 { + color: #046a6a !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-cyan-600, +[type=reset].button-cyan-600 { + color: #dfe3eb; + border-color: #046a6a; + background-color: #046a6a; +} + +:is(a).color-cyan-600 { + text-decoration-color: #046a6a !important; +} + +.color-cyan-550 { + color: #047878 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-cyan-550, +[type=reset].button-cyan-550 { + color: #dfe3eb; + border-color: #047878; + background-color: #047878; +} + +:is(a).color-cyan-550 { + text-decoration-color: #047878 !important; +} + +.color-cyan-500, .color-cyan { + color: #058686 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-cyan-500, .button-cyan:is(button, [type=submit], [type=button], [role=button]), +[type=reset].button-cyan-500, +[type=reset].button-cyan { + color: #dfe3eb; + border-color: #058686; + background-color: #058686; +} + +:is(a).color-cyan-500, .color-cyan:is(a) { + text-decoration-color: #058686 !important; +} + +.color-cyan-450 { + color: #059494 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-cyan-450, +[type=reset].button-cyan-450 { + color: #1b1b1b; + border-color: #059494; + background-color: #059494; +} + +:is(a).color-cyan-450 { + text-decoration-color: #059494 !important; +} + +.color-cyan-400 { + color: #05a2a2 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-cyan-400, +[type=reset].button-cyan-400 { + color: #1b1b1b; + border-color: #05a2a2; + background-color: #05a2a2; +} + +:is(a).color-cyan-400 { + text-decoration-color: #05a2a2 !important; +} + +.color-cyan-350 { + color: #0ab1b1 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-cyan-350, +[type=reset].button-cyan-350 { + color: #1b1b1b; + border-color: #0ab1b1; + background-color: #0ab1b1; +} + +:is(a).color-cyan-350 { + text-decoration-color: #0ab1b1 !important; +} + +.color-cyan-300 { + color: #0ac2c2 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-cyan-300, +[type=reset].button-cyan-300 { + color: #1b1b1b; + border-color: #0ac2c2; + background-color: #0ac2c2; +} + +:is(a).color-cyan-300 { + text-decoration-color: #0ac2c2 !important; +} + +.color-cyan-250 { + color: #0ccece !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-cyan-250, +[type=reset].button-cyan-250 { + color: #1b1b1b; + border-color: #0ccece; + background-color: #0ccece; +} + +:is(a).color-cyan-250 { + text-decoration-color: #0ccece !important; +} + +.color-cyan-200 { + color: #25dddd !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-cyan-200, +[type=reset].button-cyan-200 { + color: #1b1b1b; + border-color: #25dddd; + background-color: #25dddd; +} + +:is(a).color-cyan-200 { + text-decoration-color: #25dddd !important; +} + +.color-cyan-150 { + color: #3deceb !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-cyan-150, +[type=reset].button-cyan-150 { + color: #1b1b1b; + border-color: #3deceb; + background-color: #3deceb; +} + +:is(a).color-cyan-150 { + text-decoration-color: #3deceb !important; +} + +.color-cyan-100 { + color: #58faf9 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-cyan-100, +[type=reset].button-cyan-100 { + color: #1b1b1b; + border-color: #58faf9; + background-color: #58faf9; +} + +:is(a).color-cyan-100 { + text-decoration-color: #58faf9 !important; +} + +.color-cyan-50 { + color: #c3fcfa !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-cyan-50, +[type=reset].button-cyan-50 { + color: #1b1b1b; + border-color: #c3fcfa; + background-color: #c3fcfa; +} + +:is(a).color-cyan-50 { + text-decoration-color: #c3fcfa !important; +} + +.color-cyan-main { + color: #047878 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-cyan-main, +[type=reset].button-cyan-main { + color: #dfe3eb; + border-color: #047878; + background-color: #047878; +} + +:is(a).color-cyan-main { + text-decoration-color: #047878 !important; +} + +.color-jade-950 { + color: #04140c !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-jade-950, +[type=reset].button-jade-950 { + color: #dfe3eb; + border-color: #04140c; + background-color: #04140c; +} + +:is(a).color-jade-950 { + text-decoration-color: #04140c !important; +} + +.color-jade-900 { + color: #052014 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-jade-900, +[type=reset].button-jade-900 { + color: #dfe3eb; + border-color: #052014; + background-color: #052014; +} + +:is(a).color-jade-900 { + text-decoration-color: #052014 !important; +} + +.color-jade-850 { + color: #042c1b !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-jade-850, +[type=reset].button-jade-850 { + color: #dfe3eb; + border-color: #042c1b; + background-color: #042c1b; +} + +:is(a).color-jade-850 { + text-decoration-color: #042c1b !important; +} + +.color-jade-800 { + color: #033823 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-jade-800, +[type=reset].button-jade-800 { + color: #dfe3eb; + border-color: #033823; + background-color: #033823; +} + +:is(a).color-jade-800 { + text-decoration-color: #033823 !important; +} + +.color-jade-750 { + color: #00452b !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-jade-750, +[type=reset].button-jade-750 { + color: #dfe3eb; + border-color: #00452b; + background-color: #00452b; +} + +:is(a).color-jade-750 { + text-decoration-color: #00452b !important; +} + +.color-jade-700 { + color: #015234 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-jade-700, +[type=reset].button-jade-700 { + color: #dfe3eb; + border-color: #015234; + background-color: #015234; +} + +:is(a).color-jade-700 { + text-decoration-color: #015234 !important; +} + +.color-jade-650 { + color: #005f3d !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-jade-650, +[type=reset].button-jade-650 { + color: #dfe3eb; + border-color: #005f3d; + background-color: #005f3d; +} + +:is(a).color-jade-650 { + text-decoration-color: #005f3d !important; +} + +.color-jade-600 { + color: #006d46 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-jade-600, +[type=reset].button-jade-600 { + color: #dfe3eb; + border-color: #006d46; + background-color: #006d46; +} + +:is(a).color-jade-600 { + text-decoration-color: #006d46 !important; +} + +.color-jade-550 { + color: #007a50 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-jade-550, +[type=reset].button-jade-550 { + color: #dfe3eb; + border-color: #007a50; + background-color: #007a50; +} + +:is(a).color-jade-550 { + text-decoration-color: #007a50 !important; +} + +.color-jade-500, .color-jade { + color: #00895a !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-jade-500, .button-jade:is(button, [type=submit], [type=button], [role=button]), +[type=reset].button-jade-500, +[type=reset].button-jade { + color: #dfe3eb; + border-color: #00895a; + background-color: #00895a; +} + +:is(a).color-jade-500, .color-jade:is(a) { + text-decoration-color: #00895a !important; +} + +.color-jade-450 { + color: #029764 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-jade-450, +[type=reset].button-jade-450 { + color: #1b1b1b; + border-color: #029764; + background-color: #029764; +} + +:is(a).color-jade-450 { + text-decoration-color: #029764 !important; +} + +.color-jade-400 { + color: #00a66e !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-jade-400, +[type=reset].button-jade-400 { + color: #1b1b1b; + border-color: #00a66e; + background-color: #00a66e; +} + +:is(a).color-jade-400 { + text-decoration-color: #00a66e !important; +} + +.color-jade-350 { + color: #00b478 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-jade-350, +[type=reset].button-jade-350 { + color: #1b1b1b; + border-color: #00b478; + background-color: #00b478; +} + +:is(a).color-jade-350 { + text-decoration-color: #00b478 !important; +} + +.color-jade-300 { + color: #00c482 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-jade-300, +[type=reset].button-jade-300 { + color: #1b1b1b; + border-color: #00c482; + background-color: #00c482; +} + +:is(a).color-jade-300 { + text-decoration-color: #00c482 !important; +} + +.color-jade-250 { + color: #00cc88 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-jade-250, +[type=reset].button-jade-250 { + color: #1b1b1b; + border-color: #00cc88; + background-color: #00cc88; +} + +:is(a).color-jade-250 { + text-decoration-color: #00cc88 !important; +} + +.color-jade-200 { + color: #21e299 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-jade-200, +[type=reset].button-jade-200 { + color: #1b1b1b; + border-color: #21e299; + background-color: #21e299; +} + +:is(a).color-jade-200 { + text-decoration-color: #21e299 !important; +} + +.color-jade-150 { + color: #39f1a6 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-jade-150, +[type=reset].button-jade-150 { + color: #1b1b1b; + border-color: #39f1a6; + background-color: #39f1a6; +} + +:is(a).color-jade-150 { + text-decoration-color: #39f1a6 !important; +} + +.color-jade-100 { + color: #70fcba !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-jade-100, +[type=reset].button-jade-100 { + color: #1b1b1b; + border-color: #70fcba; + background-color: #70fcba; +} + +:is(a).color-jade-100 { + text-decoration-color: #70fcba !important; +} + +.color-jade-50 { + color: #cbfce1 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-jade-50, +[type=reset].button-jade-50 { + color: #1b1b1b; + border-color: #cbfce1; + background-color: #cbfce1; +} + +:is(a).color-jade-50 { + text-decoration-color: #cbfce1 !important; +} + +.color-jade-main { + color: #007a50 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-jade-main, +[type=reset].button-jade-main { + color: #dfe3eb; + border-color: #007a50; + background-color: #007a50; +} + +:is(a).color-jade-main { + text-decoration-color: #007a50 !important; +} + +.color-green-950 { + color: #0b1305 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-green-950, +[type=reset].button-green-950 { + color: #dfe3eb; + border-color: #0b1305; + background-color: #0b1305; +} + +:is(a).color-green-950 { + text-decoration-color: #0b1305 !important; +} + +.color-green-900 { + color: #131f07 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-green-900, +[type=reset].button-green-900 { + color: #dfe3eb; + border-color: #131f07; + background-color: #131f07; +} + +:is(a).color-green-900 { + text-decoration-color: #131f07 !important; +} + +.color-green-850 { + color: #152b07 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-green-850, +[type=reset].button-green-850 { + color: #dfe3eb; + border-color: #152b07; + background-color: #152b07; +} + +:is(a).color-green-850 { + text-decoration-color: #152b07 !important; +} + +.color-green-800 { + color: #173806 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-green-800, +[type=reset].button-green-800 { + color: #dfe3eb; + border-color: #173806; + background-color: #173806; +} + +:is(a).color-green-800 { + text-decoration-color: #173806 !important; +} + +.color-green-750 { + color: #1a4405 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-green-750, +[type=reset].button-green-750 { + color: #dfe3eb; + border-color: #1a4405; + background-color: #1a4405; +} + +:is(a).color-green-750 { + text-decoration-color: #1a4405 !important; +} + +.color-green-700 { + color: #205107 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-green-700, +[type=reset].button-green-700 { + color: #dfe3eb; + border-color: #205107; + background-color: #205107; +} + +:is(a).color-green-700 { + text-decoration-color: #205107 !important; +} + +.color-green-650 { + color: #265e09 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-green-650, +[type=reset].button-green-650 { + color: #dfe3eb; + border-color: #265e09; + background-color: #265e09; +} + +:is(a).color-green-650 { + text-decoration-color: #265e09 !important; +} + +.color-green-600 { + color: #2c6c0c !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-green-600, +[type=reset].button-green-600 { + color: #dfe3eb; + border-color: #2c6c0c; + background-color: #2c6c0c; +} + +:is(a).color-green-600 { + text-decoration-color: #2c6c0c !important; +} + +.color-green-550 { + color: #33790f !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-green-550, +[type=reset].button-green-550 { + color: #dfe3eb; + border-color: #33790f; + background-color: #33790f; +} + +:is(a).color-green-550 { + text-decoration-color: #33790f !important; +} + +.color-green-500, .color-green { + color: #398712 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-green-500, .button-green:is(button, [type=submit], [type=button], [role=button]), +[type=reset].button-green-500, +[type=reset].button-green { + color: #dfe3eb; + border-color: #398712; + background-color: #398712; +} + +:is(a).color-green-500, .color-green:is(a) { + text-decoration-color: #398712 !important; +} + +.color-green-450 { + color: #409614 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-green-450, +[type=reset].button-green-450 { + color: #1b1b1b; + border-color: #409614; + background-color: #409614; +} + +:is(a).color-green-450 { + text-decoration-color: #409614 !important; +} + +.color-green-400 { + color: #47a417 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-green-400, +[type=reset].button-green-400 { + color: #1b1b1b; + border-color: #47a417; + background-color: #47a417; +} + +:is(a).color-green-400 { + text-decoration-color: #47a417 !important; +} + +.color-green-350 { + color: #4eb31b !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-green-350, +[type=reset].button-green-350 { + color: #1b1b1b; + border-color: #4eb31b; + background-color: #4eb31b; +} + +:is(a).color-green-350 { + text-decoration-color: #4eb31b !important; +} + +.color-green-300 { + color: #55c21e !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-green-300, +[type=reset].button-green-300 { + color: #1b1b1b; + border-color: #55c21e; + background-color: #55c21e; +} + +:is(a).color-green-300 { + text-decoration-color: #55c21e !important; +} + +.color-green-250 { + color: #5dd121 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-green-250, +[type=reset].button-green-250 { + color: #1b1b1b; + border-color: #5dd121; + background-color: #5dd121; +} + +:is(a).color-green-250 { + text-decoration-color: #5dd121 !important; +} + +.color-green-200 { + color: #62d926 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-green-200, +[type=reset].button-green-200 { + color: #1b1b1b; + border-color: #62d926; + background-color: #62d926; +} + +:is(a).color-green-200 { + text-decoration-color: #62d926 !important; +} + +.color-green-150 { + color: #77ef3d !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-green-150, +[type=reset].button-green-150 { + color: #1b1b1b; + border-color: #77ef3d; + background-color: #77ef3d; +} + +:is(a).color-green-150 { + text-decoration-color: #77ef3d !important; +} + +.color-green-100 { + color: #95fb62 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-green-100, +[type=reset].button-green-100 { + color: #1b1b1b; + border-color: #95fb62; + background-color: #95fb62; +} + +:is(a).color-green-100 { + text-decoration-color: #95fb62 !important; +} + +.color-green-50 { + color: #d7fbc1 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-green-50, +[type=reset].button-green-50 { + color: #1b1b1b; + border-color: #d7fbc1; + background-color: #d7fbc1; +} + +:is(a).color-green-50 { + text-decoration-color: #d7fbc1 !important; +} + +.color-green-main { + color: #398712 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-green-main, +[type=reset].button-green-main { + color: #dfe3eb; + border-color: #398712; + background-color: #398712; +} + +:is(a).color-green-main { + text-decoration-color: #398712 !important; +} + +.color-lime-950 { + color: #101203 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-lime-950, +[type=reset].button-lime-950 { + color: #dfe3eb; + border-color: #101203; + background-color: #101203; +} + +:is(a).color-lime-950 { + text-decoration-color: #101203 !important; +} + +.color-lime-900 { + color: #191d03 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-lime-900, +[type=reset].button-lime-900 { + color: #dfe3eb; + border-color: #191d03; + background-color: #191d03; +} + +:is(a).color-lime-900 { + text-decoration-color: #191d03 !important; +} + +.color-lime-850 { + color: #202902 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-lime-850, +[type=reset].button-lime-850 { + color: #dfe3eb; + border-color: #202902; + background-color: #202902; +} + +:is(a).color-lime-850 { + text-decoration-color: #202902 !important; +} + +.color-lime-800 { + color: #273500 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-lime-800, +[type=reset].button-lime-800 { + color: #dfe3eb; + border-color: #273500; + background-color: #273500; +} + +:is(a).color-lime-800 { + text-decoration-color: #273500 !important; +} + +.color-lime-750 { + color: #304100 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-lime-750, +[type=reset].button-lime-750 { + color: #dfe3eb; + border-color: #304100; + background-color: #304100; +} + +:is(a).color-lime-750 { + text-decoration-color: #304100 !important; +} + +.color-lime-700 { + color: #394d00 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-lime-700, +[type=reset].button-lime-700 { + color: #dfe3eb; + border-color: #394d00; + background-color: #394d00; +} + +:is(a).color-lime-700 { + text-decoration-color: #394d00 !important; +} + +.color-lime-650 { + color: #435a00 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-lime-650, +[type=reset].button-lime-650 { + color: #dfe3eb; + border-color: #435a00; + background-color: #435a00; +} + +:is(a).color-lime-650 { + text-decoration-color: #435a00 !important; +} + +.color-lime-600 { + color: #4d6600 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-lime-600, +[type=reset].button-lime-600 { + color: #dfe3eb; + border-color: #4d6600; + background-color: #4d6600; +} + +:is(a).color-lime-600 { + text-decoration-color: #4d6600 !important; +} + +.color-lime-550 { + color: #577400 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-lime-550, +[type=reset].button-lime-550 { + color: #dfe3eb; + border-color: #577400; + background-color: #577400; +} + +:is(a).color-lime-550 { + text-decoration-color: #577400 !important; +} + +.color-lime-500, .color-lime { + color: #628100 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-lime-500, .button-lime:is(button, [type=submit], [type=button], [role=button]), +[type=reset].button-lime-500, +[type=reset].button-lime { + color: #dfe3eb; + border-color: #628100; + background-color: #628100; +} + +:is(a).color-lime-500, .color-lime:is(a) { + text-decoration-color: #628100 !important; +} + +.color-lime-450 { + color: #6c8f00 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-lime-450, +[type=reset].button-lime-450 { + color: #1b1b1b; + border-color: #6c8f00; + background-color: #6c8f00; +} + +:is(a).color-lime-450 { + text-decoration-color: #6c8f00 !important; +} + +.color-lime-400 { + color: #779c00 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-lime-400, +[type=reset].button-lime-400 { + color: #1b1b1b; + border-color: #779c00; + background-color: #779c00; +} + +:is(a).color-lime-400 { + text-decoration-color: #779c00 !important; +} + +.color-lime-350 { + color: #82ab00 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-lime-350, +[type=reset].button-lime-350 { + color: #1b1b1b; + border-color: #82ab00; + background-color: #82ab00; +} + +:is(a).color-lime-350 { + text-decoration-color: #82ab00 !important; +} + +.color-lime-300 { + color: #8eb901 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-lime-300, +[type=reset].button-lime-300 { + color: #1b1b1b; + border-color: #8eb901; + background-color: #8eb901; +} + +:is(a).color-lime-300 { + text-decoration-color: #8eb901 !important; +} + +.color-lime-250 { + color: #99c801 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-lime-250, +[type=reset].button-lime-250 { + color: #1b1b1b; + border-color: #99c801; + background-color: #99c801; +} + +:is(a).color-lime-250 { + text-decoration-color: #99c801 !important; +} + +.color-lime-200 { + color: #a5d601 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-lime-200, +[type=reset].button-lime-200 { + color: #1b1b1b; + border-color: #a5d601; + background-color: #a5d601; +} + +:is(a).color-lime-200 { + text-decoration-color: #a5d601 !important; +} + +.color-lime-150 { + color: #b2e51a !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-lime-150, +[type=reset].button-lime-150 { + color: #1b1b1b; + border-color: #b2e51a; + background-color: #b2e51a; +} + +:is(a).color-lime-150 { + text-decoration-color: #b2e51a !important; +} + +.color-lime-100 { + color: #c1f335 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-lime-100, +[type=reset].button-lime-100 { + color: #1b1b1b; + border-color: #c1f335; + background-color: #c1f335; +} + +:is(a).color-lime-100 { + text-decoration-color: #c1f335 !important; +} + +.color-lime-50 { + color: #defc85 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-lime-50, +[type=reset].button-lime-50 { + color: #1b1b1b; + border-color: #defc85; + background-color: #defc85; +} + +:is(a).color-lime-50 { + text-decoration-color: #defc85 !important; +} + +.color-lime-main { + color: #a5d601 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-lime-main, +[type=reset].button-lime-main { + color: #1b1b1b; + border-color: #a5d601; + background-color: #a5d601; +} + +:is(a).color-lime-main { + text-decoration-color: #a5d601 !important; +} + +.color-yellow-950 { + color: #141103 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-yellow-950, +[type=reset].button-yellow-950 { + color: #dfe3eb; + border-color: #141103; + background-color: #141103; +} + +:is(a).color-yellow-950 { + text-decoration-color: #141103 !important; +} + +.color-yellow-900 { + color: #1f1c02 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-yellow-900, +[type=reset].button-yellow-900 { + color: #dfe3eb; + border-color: #1f1c02; + background-color: #1f1c02; +} + +:is(a).color-yellow-900 { + text-decoration-color: #1f1c02 !important; +} + +.color-yellow-850 { + color: #2b2600 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-yellow-850, +[type=reset].button-yellow-850 { + color: #dfe3eb; + border-color: #2b2600; + background-color: #2b2600; +} + +:is(a).color-yellow-850 { + text-decoration-color: #2b2600 !important; +} + +.color-yellow-800 { + color: #363100 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-yellow-800, +[type=reset].button-yellow-800 { + color: #dfe3eb; + border-color: #363100; + background-color: #363100; +} + +:is(a).color-yellow-800 { + text-decoration-color: #363100 !important; +} + +.color-yellow-750 { + color: #423c00 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-yellow-750, +[type=reset].button-yellow-750 { + color: #dfe3eb; + border-color: #423c00; + background-color: #423c00; +} + +:is(a).color-yellow-750 { + text-decoration-color: #423c00 !important; +} + +.color-yellow-700 { + color: #4e4700 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-yellow-700, +[type=reset].button-yellow-700 { + color: #dfe3eb; + border-color: #4e4700; + background-color: #4e4700; +} + +:is(a).color-yellow-700 { + text-decoration-color: #4e4700 !important; +} + +.color-yellow-650 { + color: #5b5300 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-yellow-650, +[type=reset].button-yellow-650 { + color: #dfe3eb; + border-color: #5b5300; + background-color: #5b5300; +} + +:is(a).color-yellow-650 { + text-decoration-color: #5b5300 !important; +} + +.color-yellow-600 { + color: #685f00 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-yellow-600, +[type=reset].button-yellow-600 { + color: #dfe3eb; + border-color: #685f00; + background-color: #685f00; +} + +:is(a).color-yellow-600 { + text-decoration-color: #685f00 !important; +} + +.color-yellow-550 { + color: #756b00 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-yellow-550, +[type=reset].button-yellow-550 { + color: #dfe3eb; + border-color: #756b00; + background-color: #756b00; +} + +:is(a).color-yellow-550 { + text-decoration-color: #756b00 !important; +} + +.color-yellow-500, .color-yellow { + color: #827800 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-yellow-500, .button-yellow:is(button, [type=submit], [type=button], [role=button]), +[type=reset].button-yellow-500, +[type=reset].button-yellow { + color: #1b1b1b; + border-color: #827800; + background-color: #827800; +} + +:is(a).color-yellow-500, .color-yellow:is(a) { + text-decoration-color: #827800 !important; +} + +.color-yellow-450 { + color: #908501 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-yellow-450, +[type=reset].button-yellow-450 { + color: #1b1b1b; + border-color: #908501; + background-color: #908501; +} + +:is(a).color-yellow-450 { + text-decoration-color: #908501 !important; +} + +.color-yellow-400 { + color: #9e9200 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-yellow-400, +[type=reset].button-yellow-400 { + color: #1b1b1b; + border-color: #9e9200; + background-color: #9e9200; +} + +:is(a).color-yellow-400 { + text-decoration-color: #9e9200 !important; +} + +.color-yellow-350 { + color: #ad9f00 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-yellow-350, +[type=reset].button-yellow-350 { + color: #1b1b1b; + border-color: #ad9f00; + background-color: #ad9f00; +} + +:is(a).color-yellow-350 { + text-decoration-color: #ad9f00 !important; +} + +.color-yellow-300 { + color: #bbac00 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-yellow-300, +[type=reset].button-yellow-300 { + color: #1b1b1b; + border-color: #bbac00; + background-color: #bbac00; +} + +:is(a).color-yellow-300 { + text-decoration-color: #bbac00 !important; +} + +.color-yellow-250 { + color: #caba01 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-yellow-250, +[type=reset].button-yellow-250 { + color: #1b1b1b; + border-color: #caba01; + background-color: #caba01; +} + +:is(a).color-yellow-250 { + text-decoration-color: #caba01 !important; +} + +.color-yellow-200 { + color: #d9c800 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-yellow-200, +[type=reset].button-yellow-200 { + color: #1b1b1b; + border-color: #d9c800; + background-color: #d9c800; +} + +:is(a).color-yellow-200 { + text-decoration-color: #d9c800 !important; +} + +.color-yellow-150 { + color: #e8d600 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-yellow-150, +[type=reset].button-yellow-150 { + color: #1b1b1b; + border-color: #e8d600; + background-color: #e8d600; +} + +:is(a).color-yellow-150 { + text-decoration-color: #e8d600 !important; +} + +.color-yellow-100 { + color: #f2df0d !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-yellow-100, +[type=reset].button-yellow-100 { + color: #1b1b1b; + border-color: #f2df0d; + background-color: #f2df0d; +} + +:is(a).color-yellow-100 { + text-decoration-color: #f2df0d !important; +} + +.color-yellow-50 { + color: #fdf1b4 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-yellow-50, +[type=reset].button-yellow-50 { + color: #1b1b1b; + border-color: #fdf1b4; + background-color: #fdf1b4; +} + +:is(a).color-yellow-50 { + text-decoration-color: #fdf1b4 !important; +} + +.color-yellow-main { + color: #f2df0d !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-yellow-main, +[type=reset].button-yellow-main { + color: #1b1b1b; + border-color: #f2df0d; + background-color: #f2df0d; +} + +:is(a).color-yellow-main { + text-decoration-color: #f2df0d !important; +} + +.color-amber-950 { + color: #161003 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-amber-950, +[type=reset].button-amber-950 { + color: #dfe3eb; + border-color: #161003; + background-color: #161003; +} + +:is(a).color-amber-950 { + text-decoration-color: #161003 !important; +} + +.color-amber-900 { + color: #231a03 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-amber-900, +[type=reset].button-amber-900 { + color: #dfe3eb; + border-color: #231a03; + background-color: #231a03; +} + +:is(a).color-amber-900 { + text-decoration-color: #231a03 !important; +} + +.color-amber-850 { + color: #312302 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-amber-850, +[type=reset].button-amber-850 { + color: #dfe3eb; + border-color: #312302; + background-color: #312302; +} + +:is(a).color-amber-850 { + text-decoration-color: #312302 !important; +} + +.color-amber-800 { + color: #3f2d00 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-amber-800, +[type=reset].button-amber-800 { + color: #dfe3eb; + border-color: #3f2d00; + background-color: #3f2d00; +} + +:is(a).color-amber-800 { + text-decoration-color: #3f2d00 !important; +} + +.color-amber-750 { + color: #4d3700 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-amber-750, +[type=reset].button-amber-750 { + color: #dfe3eb; + border-color: #4d3700; + background-color: #4d3700; +} + +:is(a).color-amber-750 { + text-decoration-color: #4d3700 !important; +} + +.color-amber-700 { + color: #5b4200 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-amber-700, +[type=reset].button-amber-700 { + color: #dfe3eb; + border-color: #5b4200; + background-color: #5b4200; +} + +:is(a).color-amber-700 { + text-decoration-color: #5b4200 !important; +} + +.color-amber-650 { + color: #694d00 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-amber-650, +[type=reset].button-amber-650 { + color: #dfe3eb; + border-color: #694d00; + background-color: #694d00; +} + +:is(a).color-amber-650 { + text-decoration-color: #694d00 !important; +} + +.color-amber-600 { + color: #785800 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-amber-600, +[type=reset].button-amber-600 { + color: #dfe3eb; + border-color: #785800; + background-color: #785800; +} + +:is(a).color-amber-600 { + text-decoration-color: #785800 !important; +} + +.color-amber-550 { + color: #876400 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-amber-550, +[type=reset].button-amber-550 { + color: #dfe3eb; + border-color: #876400; + background-color: #876400; +} + +:is(a).color-amber-550 { + text-decoration-color: #876400 !important; +} + +.color-amber-500, .color-amber { + color: #977000 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-amber-500, .button-amber:is(button, [type=submit], [type=button], [role=button]), +[type=reset].button-amber-500, +[type=reset].button-amber { + color: #dfe3eb; + border-color: #977000; + background-color: #977000; +} + +:is(a).color-amber-500, .color-amber:is(a) { + text-decoration-color: #977000 !important; +} + +.color-amber-450 { + color: #a77c00 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-amber-450, +[type=reset].button-amber-450 { + color: #1b1b1b; + border-color: #a77c00; + background-color: #a77c00; +} + +:is(a).color-amber-450 { + text-decoration-color: #a77c00 !important; +} + +.color-amber-400 { + color: #b78800 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-amber-400, +[type=reset].button-amber-400 { + color: #1b1b1b; + border-color: #b78800; + background-color: #b78800; +} + +:is(a).color-amber-400 { + text-decoration-color: #b78800 !important; +} + +.color-amber-350 { + color: #c79400 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-amber-350, +[type=reset].button-amber-350 { + color: #1b1b1b; + border-color: #c79400; + background-color: #c79400; +} + +:is(a).color-amber-350 { + text-decoration-color: #c79400 !important; +} + +.color-amber-300 { + color: #d8a100 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-amber-300, +[type=reset].button-amber-300 { + color: #1b1b1b; + border-color: #d8a100; + background-color: #d8a100; +} + +:is(a).color-amber-300 { + text-decoration-color: #d8a100 !important; +} + +.color-amber-250 { + color: #e8ae01 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-amber-250, +[type=reset].button-amber-250 { + color: #1b1b1b; + border-color: #e8ae01; + background-color: #e8ae01; +} + +:is(a).color-amber-250 { + text-decoration-color: #e8ae01 !important; +} + +.color-amber-200 { + color: #ffbf00 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-amber-200, +[type=reset].button-amber-200 { + color: #1b1b1b; + border-color: #ffbf00; + background-color: #ffbf00; +} + +:is(a).color-amber-200 { + text-decoration-color: #ffbf00 !important; +} + +.color-amber-150 { + color: #fecc63 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-amber-150, +[type=reset].button-amber-150 { + color: #1b1b1b; + border-color: #fecc63; + background-color: #fecc63; +} + +:is(a).color-amber-150 { + text-decoration-color: #fecc63 !important; +} + +.color-amber-100 { + color: #fddea6 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-amber-100, +[type=reset].button-amber-100 { + color: #1b1b1b; + border-color: #fddea6; + background-color: #fddea6; +} + +:is(a).color-amber-100 { + text-decoration-color: #fddea6 !important; +} + +.color-amber-50 { + color: #fcefd9 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-amber-50, +[type=reset].button-amber-50 { + color: #1b1b1b; + border-color: #fcefd9; + background-color: #fcefd9; +} + +:is(a).color-amber-50 { + text-decoration-color: #fcefd9 !important; +} + +.color-amber-main { + color: #ffbf00 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-amber-main, +[type=reset].button-amber-main { + color: #1b1b1b; + border-color: #ffbf00; + background-color: #ffbf00; +} + +:is(a).color-amber-main { + text-decoration-color: #ffbf00 !important; +} + +.color-pumpkin-950 { + color: #180f04 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pumpkin-950, +[type=reset].button-pumpkin-950 { + color: #dfe3eb; + border-color: #180f04; + background-color: #180f04; +} + +:is(a).color-pumpkin-950 { + text-decoration-color: #180f04 !important; +} + +.color-pumpkin-900 { + color: #271805 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pumpkin-900, +[type=reset].button-pumpkin-900 { + color: #dfe3eb; + border-color: #271805; + background-color: #271805; +} + +:is(a).color-pumpkin-900 { + text-decoration-color: #271805 !important; +} + +.color-pumpkin-850 { + color: #372004 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pumpkin-850, +[type=reset].button-pumpkin-850 { + color: #dfe3eb; + border-color: #372004; + background-color: #372004; +} + +:is(a).color-pumpkin-850 { + text-decoration-color: #372004 !important; +} + +.color-pumpkin-800 { + color: #482802 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pumpkin-800, +[type=reset].button-pumpkin-800 { + color: #dfe3eb; + border-color: #482802; + background-color: #482802; +} + +:is(a).color-pumpkin-800 { + text-decoration-color: #482802 !important; +} + +.color-pumpkin-750 { + color: #593100 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pumpkin-750, +[type=reset].button-pumpkin-750 { + color: #dfe3eb; + border-color: #593100; + background-color: #593100; +} + +:is(a).color-pumpkin-750 { + text-decoration-color: #593100 !important; +} + +.color-pumpkin-700 { + color: #693a00 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pumpkin-700, +[type=reset].button-pumpkin-700 { + color: #dfe3eb; + border-color: #693a00; + background-color: #693a00; +} + +:is(a).color-pumpkin-700 { + text-decoration-color: #693a00 !important; +} + +.color-pumpkin-650 { + color: #7a4400 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pumpkin-650, +[type=reset].button-pumpkin-650 { + color: #dfe3eb; + border-color: #7a4400; + background-color: #7a4400; +} + +:is(a).color-pumpkin-650 { + text-decoration-color: #7a4400 !important; +} + +.color-pumpkin-600 { + color: #8b4f00 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pumpkin-600, +[type=reset].button-pumpkin-600 { + color: #dfe3eb; + border-color: #8b4f00; + background-color: #8b4f00; +} + +:is(a).color-pumpkin-600 { + text-decoration-color: #8b4f00 !important; +} + +.color-pumpkin-550 { + color: #9c5900 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pumpkin-550, +[type=reset].button-pumpkin-550 { + color: #dfe3eb; + border-color: #9c5900; + background-color: #9c5900; +} + +:is(a).color-pumpkin-550 { + text-decoration-color: #9c5900 !important; +} + +.color-pumpkin-500, .color-pumpkin { + color: #ad6400 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pumpkin-500, .button-pumpkin:is(button, [type=submit], [type=button], [role=button]), +[type=reset].button-pumpkin-500, +[type=reset].button-pumpkin { + color: #dfe3eb; + border-color: #ad6400; + background-color: #ad6400; +} + +:is(a).color-pumpkin-500, .color-pumpkin:is(a) { + text-decoration-color: #ad6400 !important; +} + +.color-pumpkin-450 { + color: #bf6e00 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pumpkin-450, +[type=reset].button-pumpkin-450 { + color: #1b1b1b; + border-color: #bf6e00; + background-color: #bf6e00; +} + +:is(a).color-pumpkin-450 { + text-decoration-color: #bf6e00 !important; +} + +.color-pumpkin-400 { + color: #d27a01 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pumpkin-400, +[type=reset].button-pumpkin-400 { + color: #1b1b1b; + border-color: #d27a01; + background-color: #d27a01; +} + +:is(a).color-pumpkin-400 { + text-decoration-color: #d27a01 !important; +} + +.color-pumpkin-350 { + color: #e48500 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pumpkin-350, +[type=reset].button-pumpkin-350 { + color: #1b1b1b; + border-color: #e48500; + background-color: #e48500; +} + +:is(a).color-pumpkin-350 { + text-decoration-color: #e48500 !important; +} + +.color-pumpkin-300 { + color: #ff9500 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pumpkin-300, +[type=reset].button-pumpkin-300 { + color: #1b1b1b; + border-color: #ff9500; + background-color: #ff9500; +} + +:is(a).color-pumpkin-300 { + text-decoration-color: #ff9500 !important; +} + +.color-pumpkin-250 { + color: #ffa23a !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pumpkin-250, +[type=reset].button-pumpkin-250 { + color: #1b1b1b; + border-color: #ffa23a; + background-color: #ffa23a; +} + +:is(a).color-pumpkin-250 { + text-decoration-color: #ffa23a !important; +} + +.color-pumpkin-200 { + color: #feb670 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pumpkin-200, +[type=reset].button-pumpkin-200 { + color: #1b1b1b; + border-color: #feb670; + background-color: #feb670; +} + +:is(a).color-pumpkin-200 { + text-decoration-color: #feb670 !important; +} + +.color-pumpkin-150 { + color: #fcca9b !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pumpkin-150, +[type=reset].button-pumpkin-150 { + color: #1b1b1b; + border-color: #fcca9b; + background-color: #fcca9b; +} + +:is(a).color-pumpkin-150 { + text-decoration-color: #fcca9b !important; +} + +.color-pumpkin-100 { + color: #fcdcc1 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pumpkin-100, +[type=reset].button-pumpkin-100 { + color: #1b1b1b; + border-color: #fcdcc1; + background-color: #fcdcc1; +} + +:is(a).color-pumpkin-100 { + text-decoration-color: #fcdcc1 !important; +} + +.color-pumpkin-50 { + color: #fceee3 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pumpkin-50, +[type=reset].button-pumpkin-50 { + color: #1b1b1b; + border-color: #fceee3; + background-color: #fceee3; +} + +:is(a).color-pumpkin-50 { + text-decoration-color: #fceee3 !important; +} + +.color-pumpkin-main { + color: #ff9500 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-pumpkin-main, +[type=reset].button-pumpkin-main { + color: #1b1b1b; + border-color: #ff9500; + background-color: #ff9500; +} + +:is(a).color-pumpkin-main { + text-decoration-color: #ff9500 !important; +} + +.color-orange-950 { + color: #1b0d06 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-orange-950, +[type=reset].button-orange-950 { + color: #dfe3eb; + border-color: #1b0d06; + background-color: #1b0d06; +} + +:is(a).color-orange-950 { + text-decoration-color: #1b0d06 !important; +} + +.color-orange-900 { + color: #2d1509 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-orange-900, +[type=reset].button-orange-900 { + color: #dfe3eb; + border-color: #2d1509; + background-color: #2d1509; +} + +:is(a).color-orange-900 { + text-decoration-color: #2d1509 !important; +} + +.color-orange-850 { + color: #411a0a !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-orange-850, +[type=reset].button-orange-850 { + color: #dfe3eb; + border-color: #411a0a; + background-color: #411a0a; +} + +:is(a).color-orange-850 { + text-decoration-color: #411a0a !important; +} + +.color-orange-800 { + color: #561e0a !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-orange-800, +[type=reset].button-orange-800 { + color: #dfe3eb; + border-color: #561e0a; + background-color: #561e0a; +} + +:is(a).color-orange-800 { + text-decoration-color: #561e0a !important; +} + +.color-orange-750 { + color: #6b220a !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-orange-750, +[type=reset].button-orange-750 { + color: #dfe3eb; + border-color: #6b220a; + background-color: #6b220a; +} + +:is(a).color-orange-750 { + text-decoration-color: #6b220a !important; +} + +.color-orange-700 { + color: #7f270b !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-orange-700, +[type=reset].button-orange-700 { + color: #dfe3eb; + border-color: #7f270b; + background-color: #7f270b; +} + +:is(a).color-orange-700 { + text-decoration-color: #7f270b !important; +} + +.color-orange-650 { + color: #942d0d !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-orange-650, +[type=reset].button-orange-650 { + color: #dfe3eb; + border-color: #942d0d; + background-color: #942d0d; +} + +:is(a).color-orange-650 { + text-decoration-color: #942d0d !important; +} + +.color-orange-600 { + color: #a83410 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-orange-600, +[type=reset].button-orange-600 { + color: #dfe3eb; + border-color: #a83410; + background-color: #a83410; +} + +:is(a).color-orange-600 { + text-decoration-color: #a83410 !important; +} + +.color-orange-550 { + color: #bd3c13 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-orange-550, +[type=reset].button-orange-550 { + color: #dfe3eb; + border-color: #bd3c13; + background-color: #bd3c13; +} + +:is(a).color-orange-550 { + text-decoration-color: #bd3c13 !important; +} + +.color-orange-500, .color-orange { + color: #d24317 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-orange-500, .button-orange:is(button, [type=submit], [type=button], [role=button]), +[type=reset].button-orange-500, +[type=reset].button-orange { + color: #dfe3eb; + border-color: #d24317; + background-color: #d24317; +} + +:is(a).color-orange-500, .color-orange:is(a) { + text-decoration-color: #d24317 !important; +} + +.color-orange-450 { + color: #e74b1a !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-orange-450, +[type=reset].button-orange-450 { + color: #dfe3eb; + border-color: #e74b1a; + background-color: #e74b1a; +} + +:is(a).color-orange-450 { + text-decoration-color: #e74b1a !important; +} + +.color-orange-400 { + color: #f45d2c !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-orange-400, +[type=reset].button-orange-400 { + color: #1b1b1b; + border-color: #f45d2c; + background-color: #f45d2c; +} + +:is(a).color-orange-400 { + text-decoration-color: #f45d2c !important; +} + +.color-orange-350 { + color: #f56b3d !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-orange-350, +[type=reset].button-orange-350 { + color: #1b1b1b; + border-color: #f56b3d; + background-color: #f56b3d; +} + +:is(a).color-orange-350 { + text-decoration-color: #f56b3d !important; +} + +.color-orange-300 { + color: #f68e68 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-orange-300, +[type=reset].button-orange-300 { + color: #1b1b1b; + border-color: #f68e68; + background-color: #f68e68; +} + +:is(a).color-orange-300 { + text-decoration-color: #f68e68 !important; +} + +.color-orange-250 { + color: #f8a283 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-orange-250, +[type=reset].button-orange-250 { + color: #1b1b1b; + border-color: #f8a283; + background-color: #f8a283; +} + +:is(a).color-orange-250 { + text-decoration-color: #f8a283 !important; +} + +.color-orange-200 { + color: #f8b79f !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-orange-200, +[type=reset].button-orange-200 { + color: #1b1b1b; + border-color: #f8b79f; + background-color: #f8b79f; +} + +:is(a).color-orange-200 { + text-decoration-color: #f8b79f !important; +} + +.color-orange-150 { + color: #f8cab9 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-orange-150, +[type=reset].button-orange-150 { + color: #1b1b1b; + border-color: #f8cab9; + background-color: #f8cab9; +} + +:is(a).color-orange-150 { + text-decoration-color: #f8cab9 !important; +} + +.color-orange-100 { + color: #f9dcd2 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-orange-100, +[type=reset].button-orange-100 { + color: #1b1b1b; + border-color: #f9dcd2; + background-color: #f9dcd2; +} + +:is(a).color-orange-100 { + text-decoration-color: #f9dcd2 !important; +} + +.color-orange-50 { + color: #faeeea !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-orange-50, +[type=reset].button-orange-50 { + color: #1b1b1b; + border-color: #faeeea; + background-color: #faeeea; +} + +:is(a).color-orange-50 { + text-decoration-color: #faeeea !important; +} + +.color-orange-main { + color: #d24317 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-orange-main, +[type=reset].button-orange-main { + color: #dfe3eb; + border-color: #d24317; + background-color: #d24317; +} + +:is(a).color-orange-main { + text-decoration-color: #d24317 !important; +} + +.color-sand-950 { + color: #111110 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-sand-950, +[type=reset].button-sand-950 { + color: #dfe3eb; + border-color: #111110; + background-color: #111110; +} + +:is(a).color-sand-950 { + text-decoration-color: #111110 !important; +} + +.color-sand-900 { + color: #1c1b19 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-sand-900, +[type=reset].button-sand-900 { + color: #dfe3eb; + border-color: #1c1b19; + background-color: #1c1b19; +} + +:is(a).color-sand-900 { + text-decoration-color: #1c1b19 !important; +} + +.color-sand-850 { + color: #272622 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-sand-850, +[type=reset].button-sand-850 { + color: #dfe3eb; + border-color: #272622; + background-color: #272622; +} + +:is(a).color-sand-850 { + text-decoration-color: #272622 !important; +} + +.color-sand-800 { + color: #32302b !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-sand-800, +[type=reset].button-sand-800 { + color: #dfe3eb; + border-color: #32302b; + background-color: #32302b; +} + +:is(a).color-sand-800 { + text-decoration-color: #32302b !important; +} + +.color-sand-750 { + color: #3d3b35 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-sand-750, +[type=reset].button-sand-750 { + color: #dfe3eb; + border-color: #3d3b35; + background-color: #3d3b35; +} + +:is(a).color-sand-750 { + text-decoration-color: #3d3b35 !important; +} + +.color-sand-700 { + color: #49463f !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-sand-700, +[type=reset].button-sand-700 { + color: #dfe3eb; + border-color: #49463f; + background-color: #49463f; +} + +:is(a).color-sand-700 { + text-decoration-color: #49463f !important; +} + +.color-sand-650 { + color: #55524a !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-sand-650, +[type=reset].button-sand-650 { + color: #dfe3eb; + border-color: #55524a; + background-color: #55524a; +} + +:is(a).color-sand-650 { + text-decoration-color: #55524a !important; +} + +.color-sand-600 { + color: #615e55 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-sand-600, +[type=reset].button-sand-600 { + color: #dfe3eb; + border-color: #615e55; + background-color: #615e55; +} + +:is(a).color-sand-600 { + text-decoration-color: #615e55 !important; +} + +.color-sand-550 { + color: #6e6a60 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-sand-550, +[type=reset].button-sand-550 { + color: #dfe3eb; + border-color: #6e6a60; + background-color: #6e6a60; +} + +:is(a).color-sand-550 { + text-decoration-color: #6e6a60 !important; +} + +.color-sand-500, .color-sand { + color: #7b776b !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-sand-500, .button-sand:is(button, [type=submit], [type=button], [role=button]), +[type=reset].button-sand-500, +[type=reset].button-sand { + color: #1b1b1b; + border-color: #7b776b; + background-color: #7b776b; +} + +:is(a).color-sand-500, .color-sand:is(a) { + text-decoration-color: #7b776b !important; +} + +.color-sand-450 { + color: #888377 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-sand-450, +[type=reset].button-sand-450 { + color: #1b1b1b; + border-color: #888377; + background-color: #888377; +} + +:is(a).color-sand-450 { + text-decoration-color: #888377 !important; +} + +.color-sand-400 { + color: #959082 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-sand-400, +[type=reset].button-sand-400 { + color: #1b1b1b; + border-color: #959082; + background-color: #959082; +} + +:is(a).color-sand-400 { + text-decoration-color: #959082 !important; +} + +.color-sand-350 { + color: #a39e8f !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-sand-350, +[type=reset].button-sand-350 { + color: #1b1b1b; + border-color: #a39e8f; + background-color: #a39e8f; +} + +:is(a).color-sand-350 { + text-decoration-color: #a39e8f !important; +} + +.color-sand-300 { + color: #b0ab9b !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-sand-300, +[type=reset].button-sand-300 { + color: #1b1b1b; + border-color: #b0ab9b; + background-color: #b0ab9b; +} + +:is(a).color-sand-300 { + text-decoration-color: #b0ab9b !important; +} + +.color-sand-250 { + color: #beb8a7 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-sand-250, +[type=reset].button-sand-250 { + color: #1b1b1b; + border-color: #beb8a7; + background-color: #beb8a7; +} + +:is(a).color-sand-250 { + text-decoration-color: #beb8a7 !important; +} + +.color-sand-200 { + color: #ccc6b4 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-sand-200, +[type=reset].button-sand-200 { + color: #1b1b1b; + border-color: #ccc6b4; + background-color: #ccc6b4; +} + +:is(a).color-sand-200 { + text-decoration-color: #ccc6b4 !important; +} + +.color-sand-150 { + color: #dad4c2 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-sand-150, +[type=reset].button-sand-150 { + color: #1b1b1b; + border-color: #dad4c2; + background-color: #dad4c2; +} + +:is(a).color-sand-150 { + text-decoration-color: #dad4c2 !important; +} + +.color-sand-100 { + color: #e8e2d2 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-sand-100, +[type=reset].button-sand-100 { + color: #1b1b1b; + border-color: #e8e2d2; + background-color: #e8e2d2; +} + +:is(a).color-sand-100 { + text-decoration-color: #e8e2d2 !important; +} + +.color-sand-50 { + color: #f2f0ec !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-sand-50, +[type=reset].button-sand-50 { + color: #1b1b1b; + border-color: #f2f0ec; + background-color: #f2f0ec; +} + +:is(a).color-sand-50 { + text-decoration-color: #f2f0ec !important; +} + +.color-sand-main { + color: #ccc6b4 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-sand-main, +[type=reset].button-sand-main { + color: #1b1b1b; + border-color: #ccc6b4; + background-color: #ccc6b4; +} + +:is(a).color-sand-main { + text-decoration-color: #ccc6b4 !important; +} + +.color-grey-950 { + color: #111111 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-grey-950, +[type=reset].button-grey-950 { + color: #dfe3eb; + border-color: #111111; + background-color: #111111; +} + +:is(a).color-grey-950 { + text-decoration-color: #111111 !important; +} + +.color-grey-900 { + color: #1b1b1b !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-grey-900, +[type=reset].button-grey-900 { + color: #dfe3eb; + border-color: #1b1b1b; + background-color: #1b1b1b; +} + +:is(a).color-grey-900 { + text-decoration-color: #1b1b1b !important; +} + +.color-grey-850 { + color: #262626 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-grey-850, +[type=reset].button-grey-850 { + color: #dfe3eb; + border-color: #262626; + background-color: #262626; +} + +:is(a).color-grey-850 { + text-decoration-color: #262626 !important; +} + +.color-grey-800 { + color: #303030 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-grey-800, +[type=reset].button-grey-800 { + color: #dfe3eb; + border-color: #303030; + background-color: #303030; +} + +:is(a).color-grey-800 { + text-decoration-color: #303030 !important; +} + +.color-grey-750 { + color: #3b3b3b !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-grey-750, +[type=reset].button-grey-750 { + color: #dfe3eb; + border-color: #3b3b3b; + background-color: #3b3b3b; +} + +:is(a).color-grey-750 { + text-decoration-color: #3b3b3b !important; +} + +.color-grey-700 { + color: #474747 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-grey-700, +[type=reset].button-grey-700 { + color: #dfe3eb; + border-color: #474747; + background-color: #474747; +} + +:is(a).color-grey-700 { + text-decoration-color: #474747 !important; +} + +.color-grey-650 { + color: #525252 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-grey-650, +[type=reset].button-grey-650 { + color: #dfe3eb; + border-color: #525252; + background-color: #525252; +} + +:is(a).color-grey-650 { + text-decoration-color: #525252 !important; +} + +.color-grey-600 { + color: #5e5e5e !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-grey-600, +[type=reset].button-grey-600 { + color: #dfe3eb; + border-color: #5e5e5e; + background-color: #5e5e5e; +} + +:is(a).color-grey-600 { + text-decoration-color: #5e5e5e !important; +} + +.color-grey-550 { + color: #6a6a6a !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-grey-550, +[type=reset].button-grey-550 { + color: #dfe3eb; + border-color: #6a6a6a; + background-color: #6a6a6a; +} + +:is(a).color-grey-550 { + text-decoration-color: #6a6a6a !important; +} + +.color-grey-500, .color-grey { + color: #777777 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-grey-500, .button-grey:is(button, [type=submit], [type=button], [role=button]), +[type=reset].button-grey-500, +[type=reset].button-grey { + color: #1b1b1b; + border-color: #777777; + background-color: #777777; +} + +:is(a).color-grey-500, .color-grey:is(a) { + text-decoration-color: #777777 !important; +} + +.color-grey-450 { + color: #808080 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-grey-450, +[type=reset].button-grey-450 { + color: #1b1b1b; + border-color: #808080; + background-color: #808080; +} + +:is(a).color-grey-450 { + text-decoration-color: #808080 !important; +} + +.color-grey-400 { + color: #919191 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-grey-400, +[type=reset].button-grey-400 { + color: #1b1b1b; + border-color: #919191; + background-color: #919191; +} + +:is(a).color-grey-400 { + text-decoration-color: #919191 !important; +} + +.color-grey-350 { + color: #9e9e9e !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-grey-350, +[type=reset].button-grey-350 { + color: #1b1b1b; + border-color: #9e9e9e; + background-color: #9e9e9e; +} + +:is(a).color-grey-350 { + text-decoration-color: #9e9e9e !important; +} + +.color-grey-300 { + color: #ababab !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-grey-300, +[type=reset].button-grey-300 { + color: #1b1b1b; + border-color: #ababab; + background-color: #ababab; +} + +:is(a).color-grey-300 { + text-decoration-color: #ababab !important; +} + +.color-grey-250 { + color: #b9b9b9 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-grey-250, +[type=reset].button-grey-250 { + color: #1b1b1b; + border-color: #b9b9b9; + background-color: #b9b9b9; +} + +:is(a).color-grey-250 { + text-decoration-color: #b9b9b9 !important; +} + +.color-grey-200 { + color: #c6c6c6 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-grey-200, +[type=reset].button-grey-200 { + color: #1b1b1b; + border-color: #c6c6c6; + background-color: #c6c6c6; +} + +:is(a).color-grey-200 { + text-decoration-color: #c6c6c6 !important; +} + +.color-grey-150 { + color: #d4d4d4 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-grey-150, +[type=reset].button-grey-150 { + color: #1b1b1b; + border-color: #d4d4d4; + background-color: #d4d4d4; +} + +:is(a).color-grey-150 { + text-decoration-color: #d4d4d4 !important; +} + +.color-grey-100 { + color: #e2e2e2 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-grey-100, +[type=reset].button-grey-100 { + color: #1b1b1b; + border-color: #e2e2e2; + background-color: #e2e2e2; +} + +:is(a).color-grey-100 { + text-decoration-color: #e2e2e2 !important; +} + +.color-grey-50 { + color: #f1f1f1 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-grey-50, +[type=reset].button-grey-50 { + color: #1b1b1b; + border-color: #f1f1f1; + background-color: #f1f1f1; +} + +:is(a).color-grey-50 { + text-decoration-color: #f1f1f1 !important; +} + +.color-grey-main { + color: #ababab !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-grey-main, +[type=reset].button-grey-main { + color: #1b1b1b; + border-color: #ababab; + background-color: #ababab; +} + +:is(a).color-grey-main { + text-decoration-color: #ababab !important; +} + +.color-zinc-950 { + color: #0f1114 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-zinc-950, +[type=reset].button-zinc-950 { + color: #dfe3eb; + border-color: #0f1114; + background-color: #0f1114; +} + +:is(a).color-zinc-950 { + text-decoration-color: #0f1114 !important; +} + +.color-zinc-900 { + color: #191c20 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-zinc-900, +[type=reset].button-zinc-900 { + color: #dfe3eb; + border-color: #191c20; + background-color: #191c20; +} + +:is(a).color-zinc-900 { + text-decoration-color: #191c20 !important; +} + +.color-zinc-850 { + color: #23262c !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-zinc-850, +[type=reset].button-zinc-850 { + color: #dfe3eb; + border-color: #23262c; + background-color: #23262c; +} + +:is(a).color-zinc-850 { + text-decoration-color: #23262c !important; +} + +.color-zinc-800 { + color: #2d3138 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-zinc-800, +[type=reset].button-zinc-800 { + color: #dfe3eb; + border-color: #2d3138; + background-color: #2d3138; +} + +:is(a).color-zinc-800 { + text-decoration-color: #2d3138 !important; +} + +.color-zinc-750 { + color: #373c44 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-zinc-750, +[type=reset].button-zinc-750 { + color: #dfe3eb; + border-color: #373c44; + background-color: #373c44; +} + +:is(a).color-zinc-750 { + text-decoration-color: #373c44 !important; +} + +.color-zinc-700 { + color: #424751 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-zinc-700, +[type=reset].button-zinc-700 { + color: #dfe3eb; + border-color: #424751; + background-color: #424751; +} + +:is(a).color-zinc-700 { + text-decoration-color: #424751 !important; +} + +.color-zinc-650 { + color: #4d535e !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-zinc-650, +[type=reset].button-zinc-650 { + color: #dfe3eb; + border-color: #4d535e; + background-color: #4d535e; +} + +:is(a).color-zinc-650 { + text-decoration-color: #4d535e !important; +} + +.color-zinc-600 { + color: #5c6370 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-zinc-600, +[type=reset].button-zinc-600 { + color: #dfe3eb; + border-color: #5c6370; + background-color: #5c6370; +} + +:is(a).color-zinc-600 { + text-decoration-color: #5c6370 !important; +} + +.color-zinc-550 { + color: #646b79 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-zinc-550, +[type=reset].button-zinc-550 { + color: #dfe3eb; + border-color: #646b79; + background-color: #646b79; +} + +:is(a).color-zinc-550 { + text-decoration-color: #646b79 !important; +} + +.color-zinc-500, .color-zinc { + color: #6f7887 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-zinc-500, .button-zinc:is(button, [type=submit], [type=button], [role=button]), +[type=reset].button-zinc-500, +[type=reset].button-zinc { + color: #1b1b1b; + border-color: #6f7887; + background-color: #6f7887; +} + +:is(a).color-zinc-500, .color-zinc:is(a) { + text-decoration-color: #6f7887 !important; +} + +.color-zinc-450 { + color: #7b8495 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-zinc-450, +[type=reset].button-zinc-450 { + color: #1b1b1b; + border-color: #7b8495; + background-color: #7b8495; +} + +:is(a).color-zinc-450 { + text-decoration-color: #7b8495 !important; +} + +.color-zinc-400 { + color: #8891a4 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-zinc-400, +[type=reset].button-zinc-400 { + color: #1b1b1b; + border-color: #8891a4; + background-color: #8891a4; +} + +:is(a).color-zinc-400 { + text-decoration-color: #8891a4 !important; +} + +.color-zinc-350 { + color: #969eaf !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-zinc-350, +[type=reset].button-zinc-350 { + color: #1b1b1b; + border-color: #969eaf; + background-color: #969eaf; +} + +:is(a).color-zinc-350 { + text-decoration-color: #969eaf !important; +} + +.color-zinc-300 { + color: #a4acba !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-zinc-300, +[type=reset].button-zinc-300 { + color: #1b1b1b; + border-color: #a4acba; + background-color: #a4acba; +} + +:is(a).color-zinc-300 { + text-decoration-color: #a4acba !important; +} + +.color-zinc-250 { + color: #b3b9c5 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-zinc-250, +[type=reset].button-zinc-250 { + color: #1b1b1b; + border-color: #b3b9c5; + background-color: #b3b9c5; +} + +:is(a).color-zinc-250 { + text-decoration-color: #b3b9c5 !important; +} + +.color-zinc-200 { + color: #c2c7d0 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-zinc-200, +[type=reset].button-zinc-200 { + color: #1b1b1b; + border-color: #c2c7d0; + background-color: #c2c7d0; +} + +:is(a).color-zinc-200 { + text-decoration-color: #c2c7d0 !important; +} + +.color-zinc-150 { + color: #d1d5db !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-zinc-150, +[type=reset].button-zinc-150 { + color: #1b1b1b; + border-color: #d1d5db; + background-color: #d1d5db; +} + +:is(a).color-zinc-150 { + text-decoration-color: #d1d5db !important; +} + +.color-zinc-100 { + color: #e0e3e7 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-zinc-100, +[type=reset].button-zinc-100 { + color: #1b1b1b; + border-color: #e0e3e7; + background-color: #e0e3e7; +} + +:is(a).color-zinc-100 { + text-decoration-color: #e0e3e7 !important; +} + +.color-zinc-50 { + color: #f0f1f3 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-zinc-50, +[type=reset].button-zinc-50 { + color: #1b1b1b; + border-color: #f0f1f3; + background-color: #f0f1f3; +} + +:is(a).color-zinc-50 { + text-decoration-color: #f0f1f3 !important; +} + +.color-zinc-main { + color: #646b79 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-zinc-main, +[type=reset].button-zinc-main { + color: #dfe3eb; + border-color: #646b79; + background-color: #646b79; +} + +:is(a).color-zinc-main { + text-decoration-color: #646b79 !important; +} + +.color-slate-950 { + color: #0e1118 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-slate-950, +[type=reset].button-slate-950 { + color: #dfe3eb; + border-color: #0e1118; + background-color: #0e1118; +} + +:is(a).color-slate-950 { + text-decoration-color: #0e1118 !important; +} + +.color-slate-900 { + color: #181c25 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-slate-900, +[type=reset].button-slate-900 { + color: #dfe3eb; + border-color: #181c25; + background-color: #181c25; +} + +:is(a).color-slate-900 { + text-decoration-color: #181c25 !important; +} + +.color-slate-850 { + color: #202632 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-slate-850, +[type=reset].button-slate-850 { + color: #dfe3eb; + border-color: #202632; + background-color: #202632; +} + +:is(a).color-slate-850 { + text-decoration-color: #202632 !important; +} + +.color-slate-800 { + color: #2a3140 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-slate-800, +[type=reset].button-slate-800 { + color: #dfe3eb; + border-color: #2a3140; + background-color: #2a3140; +} + +:is(a).color-slate-800 { + text-decoration-color: #2a3140 !important; +} + +.color-slate-750 { + color: #333c4e !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-slate-750, +[type=reset].button-slate-750 { + color: #dfe3eb; + border-color: #333c4e; + background-color: #333c4e; +} + +:is(a).color-slate-750 { + text-decoration-color: #333c4e !important; +} + +.color-slate-700 { + color: #3d475c !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-slate-700, +[type=reset].button-slate-700 { + color: #dfe3eb; + border-color: #3d475c; + background-color: #3d475c; +} + +:is(a).color-slate-700 { + text-decoration-color: #3d475c !important; +} + +.color-slate-650 { + color: #48536b !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-slate-650, +[type=reset].button-slate-650 { + color: #dfe3eb; + border-color: #48536b; + background-color: #48536b; +} + +:is(a).color-slate-650 { + text-decoration-color: #48536b !important; +} + +.color-slate-600 { + color: #525f7a !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-slate-600, +[type=reset].button-slate-600 { + color: #dfe3eb; + border-color: #525f7a; + background-color: #525f7a; +} + +:is(a).color-slate-600 { + text-decoration-color: #525f7a !important; +} + +.color-slate-550 { + color: #5d6b89 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-slate-550, +[type=reset].button-slate-550 { + color: #dfe3eb; + border-color: #5d6b89; + background-color: #5d6b89; +} + +:is(a).color-slate-550 { + text-decoration-color: #5d6b89 !important; +} + +.color-slate-500, .color-slate { + color: #687899 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-slate-500, .button-slate:is(button, [type=submit], [type=button], [role=button]), +[type=reset].button-slate-500, +[type=reset].button-slate { + color: #1b1b1b; + border-color: #687899; + background-color: #687899; +} + +:is(a).color-slate-500, .color-slate:is(a) { + text-decoration-color: #687899 !important; +} + +.color-slate-450 { + color: #7385a9 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-slate-450, +[type=reset].button-slate-450 { + color: #1b1b1b; + border-color: #7385a9; + background-color: #7385a9; +} + +:is(a).color-slate-450 { + text-decoration-color: #7385a9 !important; +} + +.color-slate-400 { + color: #8191b5 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-slate-400, +[type=reset].button-slate-400 { + color: #1b1b1b; + border-color: #8191b5; + background-color: #8191b5; +} + +:is(a).color-slate-400 { + text-decoration-color: #8191b5 !important; +} + +.color-slate-350 { + color: #909ebe !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-slate-350, +[type=reset].button-slate-350 { + color: #1b1b1b; + border-color: #909ebe; + background-color: #909ebe; +} + +:is(a).color-slate-350 { + text-decoration-color: #909ebe !important; +} + +.color-slate-300 { + color: #a0acc7 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-slate-300, +[type=reset].button-slate-300 { + color: #1b1b1b; + border-color: #a0acc7; + background-color: #a0acc7; +} + +:is(a).color-slate-300 { + text-decoration-color: #a0acc7 !important; +} + +.color-slate-250 { + color: #b0b9d0 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-slate-250, +[type=reset].button-slate-250 { + color: #1b1b1b; + border-color: #b0b9d0; + background-color: #b0b9d0; +} + +:is(a).color-slate-250 { + text-decoration-color: #b0b9d0 !important; +} + +.color-slate-200 { + color: #bfc7d9 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-slate-200, +[type=reset].button-slate-200 { + color: #1b1b1b; + border-color: #bfc7d9; + background-color: #bfc7d9; +} + +:is(a).color-slate-200 { + text-decoration-color: #bfc7d9 !important; +} + +.color-slate-150 { + color: #cfd5e2 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-slate-150, +[type=reset].button-slate-150 { + color: #1b1b1b; + border-color: #cfd5e2; + background-color: #cfd5e2; +} + +:is(a).color-slate-150 { + text-decoration-color: #cfd5e2 !important; +} + +.color-slate-100 { + color: #dfe3eb !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-slate-100, +[type=reset].button-slate-100 { + color: #1b1b1b; + border-color: #dfe3eb; + background-color: #dfe3eb; +} + +:is(a).color-slate-100 { + text-decoration-color: #dfe3eb !important; +} + +.color-slate-50 { + color: #eff1f4 !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-slate-50, +[type=reset].button-slate-50 { + color: #1b1b1b; + border-color: #eff1f4; + background-color: #eff1f4; +} + +:is(a).color-slate-50 { + text-decoration-color: #eff1f4 !important; +} + +.color-slate-main { + color: #525f7a !important; +} + +:is(button, [type=submit], [type=button], [role=button]).button-slate-main, +[type=reset].button-slate-main { + color: #dfe3eb; + border-color: #525f7a; + background-color: #525f7a; +} + +:is(a).color-slate-main { + text-decoration-color: #525f7a !important; +} + +@media (max-width: 576px) { + .hide-below-sm { + display: none; + } + .show-below-sm { + display: block; + } +} +@media (min-width: 577px) { + .show-below-sm { + display: none; + } +} +@media (max-width: 768px) { + .hide-below-md { + display: none; + } + .show-below-md { + display: block; + } +} +@media (min-width: 769px) { + .show-below-md { + display: none; + } +} +@media (max-width: 1024px) { + .hide-below-lg { + display: none; + } + .show-below-lg { + display: block; + } +} +@media (min-width: 1025px) { + .show-below-lg { + display: none; + } +} +@media (max-width: 1280px) { + .hide-below-xl { + display: none; + } + .show-below-xl { + display: block; + } +} +@media (min-width: 1281px) { + .show-below-xl { + display: none; + } +} +@media (max-width: 1536px) { + .hide-below-xxl { + display: none; + } + .show-below-xxl { + display: block; + } +} +@media (min-width: 1537px) { + .show-below-xxl { + display: none; + } +} + +/*# sourceMappingURL=pico-custom.css.map */ diff --git a/components/web/static_files/css/pico-custom.scss b/components/web/static_files/css/pico-custom.scss new file mode 100644 index 0000000..2cee022 --- /dev/null +++ b/components/web/static_files/css/pico-custom.scss @@ -0,0 +1,485 @@ +@use "pico" with ( + $enable-semantic-container: true, + $enable-responsive-spacings: true, + $enable-responsive-typography: false, + $theme-color: "slate", + $breakpoints: ( + sm: (breakpoint: 576px, viewport: 95%), + md: (breakpoint: 768px, viewport: 95%), + lg: (breakpoint: 1024px, viewport: 90%), + xl: (breakpoint: 1280px, viewport: 90%), + xxl: (breakpoint: 1536px, viewport: 85%) + ), +); +@use "colors" as *; +@use "sass:map"; +@use "settings" as *; +@use "sass:math"; + +@font-face { + font-family: go; + src: local("Go Regular"), url(../fonts/Go-Regular.woff2) format("woff2"); + font-style: normal; + font-weight: 400; +} +@font-face { + font-family: go; + src: local("Go Italic"), url(../fonts/Go-Italic.woff2) format("woff2"); + font-style: italic; + font-weight: 400; +} +@font-face { + font-family: go medium; + src: local("Go Medium"), url(../fonts/Go-Medium.woff2) format("woff2"); + font-style: normal; + font-weight: 500; +} +@font-face { + font-family: go medium; + src: local("Go Medium Italic"), url(../fonts/Go-Medium-Italic.woff2) format("woff2"); + font-style: italic; + font-weight: 500; +} +@font-face { + font-family: go; + src: local("Go Bold"), url(../fonts/Go-Bold.woff2) format("woff2"); + font-style: normal; + font-weight: 600; +} +@font-face { + font-family: go; + src: local("Go Bold Italic"), url(../fonts/Go-Bold-Italic.woff2) format("woff2"); + font-style: italic; + font-weight: 600; +} +@font-face { + font-family: go smallcaps; + src: local("Go Smallcaps"), url(../fonts/Go-Smallcaps.woff2) format("woff2"); + font-style: normal; + font-weight: 400; +} +@font-face { + font-family: go smallcaps; + src: local("Go Smallcaps Italic"), url(../fonts/Go-Smallcaps-Italic.woff2) format("woff2"); + font-style: italic; + font-weight: 400; +} +@font-face { + font-family: go mono; + src: local("Go Mono"), url(../fonts/Go-Mono.woff2) format("woff2"); + font-style: normal; + font-weight: 400; +} +@font-face { + font-family: go mono; + src: local("Go Mono Italic"), url(../fonts/Go-Mono-Italic.woff2) format("woff2"); + font-style: italic; + font-weight: 400; +} +@font-face { + font-family: go mono; + src: local("Go Mono Bold"), url(../fonts/Go-Mono-Bold.woff2) format("woff2"); + font-style: normal; + font-weight: 600; +} +@font-face { + font-family: go mono; + src: local("Go Mono Bold Italic"), url(../fonts/Go-Mono-Bold-Italic.woff2) format("woff2"); + font-style: italic; + font-weight: 600; +} + +:root { + --pico-font-family-sans-serif: "Go", system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, var(--pico-font-family-emoji); + --pico-form-element-spacing-vertical: .5rem; + --pico-form-element-spacing-horizontal: .5rem; + --pico-border-radius: 0; + --pico-font-size: 12pt; + --pico-line-height: 1.4; +} + +small { + font-size: .875rem; + color: var(--pico-muted-color); +} + +mark { + margin-block: calc(var(--pico-spacing)/4); + display: inline-block; + border-radius: .25rem; +} + +pre { + padding: calc(var(--pico-spacing)/2); +} + +#nav-theme-toggle { + cursor:pointer !important; +} +.dark { + filter: grayscale(100%); +} +.hi, .hi a { + font-size:1.1rem; + --pico-text-decoration: none; +} + +table td article { + margin-bottom: var(--pico-spacing); +} +table tr { + white-space: nowrap; +} +table td.created-modified { + background-image: var(--pico-icon-date); + background-position: center right var(--pico-form-element-spacing-vertical); + background-size: 1rem; + padding-inline-end: 2rem; +} +table td.created-modified, table th.created-modified { + text-align: right; +} + +.no-text-decoration { + text-decoration: none !important; + border-bottom: none !important; +} + +[data-loading] { + display: none; +} + +.help { + cursor:help; +} + +.pointer { + cursor:pointer; +} + +[contenteditable] { + display: inline-block; + border-bottom: 1px dotted #{$slate-300}; + text-decoration: none; +} +[contenteditable]:focus { + padding: calc(var(--pico-spacing)/2); + background: var(--pico-contrast); + color: var(--pico-contrast-inverse); +} + +[role="group"] { + --pico-group-box-shadow: none !important; +} + +/* +* Table navigation +*/ +.table-navigation { + user-select: none; +} +.table-navigation .paging:not(.disabled) { + padding: var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal); + color: var(--pico-color); +} +.table-navigation button.sorting { + font-size: .875rem; + border: 0; +} +.table-navigation div.filters { + white-space: wrap; +} +.table-navigation div.filter-buttons button { + font-size: 0.875rem; + padding: calc(var(--pico-form-element-spacing-vertical) / 2) calc(var(--pico-form-element-spacing-horizontal) / 2); + border-color: var(--pico-form-element-border-color); + --pico-border-radius: .5rem; +} +.table-navigation .paging.disabled { + opacity: .5; + cursor: not-allowed; +} + +/* +# Notifications +*/ +#notification-container { + position: fixed; + left: .25rem; + bottom: .25rem; + z-index: 999; + width: fit-content; +} +.notification:not(:last-child) { + margin-bottom: .25rem; +} +.notification-text { + white-space: pre-line; +} +.notification { + cursor: pointer; + padding: var(--pico-form-element-spacing-vertical) 2rem; + color: var(--pico-color); + background-color: var(--pico-background-color); + background-image: var(--pico-icon); + background-position: center left var(--pico-form-element-spacing-vertical); + background-size: calc(var(--pico-spacing) * 1.5); + background-blend-mode: color-burn; + padding-left: calc(var(--pico-form-element-spacing-vertical) * 2 + calc(var(--pico-spacing) * 1.5)); +} +.notification-error { + --pico-background-color: #{$red-600}; + --pico-icon: var(--pico-icon-invalid); + --pico-color: #{$red-50}; +} +.notification-warning { + --pico-background-color: #{$yellow-50}; + --pico-icon: var(--pico-icon-invalid); + --pico-color: #{$yellow-900}; +} +.notification-success { + --pico-background-color: #{$green-550}; + --pico-icon: var(--pico-icon-valid); + --pico-color: #{$slate-50}; +} +.notification-user { + --pico-background-color: #{$azure-450}; + --pico-icon: var(--pico-icon-chevron); + --pico-color: #{$slate-50}; +} +.notification-title { + font-weight: bold; +} +.notification-system > .notification-title:before { + content:"\1F4E2\0020 Broadcast:\0020"; +} +.notification-system { + --pico-background-color: #{$fuchsia-800}; + --pico-icon: var(--pico-icon-chevron); + --pico-color: #{$fuchsia-100}; +} + +.login-grid { + display: grid; + grid-template-columns: 20% 60% 20%; + grid-template-rows: 1fr; +} + +.login-register { grid-column-start: 2; } + + +thead th, thead td, tfoot th, tfoot td { + --pico-font-weight: 400; +} + +button, a { touch-action: manipulation; } + +button[type="submit"] { + width: auto; +} + +dialog article { + max-width: 65%; +} + +.no-text-wrap { + text-wrap: nowrap; + white-space: nowrap; +} +.text-wrap { + text-wrap: balance; + white-space: break-spaces; +} + +.split-grid.grid > article:first-child { + grid-column: span 1; +} +.split-grid.grid > article { + grid-column: span 3; +} + +.grid-end { + display: grid; + justify-content: end; + grid-auto-columns: max-content; + grid-auto-flow: column; + gap: calc(var(--pico-spacing)/2); + align-items: baseline; + white-space: nowrap; +} +.grid-space-between { + display: grid; + justify-content: space-between; + grid-auto-columns: max-content; + grid-auto-flow: column; + gap: calc(var(--pico-spacing) /2); + white-space: nowrap; + align-items: baseline; + margin: calc(var(--pico-spacing) /2) auto; +} +.grid-3-cols { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: calc(var(--pico-spacing) /2); +} +.grid-3-cols .span-3 { + grid-column: span 3; +} +.grid-3-cols > article { + margin-bottom: 0; + padding-bottom: var(--pico-form-element-spacing-vertical); + --pico-border-radius: .5rem; +} + +.table-select { + background-color: rgb(128 128 128 / 10%); + backdrop-filter: blur(1px); + position: sticky; + bottom: var(--pico-form-element-spacing-vertical); + padding: var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal); +} + +nav details.dropdown { + width: max-content; +} + +fieldset.vault-unlock { + padding: var(--pico-spacing) 0; +} + +@media only screen and (max-width: 600px) { + #notification-container { + left: 0; + bottom: 0; + width: 100%; + } + dialog article { + max-width: 95%; + } + .grid-3-cols { + display: grid; + grid-template-columns: repeat(1, 1fr); + gap: calc(var(--pico-spacing)/2); + } + .grid-3-cols .span-3 { + grid-column: span 1; + } + .grid-3-cols > article { + margin-bottom: 0; + --pico-border-radius: .5rem; + } +} +@media (max-width: 1024px) { + .split-grid.grid { + gap: calc(var(--pico-spacing) /2); + } + .split-grid.grid > article { + padding: 0; + background-color: transparent; + box-shadow: none; + } + + th, td { + --pico-spacing: 0.75rem; + } +} + +.group { + opacity: 0; +} + +textarea.dns-data { + border: 0; + background: transparent; + font-family: monospace; +} + +fieldset.system-field label:before { + content:"\1f512\0020"; +} + +fieldset.keypair { + border: 1px solid var(--pico-form-element-border-color); + padding: var(--pico-spacing); +} + + +/////////////////////////////////////// +// Generators for colors and breakpoints +/////////////////////////////////////// + +@function get-luminance($color) { + $red: math.div(red($color), 255); + $green: math.div(green($color), 255); + $blue: math.div(blue($color), 255); + + @return ($red * 0.2126) + ($green * 0.7152) + ($blue * 0.0722); +} + +@function get-contrast-ratio($color1, $color2) { + $l1: get-luminance($color1); + $l2: get-luminance($color2); + + @if $l1 > $l2 { + @return math.div($l1, $l2); + } @else { + @return math.div($l2, $l1); + } +} + +@function get-contrast-color($color) { + $dark-color: $grey-900; + $light-color: $slate-100; + $contrast-with-dark: get-contrast-ratio($color, $dark-color); + $contrast-with-light: get-contrast-ratio($color, $light-color); + + @if $contrast-with-light >= 2.0 { + @return $light-color; + } @else { + @return $dark-color; + } +} + +@each $color-key, $color-var in $colors { + @each $shade, $value in $color-var { + .color-#{"#{$color-key}"}-#{$shade} { + color: $value !important; + } + :is(button, [type="submit"], [type="button"], [role="button"]).button-#{"#{$color-key}"}-#{$shade}, + [type="reset"].button-#{"#{$color-key}"}-#{$shade} { + color: get-contrast-color($value); + border-color: $value; + background-color: $value; + } + :is(a).color-#{"#{$color-key}"}-#{$shade} { + text-decoration-color: $value !important; + } + } + + @if map-has-key($color-var, 500) { + .color-#{"#{$color-key}"} { + @extend .color-#{"#{$color-key}"}-500; + } + :is(button, [type="submit"], [type="button"], [role="button"]).button-#{"#{$color-key}"}, + [type="reset"].button-#{"#{$color-key}"} { + @extend .button-#{"#{$color-key}"}-500; + } + } +} + +@each $size, $data in $breakpoints { + $breakpoint: map-get($data, breakpoint); + @media (max-width: $breakpoint) { + .hide-below-#{$size} { + display: none; + } + .show-below-#{$size} { + display: block; + } + } + @media (min-width: $breakpoint + 1px) { + .show-below-#{$size} { + display: none; + } + } +} diff --git a/components/web/static_files/empty.html b/components/web/static_files/empty.html new file mode 100644 index 0000000..e69de29 diff --git a/components/web/static_files/fonts/Go-Bold-Italic.woff2 b/components/web/static_files/fonts/Go-Bold-Italic.woff2 new file mode 100644 index 0000000..1c6df10 Binary files /dev/null and b/components/web/static_files/fonts/Go-Bold-Italic.woff2 differ diff --git a/components/web/static_files/fonts/Go-Bold.woff2 b/components/web/static_files/fonts/Go-Bold.woff2 new file mode 100644 index 0000000..c3e84cc Binary files /dev/null and b/components/web/static_files/fonts/Go-Bold.woff2 differ diff --git a/components/web/static_files/fonts/Go-Italic.woff2 b/components/web/static_files/fonts/Go-Italic.woff2 new file mode 100644 index 0000000..bb2fc43 Binary files /dev/null and b/components/web/static_files/fonts/Go-Italic.woff2 differ diff --git a/components/web/static_files/fonts/Go-Medium-Italic.woff2 b/components/web/static_files/fonts/Go-Medium-Italic.woff2 new file mode 100644 index 0000000..6dc7ec1 Binary files /dev/null and b/components/web/static_files/fonts/Go-Medium-Italic.woff2 differ diff --git a/components/web/static_files/fonts/Go-Medium.woff2 b/components/web/static_files/fonts/Go-Medium.woff2 new file mode 100644 index 0000000..1596653 Binary files /dev/null and b/components/web/static_files/fonts/Go-Medium.woff2 differ diff --git a/components/web/static_files/fonts/Go-Mono-Bold-Italic.woff2 b/components/web/static_files/fonts/Go-Mono-Bold-Italic.woff2 new file mode 100644 index 0000000..1de9603 Binary files /dev/null and b/components/web/static_files/fonts/Go-Mono-Bold-Italic.woff2 differ diff --git a/components/web/static_files/fonts/Go-Mono-Bold.woff2 b/components/web/static_files/fonts/Go-Mono-Bold.woff2 new file mode 100644 index 0000000..856a7cd Binary files /dev/null and b/components/web/static_files/fonts/Go-Mono-Bold.woff2 differ diff --git a/components/web/static_files/fonts/Go-Mono-Italic.woff2 b/components/web/static_files/fonts/Go-Mono-Italic.woff2 new file mode 100644 index 0000000..0799755 Binary files /dev/null and b/components/web/static_files/fonts/Go-Mono-Italic.woff2 differ diff --git a/components/web/static_files/fonts/Go-Mono.woff2 b/components/web/static_files/fonts/Go-Mono.woff2 new file mode 100644 index 0000000..e1df9eb Binary files /dev/null and b/components/web/static_files/fonts/Go-Mono.woff2 differ diff --git a/components/web/static_files/fonts/Go-Regular.woff2 b/components/web/static_files/fonts/Go-Regular.woff2 new file mode 100644 index 0000000..1894c47 Binary files /dev/null and b/components/web/static_files/fonts/Go-Regular.woff2 differ diff --git a/components/web/static_files/fonts/Go-Smallcaps-Italic.woff2 b/components/web/static_files/fonts/Go-Smallcaps-Italic.woff2 new file mode 100644 index 0000000..530e14f Binary files /dev/null and b/components/web/static_files/fonts/Go-Smallcaps-Italic.woff2 differ diff --git a/components/web/static_files/fonts/Go-Smallcaps.woff2 b/components/web/static_files/fonts/Go-Smallcaps.woff2 new file mode 100644 index 0000000..4be5ed5 Binary files /dev/null and b/components/web/static_files/fonts/Go-Smallcaps.woff2 differ diff --git a/components/web/static_files/fonts/LICENSE.txt b/components/web/static_files/fonts/LICENSE.txt new file mode 100644 index 0000000..7043c36 --- /dev/null +++ b/components/web/static_files/fonts/LICENSE.txt @@ -0,0 +1,36 @@ +These fonts were created by the Bigelow & Holmes foundry specifically for the +Go project. See https://blog.golang.org/go-fonts for details. + +They are licensed under the same open source license as the rest of the Go +project's software: + +Copyright (c) 2016 Bigelow & Holmes Inc.. All rights reserved. + +Distribution of this font is governed by the following license. If you do not +agree to this license, including the disclaimer, do not distribute or modify +this font. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of Google Inc. nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +DISCLAIMER: THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/components/web/static_files/hyperscript/common._hs b/components/web/static_files/hyperscript/common._hs new file mode 100644 index 0000000..b17e258 --- /dev/null +++ b/components/web/static_files/hyperscript/common._hs @@ -0,0 +1,272 @@ +def postJson(url, dataObject) + fetch `${url}` with method:'POST', body:dataObject as JSON, headers:{content-type:'application/json'} + return result +end + +def getJsonUrlAsObject(url) + fetch `${url}` with method:'GET', headers:{content-type:'application/json'} + return result as Object +end + +def countdownSeconds(el, sec) + set i to sec + set _s to el's textContent + repeat until i is 0 + put `${_s} (${i}s)` into el + decrement i by 1 + wait 1s + end +end + +def setCheckboxes(el, option) + repeat for e in ( in el) + if option == 'all' + set e's @checked to 'true' + set e.checked to true + else if option == 'none' + remove @checked from e + set e.checked to false + else if option == 'invert' + if e.checked + toggle [@checked='false'] on e + set e.checked to false + else + toggle [@checked='true'] on e + set e.checked to true + end + else if option == 'toggle' + toggle [@checked='true'] on e + if e's @checked set e.checked to true else set e.checked to false end + end + end +end + +behavior trCheckboxSelect + on click + document.getSelection().removeAllRanges() + if not event.shiftKey + take .select-tr-element from in closest for me + call setCheckboxes(me, 'toggle') unless event.target.tagName.toLowerCase() === 'a' + else + document.getSelection().removeAllRanges() + get first .select-tr-element in closest
+ if it + set toggleTo to 'none' + if checked of first .multiselect in it + set toggleTo to 'all' + end + set selectedTrElement to it + if it.rowIndex < my.rowIndex + repeat while selectedTrElement.nextElementSibling + set selectedTrElement to selectedTrElement.nextElementSibling + call setCheckboxes(selectedTrElement, toggleTo) + if selectedTrElement is me + break + end + end + else + repeat while selectedTrElement.previousElementSibling + call setCheckboxes(selectedTrElement, toggleTo) + if selectedTrElement is me + break + end + set selectedTrElement to selectedTrElement.previousElementSibling + end + end + end + end + end +end + +behavior buttonCheckHtmxResponse + on htmx:afterRequest from closest to me + if (closest to me) != (event.target) exit end + set :_v to my textContent unless :_v + if event.detail.successful then + put `👍` into me + else + put `🤖 An error occured` into me + end + wait 1s + put :_v into me + set :_v to null + end +end + +behavior confirmButton + init set :inner to my.innerHTML end + on every click + halt the event + end + on click[event.detail==1] from me queue none + set x to 3 + repeat until x == 0 + put `Confirm ${x}x` into me + wait for a click or 1500ms + if the result's type is 'click' + decrement x + else + put :inner into me + exit + end + end + put :inner into me + trigger confirmedButton + end +end + +behavior inlineHtmxRename + init + set :_textContent to my.textContent + end + + on click halt the event end + + on htmx:afterRequest + if event.detail.successful == true + set :_textContent to my.textContent + end + set my.textContent to :_textContent + end + + on htmx:confirm(issueRequest) + halt the event + call confirm(`${:_textContent} to ${my.textContent}?`) + if not result set my.textContent to :_textContent else issueRequest() end + end + + on blur + if my.textContent == '' set my.textContent to :_textContent then exit end + if my.textContent == :_textContent exit end + set @hx-vals to `{"${my @data-patch-parameter}": "${my.textContent}"}` + trigger editContent on me + end + + on keydown[keyCode == 13] + me.blur() + halt the event + end +end + +behavior tresorToggle + def setUnlocked + get #vault-unlock-pin + add @disabled to it + set its @placeholder to 'Tresor is unlocked' + set #vault-unlock's textContent to '🔓' + end + def setLocked + get #vault-unlock-pin + remove @disabled from it + set its @placeholder to 'Tresor password' + set #vault-unlock's textContent to '🔐' + end + def noTresor + get #vault-unlock-pin + add @disabled to it + set its @placeholder to 'No tresor available' + set #vault-unlock's textContent to '⛔' + end + init + if window.vault.isUnlocked() + call setUnlocked() + else + if #vault-unlock's @data-tresor != "" + call setLocked() + else + call noTresor() + end + end + end + on keydown[keyCode == 13] from #vault-unlock-pin + trigger click on #vault-unlock unless #vault-unlock-pin's value is empty + end + on click from #vault-unlock + halt the event + if not window.vault.isUnlocked() + exit unless value of #vault-unlock-pin + call JSON.parse(#vault-unlock's @data-tresor) set keyData to the result + call VaultUnlockPrivateKey(value of #vault-unlock-pin, keyData) + call setUnlocked() + else + call window.vault.lock() + call setLocked() + end + set value of #vault-unlock-pin to '' + on exception(error) + trigger notification( + title: 'Tresor error', + level: 'validationError', + message: 'Could not unlock tresor, check your PIN', + duration: 3000, + locations: ['vault-unlock-pin'] + ) + end +end + +behavior bodydefault + on htmx:wsError or htmx:wsClose + set #ws-indicator's textContent to '⭕' + end + + on keydown + exit unless window.vault.isUnlocked() + if navigator.platform.toUpperCase().indexOf('MAC') >= 0 + set ctrlOrCmd to event.metaKey + else + set ctrlOrCmd to event.ctrlKey + end + if (event.key is "F5" or (ctrlOrCmd and event.key.toLowerCase() === "r")) or ((ctrlOrCmd and event.shiftKey and event.key.toLowerCase() === "r") or (event.shiftKey and e.key === "F5")) + trigger notification( + title: 'Unlocked session', + level: 'user', + message: 'Preventing window reload due to unlocked session', + duration: 2000, + locations: [] + ) + halt the event + end + end + + on htmx:responseError + set status to event.detail.xhr.status + if status >= 500 + trigger notification(title: 'Server error', level: 'error', message: 'The server could not handle the given request', duration: 10000) + else if status == 404 + trigger notification(title: 'Not found', level: 'error', message: `Route not found: ${event.detail.xhr.responseURL}`, duration: 3000) + end + end + + on htmx:configRequest + if window.vault.isUnlocked() + repeat for p in event.detail.parameters + log p + end + end + end + + on htmx:beforeRequest + remove @aria-invalid from <[aria-invalid]/> + end +end + +behavior objectFilters(submitForm) + on click from in me + halt the event + put '' into .generated-filters in me + + repeat for btn in ( in me) + if (btn is event.target and btn does not match .active) or (btn is not event.target and btn matches .active) + render #filter-item with (value: btn's value) + then put the result at the end of .generated-filters in me + end + end + + if length of <.generated-filters input/> is 0 + render #filter-item with (value: '') + then put the result at the end of .generated-filters in me + end + + trigger submit on submitForm unless submitForm == '' + end +end diff --git a/components/web/static_files/js/_hyperscript/_hyperscript.js b/components/web/static_files/js/_hyperscript/_hyperscript.js new file mode 100644 index 0000000..7a61cbc --- /dev/null +++ b/components/web/static_files/js/_hyperscript/_hyperscript.js @@ -0,0 +1,7723 @@ +/** + * @typedef {Object} Hyperscript + */ + +(function (self, factory) { + const _hyperscript = factory(self) + + if (typeof exports === 'object' && typeof exports['nodeName'] !== 'string') { + module.exports = _hyperscript + } else { + self['_hyperscript'] = _hyperscript + if ('document' in self) self['_hyperscript'].browserInit() + } +})(typeof self !== 'undefined' ? self : this, (globalScope) => { + + 'use strict'; + + /** + * @type {Object} + * @property {DynamicConverter[]} dynamicResolvers + * + * @callback DynamicConverter + * @param {String} str + * @param {*} value + * @returns {*} + */ + const conversions = { + dynamicResolvers: [ + function(str, value){ + if (str === "Fixed") { + return Number(value).toFixed(); + } else if (str.indexOf("Fixed:") === 0) { + let num = str.split(":")[1]; + return Number(value).toFixed(parseInt(num)); + } + } + ], + String: function (val) { + if (val.toString) { + return val.toString(); + } else { + return "" + val; + } + }, + Int: function (val) { + return parseInt(val); + }, + Float: function (val) { + return parseFloat(val); + }, + Number: function (val) { + return Number(val); + }, + Date: function (val) { + return new Date(val); + }, + Array: function (val) { + return Array.from(val); + }, + JSON: function (val) { + return JSON.stringify(val); + }, + Object: function (val) { + if (val instanceof String) { + val = val.toString(); + } + if (typeof val === "string") { + return JSON.parse(val); + } else { + return Object.assign({}, val); + } + }, + } + + const config = { + attributes: "_, script, data-script", + defaultTransition: "all 500ms ease-in", + disableSelector: "[disable-scripting], [data-disable-scripting]", + hideShowStrategies: {}, + conversions, + } + + class Lexer { + static OP_TABLE = { + "+": "PLUS", + "-": "MINUS", + "*": "MULTIPLY", + "/": "DIVIDE", + ".": "PERIOD", + "..": "ELLIPSIS", + "\\": "BACKSLASH", + ":": "COLON", + "%": "PERCENT", + "|": "PIPE", + "!": "EXCLAMATION", + "?": "QUESTION", + "#": "POUND", + "&": "AMPERSAND", + $: "DOLLAR", + ";": "SEMI", + ",": "COMMA", + "(": "L_PAREN", + ")": "R_PAREN", + "<": "L_ANG", + ">": "R_ANG", + "<=": "LTE_ANG", + ">=": "GTE_ANG", + "==": "EQ", + "===": "EQQ", + "!=": "NEQ", + "!==": "NEQQ", + "{": "L_BRACE", + "}": "R_BRACE", + "[": "L_BRACKET", + "]": "R_BRACKET", + "=": "EQUALS", + }; + + /** + * isValidCSSClassChar returns `true` if the provided character is valid in a CSS class. + * @param {string} c + * @returns boolean + */ + static isValidCSSClassChar(c) { + return Lexer.isAlpha(c) || Lexer.isNumeric(c) || c === "-" || c === "_" || c === ":"; + } + + /** + * isValidCSSIDChar returns `true` if the provided character is valid in a CSS ID + * @param {string} c + * @returns boolean + */ + static isValidCSSIDChar(c) { + return Lexer.isAlpha(c) || Lexer.isNumeric(c) || c === "-" || c === "_" || c === ":"; + } + + /** + * isWhitespace returns `true` if the provided character is whitespace. + * @param {string} c + * @returns boolean + */ + static isWhitespace(c) { + return c === " " || c === "\t" || Lexer.isNewline(c); + } + + /** + * positionString returns a string representation of a Token's line and column details. + * @param {Token} token + * @returns string + */ + static positionString(token) { + return "[Line: " + token.line + ", Column: " + token.column + "]"; + } + + /** + * isNewline returns `true` if the provided character is a carriage return or newline + * @param {string} c + * @returns boolean + */ + static isNewline(c) { + return c === "\r" || c === "\n"; + } + + /** + * isNumeric returns `true` if the provided character is a number (0-9) + * @param {string} c + * @returns boolean + */ + static isNumeric(c) { + return c >= "0" && c <= "9"; + } + + /** + * isAlpha returns `true` if the provided character is a letter in the alphabet + * @param {string} c + * @returns boolean + */ + static isAlpha(c) { + return (c >= "a" && c <= "z") || (c >= "A" && c <= "Z"); + } + + /** + * @param {string} c + * @param {boolean} [dollarIsOp] + * @returns boolean + */ + static isIdentifierChar(c, dollarIsOp) { + return c === "_" || c === "$"; + } + + /** + * @param {string} c + * @returns boolean + */ + static isReservedChar(c) { + return c === "`" || c === "^"; + } + + /** + * @param {Token[]} tokens + * @returns {boolean} + */ + static isValidSingleQuoteStringStart(tokens) { + if (tokens.length > 0) { + var previousToken = tokens[tokens.length - 1]; + if ( + previousToken.type === "IDENTIFIER" || + previousToken.type === "CLASS_REF" || + previousToken.type === "ID_REF" + ) { + return false; + } + if (previousToken.op && (previousToken.value === ">" || previousToken.value === ")")) { + return false; + } + } + return true; + } + + /** + * @param {string} string + * @param {boolean} [template] + * @returns {Tokens} + */ + static tokenize(string, template) { + var tokens = /** @type {Token[]}*/ []; + var source = string; + var position = 0; + var column = 0; + var line = 1; + var lastToken = ""; + var templateBraceCount = 0; + + function inTemplate() { + return template && templateBraceCount === 0; + } + + while (position < source.length) { + if ((currentChar() === "-" && nextChar() === "-" && (Lexer.isWhitespace(nextCharAt(2)) || nextCharAt(2) === "" || nextCharAt(2) === "-")) + || (currentChar() === "/" && nextChar() === "/" && (Lexer.isWhitespace(nextCharAt(2)) || nextCharAt(2) === "" || nextCharAt(2) === "/"))) { + consumeComment(); + } else if (currentChar() === "/" && nextChar() === "*" && (Lexer.isWhitespace(nextCharAt(2)) || nextCharAt(2) === "" || nextCharAt(2) === "*")) { + consumeCommentMultiline(); + } else { + if (Lexer.isWhitespace(currentChar())) { + tokens.push(consumeWhitespace()); + } else if ( + !possiblePrecedingSymbol() && + currentChar() === "." && + (Lexer.isAlpha(nextChar()) || nextChar() === "{" || nextChar() === "-") + ) { + tokens.push(consumeClassReference()); + } else if ( + !possiblePrecedingSymbol() && + currentChar() === "#" && + (Lexer.isAlpha(nextChar()) || nextChar() === "{") + ) { + tokens.push(consumeIdReference()); + } else if (currentChar() === "[" && nextChar() === "@") { + tokens.push(consumeAttributeReference()); + } else if (currentChar() === "@") { + tokens.push(consumeShortAttributeReference()); + } else if (currentChar() === "*" && Lexer.isAlpha(nextChar())) { + tokens.push(consumeStyleReference()); + } else if (Lexer.isAlpha(currentChar()) || (!inTemplate() && Lexer.isIdentifierChar(currentChar()))) { + tokens.push(consumeIdentifier()); + } else if (Lexer.isNumeric(currentChar())) { + tokens.push(consumeNumber()); + } else if (!inTemplate() && (currentChar() === '"' || currentChar() === "`")) { + tokens.push(consumeString()); + } else if (!inTemplate() && currentChar() === "'") { + if (Lexer.isValidSingleQuoteStringStart(tokens)) { + tokens.push(consumeString()); + } else { + tokens.push(consumeOp()); + } + } else if (Lexer.OP_TABLE[currentChar()]) { + if (lastToken === "$" && currentChar() === "{") { + templateBraceCount++; + } + if (currentChar() === "}") { + templateBraceCount--; + } + tokens.push(consumeOp()); + } else if (inTemplate() || Lexer.isReservedChar(currentChar())) { + tokens.push(makeToken("RESERVED", consumeChar())); + } else { + if (position < source.length) { + throw Error("Unknown token: " + currentChar() + " "); + } + } + } + } + + return new Tokens(tokens, [], source); + + /** + * @param {string} [type] + * @param {string} [value] + * @returns {Token} + */ + function makeOpToken(type, value) { + var token = makeToken(type, value); + token.op = true; + return token; + } + + /** + * @param {string} [type] + * @param {string} [value] + * @returns {Token} + */ + function makeToken(type, value) { + return { + type: type, + value: value || "", + start: position, + end: position + 1, + column: column, + line: line, + }; + } + + function consumeComment() { + while (currentChar() && !Lexer.isNewline(currentChar())) { + consumeChar(); + } + consumeChar(); // Consume newline + } + + function consumeCommentMultiline() { + while (currentChar() && !(currentChar() === '*' && nextChar() === '/')) { + consumeChar(); + } + consumeChar(); // Consume "*/" + consumeChar(); + } + + /** + * @returns Token + */ + function consumeClassReference() { + var classRef = makeToken("CLASS_REF"); + var value = consumeChar(); + if (currentChar() === "{") { + classRef.template = true; + value += consumeChar(); + while (currentChar() && currentChar() !== "}") { + value += consumeChar(); + } + if (currentChar() !== "}") { + throw Error("Unterminated class reference"); + } else { + value += consumeChar(); // consume final curly + } + } else { + while (Lexer.isValidCSSClassChar(currentChar())) { + value += consumeChar(); + } + } + classRef.value = value; + classRef.end = position; + return classRef; + } + + /** + * @returns Token + */ + function consumeAttributeReference() { + var attributeRef = makeToken("ATTRIBUTE_REF"); + var value = consumeChar(); + while (position < source.length && currentChar() !== "]") { + value += consumeChar(); + } + if (currentChar() === "]") { + value += consumeChar(); + } + attributeRef.value = value; + attributeRef.end = position; + return attributeRef; + } + + function consumeShortAttributeReference() { + var attributeRef = makeToken("ATTRIBUTE_REF"); + var value = consumeChar(); + while (Lexer.isValidCSSIDChar(currentChar())) { + value += consumeChar(); + } + if (currentChar() === '=') { + value += consumeChar(); + if (currentChar() === '"' || currentChar() === "'") { + let stringValue = consumeString(); + value += stringValue.value; + } else if(Lexer.isAlpha(currentChar()) || + Lexer.isNumeric(currentChar()) || + Lexer.isIdentifierChar(currentChar())) { + let id = consumeIdentifier(); + value += id.value; + } + } + attributeRef.value = value; + attributeRef.end = position; + return attributeRef; + } + + function consumeStyleReference() { + var styleRef = makeToken("STYLE_REF"); + var value = consumeChar(); + while (Lexer.isAlpha(currentChar()) || currentChar() === "-") { + value += consumeChar(); + } + styleRef.value = value; + styleRef.end = position; + return styleRef; + } + + /** + * @returns Token + */ + function consumeIdReference() { + var idRef = makeToken("ID_REF"); + var value = consumeChar(); + if (currentChar() === "{") { + idRef.template = true; + value += consumeChar(); + while (currentChar() && currentChar() !== "}") { + value += consumeChar(); + } + if (currentChar() !== "}") { + throw Error("Unterminated id reference"); + } else { + consumeChar(); // consume final quote + } + } else { + while (Lexer.isValidCSSIDChar(currentChar())) { + value += consumeChar(); + } + } + idRef.value = value; + idRef.end = position; + return idRef; + } + + /** + * @returns Token + */ + function consumeIdentifier() { + var identifier = makeToken("IDENTIFIER"); + var value = consumeChar(); + while (Lexer.isAlpha(currentChar()) || + Lexer.isNumeric(currentChar()) || + Lexer.isIdentifierChar(currentChar())) { + value += consumeChar(); + } + if (currentChar() === "!" && value === "beep") { + value += consumeChar(); + } + identifier.value = value; + identifier.end = position; + return identifier; + } + + /** + * @returns Token + */ + function consumeNumber() { + var number = makeToken("NUMBER"); + var value = consumeChar(); + + // given possible XXX.YYY(e|E)[-]ZZZ consume XXX + while (Lexer.isNumeric(currentChar())) { + value += consumeChar(); + } + + // consume .YYY + if (currentChar() === "." && Lexer.isNumeric(nextChar())) { + value += consumeChar(); + } + while (Lexer.isNumeric(currentChar())) { + value += consumeChar(); + } + + // consume (e|E)[-] + if (currentChar() === "e" || currentChar() === "E") { + // possible scientific notation, e.g. 1e6 or 1e-6 + if (Lexer.isNumeric(nextChar())) { + // e.g. 1e6 + value += consumeChar(); + } else if (nextChar() === "-") { + // e.g. 1e-6 + value += consumeChar(); + // consume the - as well since otherwise we would stop on the next loop + value += consumeChar(); + } + } + + // consume ZZZ + while (Lexer.isNumeric(currentChar())) { + value += consumeChar(); + } + number.value = value; + number.end = position; + return number; + } + + /** + * @returns Token + */ + function consumeOp() { + var op = makeOpToken(); + var value = consumeChar(); // consume leading char + while (currentChar() && Lexer.OP_TABLE[value + currentChar()]) { + value += consumeChar(); + } + op.type = Lexer.OP_TABLE[value]; + op.value = value; + op.end = position; + return op; + } + + /** + * @returns Token + */ + function consumeString() { + var string = makeToken("STRING"); + var startChar = consumeChar(); // consume leading quote + var value = ""; + while (currentChar() && currentChar() !== startChar) { + if (currentChar() === "\\") { + consumeChar(); // consume escape char and get the next one + let nextChar = consumeChar(); + if (nextChar === "b") { + value += "\b"; + } else if (nextChar === "f") { + value += "\f"; + } else if (nextChar === "n") { + value += "\n"; + } else if (nextChar === "r") { + value += "\r"; + } else if (nextChar === "t") { + value += "\t"; + } else if (nextChar === "v") { + value += "\v"; + } else if (nextChar === "x") { + const hex = consumeHexEscape(); + if (Number.isNaN(hex)) { + throw Error("Invalid hexadecimal escape at " + Lexer.positionString(string)); + } + value += String.fromCharCode(hex); + } else { + value += nextChar; + } + } else { + value += consumeChar(); + } + } + if (currentChar() !== startChar) { + throw Error("Unterminated string at " + Lexer.positionString(string)); + } else { + consumeChar(); // consume final quote + } + string.value = value; + string.end = position; + string.template = startChar === "`"; + return string; + } + + /** + * @returns number + */ + function consumeHexEscape() { + const BASE = 16; + if (!currentChar()) { + return NaN; + } + let result = BASE * Number.parseInt(consumeChar(), BASE); + if (!currentChar()) { + return NaN; + } + result += Number.parseInt(consumeChar(), BASE); + + return result; + } + + /** + * @returns string + */ + function currentChar() { + return source.charAt(position); + } + + /** + * @returns string + */ + function nextChar() { + return source.charAt(position + 1); + } + + function nextCharAt(number = 1) { + return source.charAt(position + number); + } + + /** + * @returns string + */ + function consumeChar() { + lastToken = currentChar(); + position++; + column++; + return lastToken; + } + + /** + * @returns boolean + */ + function possiblePrecedingSymbol() { + return ( + Lexer.isAlpha(lastToken) || + Lexer.isNumeric(lastToken) || + lastToken === ")" || + lastToken === "\"" || + lastToken === "'" || + lastToken === "`" || + lastToken === "}" || + lastToken === "]" + ); + } + + /** + * @returns Token + */ + function consumeWhitespace() { + var whitespace = makeToken("WHITESPACE"); + var value = ""; + while (currentChar() && Lexer.isWhitespace(currentChar())) { + if (Lexer.isNewline(currentChar())) { + column = 0; + line++; + } + value += consumeChar(); + } + whitespace.value = value; + whitespace.end = position; + return whitespace; + } + } + + /** + * @param {string} string + * @param {boolean} [template] + * @returns {Tokens} + */ + tokenize(string, template) { + return Lexer.tokenize(string, template) + } + } + + /** + * @typedef {Object} Token + * @property {string} [type] + * @property {string} value + * @property {number} [start] + * @property {number} [end] + * @property {number} [column] + * @property {number} [line] + * @property {boolean} [op] `true` if this token represents an operator + * @property {boolean} [template] `true` if this token is a template, for class refs, id refs, strings + */ + + class Tokens { + constructor(tokens, consumed, source) { + this.tokens = tokens + this.consumed = consumed + this.source = source + + this.consumeWhitespace(); // consume initial whitespace + } + + get list() { + return this.tokens + } + + /** @type Token | null */ + _lastConsumed = null; + + consumeWhitespace() { + while (this.token(0, true).type === "WHITESPACE") { + this.consumed.push(this.tokens.shift()); + } + } + + /** + * @param {Tokens} tokens + * @param {*} error + * @returns {never} + */ + raiseError(tokens, error) { + Parser.raiseParseError(tokens, error); + } + + /** + * @param {string} value + * @returns {Token} + */ + requireOpToken(value) { + var token = this.matchOpToken(value); + if (token) { + return token; + } else { + this.raiseError(this, "Expected '" + value + "' but found '" + this.currentToken().value + "'"); + } + } + + /** + * @param {string} op1 + * @param {string} [op2] + * @param {string} [op3] + * @returns {Token | void} + */ + matchAnyOpToken(op1, op2, op3) { + for (var i = 0; i < arguments.length; i++) { + var opToken = arguments[i]; + var match = this.matchOpToken(opToken); + if (match) { + return match; + } + } + } + + /** + * @param {string} op1 + * @param {string} [op2] + * @param {string} [op3] + * @returns {Token | void} + */ + matchAnyToken(op1, op2, op3) { + for (var i = 0; i < arguments.length; i++) { + var opToken = arguments[i]; + var match = this.matchToken(opToken); + if (match) { + return match; + } + } + } + + /** + * @param {string} value + * @returns {Token | void} + */ + matchOpToken(value) { + if (this.currentToken() && this.currentToken().op && this.currentToken().value === value) { + return this.consumeToken(); + } + } + + /** + * @param {string} type1 + * @param {string} [type2] + * @param {string} [type3] + * @param {string} [type4] + * @returns {Token} + */ + requireTokenType(type1, type2, type3, type4) { + var token = this.matchTokenType(type1, type2, type3, type4); + if (token) { + return token; + } else { + this.raiseError(this, "Expected one of " + JSON.stringify([type1, type2, type3])); + } + } + + /** + * @param {string} type1 + * @param {string} [type2] + * @param {string} [type3] + * @param {string} [type4] + * @returns {Token | void} + */ + matchTokenType(type1, type2, type3, type4) { + if ( + this.currentToken() && + this.currentToken().type && + [type1, type2, type3, type4].indexOf(this.currentToken().type) >= 0 + ) { + return this.consumeToken(); + } + } + + /** + * @param {string} value + * @param {string} [type] + * @returns {Token} + */ + requireToken(value, type) { + var token = this.matchToken(value, type); + if (token) { + return token; + } else { + this.raiseError(this, "Expected '" + value + "' but found '" + this.currentToken().value + "'"); + } + } + + peekToken(value, peek, type) { + peek = peek || 0; + type = type || "IDENTIFIER"; + if(this.tokens[peek] && this.tokens[peek].value === value && this.tokens[peek].type === type){ + return this.tokens[peek]; + } + } + + /** + * @param {string} value + * @param {string} [type] + * @returns {Token | void} + */ + matchToken(value, type) { + if (this.follows.indexOf(value) !== -1) { + return; // disallowed token here + } + type = type || "IDENTIFIER"; + if (this.currentToken() && this.currentToken().value === value && this.currentToken().type === type) { + return this.consumeToken(); + } + } + + /** + * @returns {Token} + */ + consumeToken() { + var match = this.tokens.shift(); + this.consumed.push(match); + this._lastConsumed = match; + this.consumeWhitespace(); // consume any whitespace + return match; + } + + /** + * @param {string | null} value + * @param {string | null} [type] + * @returns {Token[]} + */ + consumeUntil(value, type) { + /** @type Token[] */ + var tokenList = []; + var currentToken = this.token(0, true); + + while ( + (type == null || currentToken.type !== type) && + (value == null || currentToken.value !== value) && + currentToken.type !== "EOF" + ) { + var match = this.tokens.shift(); + this.consumed.push(match); + tokenList.push(currentToken); + currentToken = this.token(0, true); + } + this.consumeWhitespace(); // consume any whitespace + return tokenList; + } + + /** + * @returns {string} + */ + lastWhitespace() { + if (this.consumed[this.consumed.length - 1] && this.consumed[this.consumed.length - 1].type === "WHITESPACE") { + return this.consumed[this.consumed.length - 1].value; + } else { + return ""; + } + } + + consumeUntilWhitespace() { + return this.consumeUntil(null, "WHITESPACE"); + } + + /** + * @returns {boolean} + */ + hasMore() { + return this.tokens.length > 0; + } + + /** + * @param {number} n + * @param {boolean} [dontIgnoreWhitespace] + * @returns {Token} + */ + token(n, dontIgnoreWhitespace) { + var /**@type {Token}*/ token; + var i = 0; + do { + if (!dontIgnoreWhitespace) { + while (this.tokens[i] && this.tokens[i].type === "WHITESPACE") { + i++; + } + } + token = this.tokens[i]; + n--; + i++; + } while (n > -1); + if (token) { + return token; + } else { + return { + type: "EOF", + value: "<<>>", + }; + } + } + + /** + * @returns {Token} + */ + currentToken() { + return this.token(0); + } + + /** + * @returns {Token | null} + */ + lastMatch() { + return this._lastConsumed; + } + + /** + * @returns {string} + */ + static sourceFor = function () { + return this.programSource.substring(this.startToken.start, this.endToken.end); + } + + /** + * @returns {string} + */ + static lineFor = function () { + return this.programSource.split("\n")[this.startToken.line - 1]; + } + + follows = []; + + pushFollow(str) { + this.follows.push(str); + } + + popFollow() { + this.follows.pop(); + } + + clearFollows() { + var tmp = this.follows; + this.follows = []; + return tmp; + } + + restoreFollows(f) { + this.follows = f; + } + } + + /** + * @callback ParseRule + * @param {Parser} parser + * @param {Runtime} runtime + * @param {Tokens} tokens + * @param {*} [root] + * @returns {ASTNode | undefined} + * + * @typedef {Object} ASTNode + * @member {boolean} isFeature + * @member {string} type + * @member {any[]} args + * @member {(this: ASTNode, ctx:Context, root:any, ...args:any) => any} op + * @member {(this: ASTNode, context?:Context) => any} evaluate + * @member {ASTNode} parent + * @member {Set} children + * @member {ASTNode} root + * @member {String} keyword + * @member {Token} endToken + * @member {ASTNode} next + * @member {(context:Context) => ASTNode} resolveNext + * @member {EventSource} eventSource + * @member {(this: ASTNode) => void} install + * @member {(this: ASTNode, context:Context) => void} execute + * @member {(this: ASTNode, target: object, source: object, args?: Object) => void} apply + * + * + */ + + class Parser { + /** + * + * @param {Runtime} runtime + */ + constructor(runtime) { + this.runtime = runtime + + this.possessivesDisabled = false + + /* ============================================================================================ */ + /* Core hyperscript Grammar Elements */ + /* ============================================================================================ */ + this.addGrammarElement("feature", function (parser, runtime, tokens) { + if (tokens.matchOpToken("(")) { + var featureElement = parser.requireElement("feature", tokens); + tokens.requireOpToken(")"); + return featureElement; + } + + var featureDefinition = parser.FEATURES[tokens.currentToken().value || ""]; + if (featureDefinition) { + return featureDefinition(parser, runtime, tokens); + } + }); + + this.addGrammarElement("command", function (parser, runtime, tokens) { + if (tokens.matchOpToken("(")) { + const commandElement = parser.requireElement("command", tokens); + tokens.requireOpToken(")"); + return commandElement; + } + + var commandDefinition = parser.COMMANDS[tokens.currentToken().value || ""]; + let commandElement; + if (commandDefinition) { + commandElement = commandDefinition(parser, runtime, tokens); + } else if (tokens.currentToken().type === "IDENTIFIER") { + commandElement = parser.parseElement("pseudoCommand", tokens); + } + if (commandElement) { + return parser.parseElement("indirectStatement", tokens, commandElement); + } + + return commandElement; + }); + + this.addGrammarElement("commandList", function (parser, runtime, tokens) { + if (tokens.hasMore()) { + var cmd = parser.parseElement("command", tokens); + if (cmd) { + tokens.matchToken("then"); + const next = parser.parseElement("commandList", tokens); + if (next) cmd.next = next; + return cmd; + } + } + return { + type: "emptyCommandListCommand", + op: function(context){ + return runtime.findNext(this, context); + }, + execute: function (context) { + return runtime.unifiedExec(this, context); + } + } + }); + + this.addGrammarElement("leaf", function (parser, runtime, tokens) { + var result = parser.parseAnyOf(parser.LEAF_EXPRESSIONS, tokens); + // symbol is last so it doesn't consume any constants + if (result == null) { + return parser.parseElement("symbol", tokens); + } + + return result; + }); + + this.addGrammarElement("indirectExpression", function (parser, runtime, tokens, root) { + for (var i = 0; i < parser.INDIRECT_EXPRESSIONS.length; i++) { + var indirect = parser.INDIRECT_EXPRESSIONS[i]; + root.endToken = tokens.lastMatch(); + var result = parser.parseElement(indirect, tokens, root); + if (result) { + return result; + } + } + return root; + }); + + this.addGrammarElement("indirectStatement", function (parser, runtime, tokens, root) { + if (tokens.matchToken("unless")) { + root.endToken = tokens.lastMatch(); + var conditional = parser.requireElement("expression", tokens); + var unless = { + type: "unlessStatementModifier", + args: [conditional], + op: function (context, conditional) { + if (conditional) { + return this.next; + } else { + return root; + } + }, + execute: function (context) { + return runtime.unifiedExec(this, context); + }, + }; + root.parent = unless; + return unless; + } + return root; + }); + + this.addGrammarElement("primaryExpression", function (parser, runtime, tokens) { + var leaf = parser.parseElement("leaf", tokens); + if (leaf) { + return parser.parseElement("indirectExpression", tokens, leaf); + } + parser.raiseParseError(tokens, "Unexpected value: " + tokens.currentToken().value); + }); + } + + use(plugin) { + plugin(this) + return this + } + + /** @type {Object} */ + GRAMMAR = {}; + + /** @type {Object} */ + COMMANDS = {}; + + /** @type {Object} */ + FEATURES = {}; + + /** @type {string[]} */ + LEAF_EXPRESSIONS = []; + /** @type {string[]} */ + INDIRECT_EXPRESSIONS = []; + + /** + * @param {*} parseElement + * @param {*} start + * @param {Tokens} tokens + */ + initElt(parseElement, start, tokens) { + parseElement.startToken = start; + parseElement.sourceFor = Tokens.sourceFor; + parseElement.lineFor = Tokens.lineFor; + parseElement.programSource = tokens.source; + } + + /** + * @param {string} type + * @param {Tokens} tokens + * @param {ASTNode?} root + * @returns {ASTNode} + */ + parseElement(type, tokens, root = undefined) { + var elementDefinition = this.GRAMMAR[type]; + if (elementDefinition) { + var start = tokens.currentToken(); + var parseElement = elementDefinition(this, this.runtime, tokens, root); + if (parseElement) { + this.initElt(parseElement, start, tokens); + parseElement.endToken = parseElement.endToken || tokens.lastMatch(); + var root = parseElement.root; + while (root != null) { + this.initElt(root, start, tokens); + root = root.root; + } + } + return parseElement; + } + } + + /** + * @param {string} type + * @param {Tokens} tokens + * @param {string} [message] + * @param {*} [root] + * @returns {ASTNode} + */ + requireElement(type, tokens, message, root) { + var result = this.parseElement(type, tokens, root); + if (!result) Parser.raiseParseError(tokens, message || "Expected " + type); + // @ts-ignore + return result; + } + + /** + * @param {string[]} types + * @param {Tokens} tokens + * @returns {ASTNode} + */ + parseAnyOf(types, tokens) { + for (var i = 0; i < types.length; i++) { + var type = types[i]; + var expression = this.parseElement(type, tokens); + if (expression) { + return expression; + } + } + } + + /** + * @param {string} name + * @param {ParseRule} definition + */ + addGrammarElement(name, definition) { + this.GRAMMAR[name] = definition; + } + + /** + * @param {string} keyword + * @param {ParseRule} definition + */ + addCommand(keyword, definition) { + var commandGrammarType = keyword + "Command"; + var commandDefinitionWrapper = function (parser, runtime, tokens) { + const commandElement = definition(parser, runtime, tokens); + if (commandElement) { + commandElement.type = commandGrammarType; + commandElement.execute = function (context) { + context.meta.command = commandElement; + return runtime.unifiedExec(this, context); + }; + return commandElement; + } + }; + this.GRAMMAR[commandGrammarType] = commandDefinitionWrapper; + this.COMMANDS[keyword] = commandDefinitionWrapper; + } + + /** + * @param {string} keyword + * @param {ParseRule} definition + */ + addFeature(keyword, definition) { + var featureGrammarType = keyword + "Feature"; + + /** @type {ParseRule} */ + var featureDefinitionWrapper = function (parser, runtime, tokens) { + var featureElement = definition(parser, runtime, tokens); + if (featureElement) { + featureElement.isFeature = true; + featureElement.keyword = keyword; + featureElement.type = featureGrammarType; + return featureElement; + } + }; + this.GRAMMAR[featureGrammarType] = featureDefinitionWrapper; + this.FEATURES[keyword] = featureDefinitionWrapper; + } + + /** + * @param {string} name + * @param {ParseRule} definition + */ + addLeafExpression(name, definition) { + this.LEAF_EXPRESSIONS.push(name); + this.addGrammarElement(name, definition); + } + + /** + * @param {string} name + * @param {ParseRule} definition + */ + addIndirectExpression(name, definition) { + this.INDIRECT_EXPRESSIONS.push(name); + this.addGrammarElement(name, definition); + } + + /** + * + * @param {Tokens} tokens + * @returns string + */ + static createParserContext(tokens) { + var currentToken = tokens.currentToken(); + var source = tokens.source; + var lines = source.split("\n"); + var line = currentToken && currentToken.line ? currentToken.line - 1 : lines.length - 1; + var contextLine = lines[line]; + var offset = /** @type {number} */ ( + currentToken && currentToken.line ? currentToken.column : contextLine.length - 1); + return contextLine + "\n" + " ".repeat(offset) + "^^\n\n"; + } + + /** + * @param {Tokens} tokens + * @param {string} [message] + * @returns {never} + */ + static raiseParseError(tokens, message) { + message = + (message || "Unexpected Token : " + tokens.currentToken().value) + "\n\n" + Parser.createParserContext(tokens); + var error = new Error(message); + error["tokens"] = tokens; + throw error; + } + + /** + * @param {Tokens} tokens + * @param {string} [message] + */ + raiseParseError(tokens, message) { + Parser.raiseParseError(tokens, message) + } + + /** + * @param {Tokens} tokens + * @returns {ASTNode} + */ + parseHyperScript(tokens) { + var result = this.parseElement("hyperscript", tokens); + if (tokens.hasMore()) this.raiseParseError(tokens); + if (result) return result; + } + + /** + * @param {ASTNode | undefined} elt + * @param {ASTNode} parent + */ + setParent(elt, parent) { + if (typeof elt === 'object') { + elt.parent = parent; + if (typeof parent === 'object') { + parent.children = (parent.children || new Set()); + parent.children.add(elt) + } + this.setParent(elt.next, parent); + } + } + + /** + * @param {Token} token + * @returns {ParseRule} + */ + commandStart(token) { + return this.COMMANDS[token.value || ""]; + } + + /** + * @param {Token} token + * @returns {ParseRule} + */ + featureStart(token) { + return this.FEATURES[token.value || ""]; + } + + /** + * @param {Token} token + * @returns {boolean} + */ + commandBoundary(token) { + if ( + token.value == "end" || + token.value == "then" || + token.value == "else" || + token.value == "otherwise" || + token.value == ")" || + this.commandStart(token) || + this.featureStart(token) || + token.type == "EOF" + ) { + return true; + } + return false; + } + + /** + * @param {Tokens} tokens + * @returns {(string | ASTNode)[]} + */ + parseStringTemplate(tokens) { + /** @type {(string | ASTNode)[]} */ + var returnArr = [""]; + do { + returnArr.push(tokens.lastWhitespace()); + if (tokens.currentToken().value === "$") { + tokens.consumeToken(); + var startingBrace = tokens.matchOpToken("{"); + returnArr.push(this.requireElement("expression", tokens)); + if (startingBrace) { + tokens.requireOpToken("}"); + } + returnArr.push(""); + } else if (tokens.currentToken().value === "\\") { + tokens.consumeToken(); // skip next + tokens.consumeToken(); + } else { + var token = tokens.consumeToken(); + returnArr[returnArr.length - 1] += token ? token.value : ""; + } + } while (tokens.hasMore()); + returnArr.push(tokens.lastWhitespace()); + return returnArr; + } + + /** + * @param {ASTNode} commandList + */ + ensureTerminated(commandList) { + const runtime = this.runtime + var implicitReturn = { + type: "implicitReturn", + op: function (context) { + context.meta.returned = true; + if (context.meta.resolve) { + context.meta.resolve(); + } + return runtime.HALT; + }, + execute: function (ctx) { + // do nothing + }, + }; + + var end = commandList; + while (end.next) { + end = end.next; + } + end.next = implicitReturn; + } + } + + class Runtime { + /** + * + * @param {Lexer} [lexer] + * @param {Parser} [parser] + */ + constructor(lexer, parser) { + this.lexer = lexer ?? new Lexer; + this.parser = parser ?? new Parser(this) + .use(hyperscriptCoreGrammar) + .use(hyperscriptWebGrammar); + this.parser.runtime = this + } + + /** + * @param {HTMLElement} elt + * @param {string} selector + * @returns boolean + */ + matchesSelector(elt, selector) { + // noinspection JSUnresolvedVariable + var matchesFunction = + // @ts-ignore + elt.matches || elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector || elt.webkitMatchesSelector || elt.oMatchesSelector; + return matchesFunction && matchesFunction.call(elt, selector); + } + + /** + * @param {string} eventName + * @param {Object} [detail] + * @returns {Event} + */ + makeEvent(eventName, detail) { + var evt; + if (globalScope.Event && typeof globalScope.Event === "function") { + evt = new Event(eventName, { + bubbles: true, + cancelable: true, + }); + evt['detail'] = detail; + } else { + evt = document.createEvent("CustomEvent"); + evt.initCustomEvent(eventName, true, true, detail); + } + return evt; + } + + /** + * @param {Element} elt + * @param {string} eventName + * @param {Object} [detail] + * @param {Element} [sender] + * @returns {boolean} + */ + triggerEvent(elt, eventName, detail, sender) { + detail = detail || {}; + detail["sender"] = sender; + var event = this.makeEvent(eventName, detail); + var eventResult = elt.dispatchEvent(event); + return eventResult; + } + + /** + * isArrayLike returns `true` if the provided value is an array or + * a NodeList (which is close enough to being an array for our purposes). + * + * @param {any} value + * @returns {value is Array | NodeList} + */ + isArrayLike(value) { + return Array.isArray(value) || + (typeof NodeList !== 'undefined' && (value instanceof NodeList || value instanceof HTMLCollection)); + } + + /** + * isIterable returns `true` if the provided value supports the + * iterator protocol. + * + * @param {any} value + * @returns {value is Iterable} + */ + isIterable(value) { + return typeof value === 'object' + && Symbol.iterator in value + && typeof value[Symbol.iterator] === 'function'; + } + + /** + * shouldAutoIterate returns `true` if the provided value + * should be implicitly iterated over when accessing properties, + * and as the target of some commands. + * + * Currently, this is when the value is an {ElementCollection} + * or {isArrayLike} returns true. + * + * @param {any} value + * @returns {value is (any[] | ElementCollection)} + */ + shouldAutoIterate(value) { + return value != null && value[shouldAutoIterateSymbol] || + this.isArrayLike(value); + } + + /** + * forEach executes the provided `func` on every item in the `value` array. + * if `value` is a single item (and not an array) then `func` is simply called + * once. If `value` is null, then no further actions are taken. + * + * @template T + * @param {T | Iterable} value + * @param {(item: T) => void} func + */ + forEach(value, func) { + if (value == null) { + // do nothing + } else if (this.isIterable(value)) { + for (const nth of value) { + func(nth); + } + } else if (this.isArrayLike(value)) { + for (var i = 0; i < value.length; i++) { + func(value[i]); + } + } else { + func(value); + } + } + + /** + * implicitLoop executes the provided `func` on: + * - every item of {value}, if {value} should be auto-iterated + * (see {shouldAutoIterate}) + * - {value} otherwise + * + * @template T + * @param {ElementCollection | T | T[]} value + * @param {(item: T) => void} func + */ + implicitLoop(value, func) { + if (this.shouldAutoIterate(value)) { + for (const x of value) func(x); + } else { + func(value); + } + } + + wrapArrays(args) { + var arr = []; + for (var i = 0; i < args.length; i++) { + var arg = args[i]; + if (Array.isArray(arg)) { + arr.push(Promise.all(arg)); + } else { + arr.push(arg); + } + } + return arr; + } + + unwrapAsyncs(values) { + for (var i = 0; i < values.length; i++) { + var value = values[i]; + if (value.asyncWrapper) { + values[i] = value.value; + } + if (Array.isArray(value)) { + for (var j = 0; j < value.length; j++) { + var valueElement = value[j]; + if (valueElement.asyncWrapper) { + value[j] = valueElement.value; + } + } + } + } + } + + static HALT = {}; + HALT = Runtime.HALT; + + /** + * @param {ASTNode} command + * @param {Context} ctx + */ + unifiedExec(command, ctx) { + while (true) { + try { + var next = this.unifiedEval(command, ctx); + } catch (e) { + if (ctx.meta.handlingFinally) { + console.error(" Exception in finally block: ", e); + next = Runtime.HALT; + } else { + this.registerHyperTrace(ctx, e); + if (ctx.meta.errorHandler && !ctx.meta.handlingError) { + ctx.meta.handlingError = true; + ctx.locals[ctx.meta.errorSymbol] = e; + command = ctx.meta.errorHandler; + continue; + } else { + ctx.meta.currentException = e; + next = Runtime.HALT; + } + } + } + if (next == null) { + console.error(command, " did not return a next element to execute! context: ", ctx); + return; + } else if (next.then) { + next.then(resolvedNext => { + this.unifiedExec(resolvedNext, ctx); + }).catch(reason => { + this.unifiedExec({ // Anonymous command to simply throw the exception + op: function(){ + throw reason; + } + }, ctx); + }); + return; + } else if (next === Runtime.HALT) { + if (ctx.meta.finallyHandler && !ctx.meta.handlingFinally) { + ctx.meta.handlingFinally = true; + command = ctx.meta.finallyHandler; + } else { + if (ctx.meta.onHalt) { + ctx.meta.onHalt(); + } + if (ctx.meta.currentException) { + if (ctx.meta.reject) { + ctx.meta.reject(ctx.meta.currentException); + return; + } else { + throw ctx.meta.currentException; + } + } else { + return; + } + } + } else { + command = next; // move to the next command + } + } + } + + /** + * @param {*} parseElement + * @param {Context} ctx + * @returns {*} + */ + unifiedEval(parseElement, ctx) { + /** @type any[] */ + var args = [ctx]; + var async = false; + var wrappedAsyncs = false; + + if (parseElement.args) { + for (var i = 0; i < parseElement.args.length; i++) { + var argument = parseElement.args[i]; + if (argument == null) { + args.push(null); + } else if (Array.isArray(argument)) { + var arr = []; + for (var j = 0; j < argument.length; j++) { + var element = argument[j]; + var value = element ? element.evaluate(ctx) : null; // OK + if (value) { + if (value.then) { + async = true; + } else if (value.asyncWrapper) { + wrappedAsyncs = true; + } + } + arr.push(value); + } + args.push(arr); + } else if (argument.evaluate) { + var value = argument.evaluate(ctx); // OK + if (value) { + if (value.then) { + async = true; + } else if (value.asyncWrapper) { + wrappedAsyncs = true; + } + } + args.push(value); + } else { + args.push(argument); + } + } + } + if (async) { + return new Promise((resolve, reject) => { + args = this.wrapArrays(args); + Promise.all(args) + .then(function (values) { + if (wrappedAsyncs) { + this.unwrapAsyncs(values); + } + try { + var apply = parseElement.op.apply(parseElement, values); + resolve(apply); + } catch (e) { + reject(e); + } + }) + .catch(function (reason) { + reject(reason); + }); + }); + } else { + if (wrappedAsyncs) { + this.unwrapAsyncs(args); + } + return parseElement.op.apply(parseElement, args); + } + } + + /** + * @type {string[] | null} + */ + _scriptAttrs = null; + + /** + * getAttributes returns the attribute name(s) to use when + * locating hyperscript scripts in a DOM element. If no value + * has been configured, it defaults to config.attributes + * @returns string[] + */ + getScriptAttributes() { + if (this._scriptAttrs == null) { + this._scriptAttrs = config.attributes.replace(/ /g, "").split(","); + } + return this._scriptAttrs; + } + + /** + * @param {Element} elt + * @returns {string | null} + */ + getScript(elt) { + for (var i = 0; i < this.getScriptAttributes().length; i++) { + var scriptAttribute = this.getScriptAttributes()[i]; + if (elt.hasAttribute && elt.hasAttribute(scriptAttribute)) { + return elt.getAttribute(scriptAttribute); + } + } + if (elt instanceof HTMLScriptElement && elt.type === "text/hyperscript") { + return elt.innerText; + } + return null; + } + + hyperscriptFeaturesMap = new WeakMap + + /** + * @param {*} elt + * @returns {Object} + */ + getHyperscriptFeatures(elt) { + var hyperscriptFeatures = this.hyperscriptFeaturesMap.get(elt); + if (typeof hyperscriptFeatures === 'undefined') { + if (elt) { + // in some rare cases, elt is null and this line crashes + this.hyperscriptFeaturesMap.set(elt, hyperscriptFeatures = {}); + } + } + return hyperscriptFeatures; + } + + /** + * @param {Object} owner + * @param {Context} ctx + */ + addFeatures(owner, ctx) { + if (owner) { + Object.assign(ctx.locals, this.getHyperscriptFeatures(owner)); + this.addFeatures(owner.parentElement, ctx); + } + } + + /** + * @param {*} owner + * @param {*} feature + * @param {*} hyperscriptTarget + * @param {*} event + * @returns {Context} + */ + makeContext(owner, feature, hyperscriptTarget, event) { + return new Context(owner, feature, hyperscriptTarget, event, this) + } + + /** + * @returns string + */ + getScriptSelector() { + return this.getScriptAttributes() + .map(function (attribute) { + return "[" + attribute + "]"; + }) + .join(", "); + } + + /** + * @param {any} value + * @param {string} type + * @returns {any} + */ + convertValue(value, type) { + var dynamicResolvers = conversions.dynamicResolvers; + for (var i = 0; i < dynamicResolvers.length; i++) { + var dynamicResolver = dynamicResolvers[i]; + var converted = dynamicResolver(type, value); + if (converted !== undefined) { + return converted; + } + } + + if (value == null) { + return null; + } + var converter = conversions[type]; + if (converter) { + return converter(value); + } + + throw "Unknown conversion : " + type; + } + + /** + * @param {string} src + * @returns {ASTNode} + */ + parse(src) { + const lexer = this.lexer, parser = this.parser + var tokens = lexer.tokenize(src); + if (this.parser.commandStart(tokens.currentToken())) { + var commandList = parser.requireElement("commandList", tokens); + if (tokens.hasMore()) parser.raiseParseError(tokens); + parser.ensureTerminated(commandList); + return commandList; + } else if (parser.featureStart(tokens.currentToken())) { + var hyperscript = parser.requireElement("hyperscript", tokens); + if (tokens.hasMore()) parser.raiseParseError(tokens); + return hyperscript; + } else { + var expression = parser.requireElement("expression", tokens); + if (tokens.hasMore()) parser.raiseParseError(tokens); + return expression; + } + } + + /** + * + * @param {ASTNode} elt + * @param {Context} ctx + * @returns {any} + */ + evaluateNoPromise(elt, ctx) { + let result = elt.evaluate(ctx); + if (result.next) { + throw new Error(Tokens.sourceFor.call(elt) + " returned a Promise in a context that they are not allowed."); + } + return result; + } + + /** + * @param {string} src + * @param {Partial} [ctx] + * @param {Object} [args] + * @returns {any} + */ + evaluate(src, ctx, args) { + class HyperscriptModule extends EventTarget { + constructor(mod) { + super(); + this.module = mod; + } + toString() { + return this.module.id; + } + } + + var body = 'document' in globalScope + ? globalScope.document.body + : new HyperscriptModule(args && args.module); + ctx = Object.assign(this.makeContext(body, null, body, null), ctx || {}); + var element = this.parse(src); + if (element.execute) { + element.execute(ctx); + if(typeof ctx.meta.returnValue !== 'undefined'){ + return ctx.meta.returnValue; + } else { + return ctx.result; + } + } else if (element.apply) { + element.apply(body, body, args); + return this.getHyperscriptFeatures(body); + } else { + return element.evaluate(ctx); + } + + function makeModule() { + return {} + } + } + + /** + * @param {HTMLElement} elt + */ + processNode(elt) { + var selector = this.getScriptSelector(); + if (this.matchesSelector(elt, selector)) { + this.initElement(elt, elt); + } + if (elt instanceof HTMLScriptElement && elt.type === "text/hyperscript") { + this.initElement(elt, document.body); + } + if (elt.querySelectorAll) { + this.forEach(elt.querySelectorAll(selector + ", [type='text/hyperscript']"), elt => { + this.initElement(elt, elt instanceof HTMLScriptElement && elt.type === "text/hyperscript" ? document.body : elt); + }); + } + } + + /** + * @param {Element} elt + * @param {Element} [target] + */ + initElement(elt, target) { + if (elt.closest && elt.closest(config.disableSelector)) { + return; + } + var internalData = this.getInternalData(elt); + if (!internalData.initialized) { + var src = this.getScript(elt); + if (src) { + try { + internalData.initialized = true; + internalData.script = src; + const lexer = this.lexer, parser = this.parser + var tokens = lexer.tokenize(src); + var hyperScript = parser.parseHyperScript(tokens); + if (!hyperScript) return; + hyperScript.apply(target || elt, elt); + setTimeout(() => { + this.triggerEvent(target || elt, "load", { + hyperscript: true, + }); + }, 1); + } catch (e) { + this.triggerEvent(elt, "exception", { + error: e, + }); + console.error( + "hyperscript errors were found on the following element:", + elt, + "\n\n", + e.message, + e.stack + ); + } + } + } + } + + internalDataMap = new WeakMap + + /** + * @param {Element} elt + * @returns {Object} + */ + getInternalData(elt) { + var internalData = this.internalDataMap.get(elt); + if (typeof internalData === 'undefined') { + this.internalDataMap.set(elt, internalData = {}); + } + return internalData; + } + + /** + * @param {any} value + * @param {string} typeString + * @param {boolean} [nullOk] + * @returns {boolean} + */ + typeCheck(value, typeString, nullOk) { + if (value == null && nullOk) { + return true; + } + var typeName = Object.prototype.toString.call(value).slice(8, -1); + return typeName === typeString; + } + + getElementScope(context) { + var elt = context.meta && context.meta.owner; + if (elt) { + var internalData = this.getInternalData(elt); + var scopeName = "elementScope"; + if (context.meta.feature && context.meta.feature.behavior) { + scopeName = context.meta.feature.behavior + "Scope"; + } + var elementScope = getOrInitObject(internalData, scopeName); + return elementScope; + } else { + return {}; // no element, return empty scope + } + } + + /** + * @param {string} str + * @returns {boolean} + */ + isReservedWord(str) { + return ["meta", "it", "result", "locals", "event", "target", "detail", "sender", "body"].includes(str) + } + + /** + * @param {any} context + * @returns {boolean} + */ + isHyperscriptContext(context) { + return context instanceof Context; + } + + /** + * @param {string} str + * @param {Context} context + * @returns {any} + */ + resolveSymbol(str, context, type) { + if (str === "me" || str === "my" || str === "I") { + return context.me; + } + if (str === "it" || str === "its" || str === "result") { + return context.result; + } + if (str === "you" || str === "your" || str === "yourself") { + return context.you; + } else { + if (type === "global") { + return globalScope[str]; + } else if (type === "element") { + var elementScope = this.getElementScope(context); + return elementScope[str]; + } else if (type === "local") { + return context.locals[str]; + } else { + // meta scope (used for event conditionals) + if (context.meta && context.meta.context) { + var fromMetaContext = context.meta.context[str]; + if (typeof fromMetaContext !== "undefined") { + return fromMetaContext; + } + // resolve against the `detail` object in the meta context as well + if (context.meta.context.detail) { + fromMetaContext = context.meta.context.detail[str]; + if (typeof fromMetaContext !== "undefined") { + return fromMetaContext; + } + } + } + if (this.isHyperscriptContext(context) && !this.isReservedWord(str)) { + // local scope + var fromContext = context.locals[str]; + } else { + // direct get from normal JS object or top-level of context + var fromContext = context[str]; + } + if (typeof fromContext !== "undefined") { + return fromContext; + } else { + // element scope + var elementScope = this.getElementScope(context); + fromContext = elementScope[str]; + if (typeof fromContext !== "undefined") { + return fromContext; + } else { + // global scope + return globalScope[str]; + } + } + } + } + } + + setSymbol(str, context, type, value) { + if (type === "global") { + globalScope[str] = value; + } else if (type === "element") { + var elementScope = this.getElementScope(context); + elementScope[str] = value; + } else if (type === "local") { + context.locals[str] = value; + } else { + if (this.isHyperscriptContext(context) && !this.isReservedWord(str) && typeof context.locals[str] !== "undefined") { + // local scope + context.locals[str] = value; + } else { + // element scope + var elementScope = this.getElementScope(context); + var fromContext = elementScope[str]; + if (typeof fromContext !== "undefined") { + elementScope[str] = value; + } else { + if (this.isHyperscriptContext(context) && !this.isReservedWord(str)) { + // local scope + context.locals[str] = value; + } else { + // direct set on normal JS object or top-level of context + context[str] = value; + } + } + } + } + } + + /** + * @param {ASTNode} command + * @param {Context} context + * @returns {undefined | ASTNode} + */ + findNext(command, context) { + if (command) { + if (command.resolveNext) { + return command.resolveNext(context); + } else if (command.next) { + return command.next; + } else { + return this.findNext(command.parent, context); + } + } + } + + /** + * @param {Object} root + * @param {string} property + * @param {Getter} getter + * @returns {any} + * + * @callback Getter + * @param {Object} root + * @param {string} property + */ + flatGet(root, property, getter) { + if (root != null) { + var val = getter(root, property); + if (typeof val !== "undefined") { + return val; + } + + if (this.shouldAutoIterate(root)) { + // flat map + var result = []; + for (var component of root) { + var componentValue = getter(component, property); + result.push(componentValue); + } + return result; + } + } + } + + resolveProperty(root, property) { + return this.flatGet(root, property, (root, property) => root[property] ) + } + + resolveAttribute(root, property) { + return this.flatGet(root, property, (root, property) => root.getAttribute && root.getAttribute(property) ) + } + + /** + * + * @param {Object} root + * @param {string} property + * @returns {string} + */ + resolveStyle(root, property) { + return this.flatGet(root, property, (root, property) => root.style && root.style[property] ) + } + + /** + * + * @param {Object} root + * @param {string} property + * @returns {string} + */ + resolveComputedStyle(root, property) { + return this.flatGet(root, property, (root, property) => getComputedStyle( + /** @type {Element} */ (root)).getPropertyValue(property) ) + } + + /** + * @param {Element} elt + * @param {string[]} nameSpace + * @param {string} name + * @param {any} value + */ + assignToNamespace(elt, nameSpace, name, value) { + let root + if (typeof document !== "undefined" && elt === document.body) { + root = globalScope; + } else { + root = this.getHyperscriptFeatures(elt); + } + var propertyName; + while ((propertyName = nameSpace.shift()) !== undefined) { + var newRoot = root[propertyName]; + if (newRoot == null) { + newRoot = {}; + root[propertyName] = newRoot; + } + root = newRoot; + } + + root[name] = value; + } + + getHyperTrace(ctx, thrown) { + var trace = []; + var root = ctx; + while (root.meta.caller) { + root = root.meta.caller; + } + if (root.meta.traceMap) { + return root.meta.traceMap.get(thrown, trace); + } + } + + registerHyperTrace(ctx, thrown) { + var trace = []; + var root = null; + while (ctx != null) { + trace.push(ctx); + root = ctx; + ctx = ctx.meta.caller; + } + if (root.meta.traceMap == null) { + root.meta.traceMap = new Map(); // TODO - WeakMap? + } + if (!root.meta.traceMap.get(thrown)) { + var traceEntry = { + trace: trace, + print: function (logger) { + logger = logger || console.error; + logger("hypertrace /// "); + var maxLen = 0; + for (var i = 0; i < trace.length; i++) { + maxLen = Math.max(maxLen, trace[i].meta.feature.displayName.length); + } + for (var i = 0; i < trace.length; i++) { + var traceElt = trace[i]; + logger( + " ->", + traceElt.meta.feature.displayName.padEnd(maxLen + 2), + "-", + traceElt.meta.owner + ); + } + }, + }; + root.meta.traceMap.set(thrown, traceEntry); + } + } + + /** + * @param {string} str + * @returns {string} + */ + escapeSelector(str) { + return str.replace(/:/g, function (str) { + return "\\" + str; + }); + } + + /** + * @param {any} value + * @param {*} elt + */ + nullCheck(value, elt) { + if (value == null) { + throw new Error("'" + elt.sourceFor() + "' is null"); + } + } + + /** + * @param {any} value + * @returns {boolean} + */ + isEmpty(value) { + return value == undefined || value.length === 0; + } + + /** + * @param {any} value + * @returns {boolean} + */ + doesExist(value) { + if(value == null){ + return false; + } + if (this.shouldAutoIterate(value)) { + for (const elt of value) { + return true; + } + return false; + } + return true; + } + + /** + * @param {Node} node + * @returns {Document|ShadowRoot} + */ + getRootNode(node) { + if (node && node instanceof Node) { + var rv = node.getRootNode(); + if (rv instanceof Document || rv instanceof ShadowRoot) return rv; + } + return document; + } + + /** + * + * @param {Element} elt + * @param {ASTNode} onFeature + * @returns {EventQueue} + * + * @typedef {{queue:Array, executing:boolean}} EventQueue + */ + getEventQueueFor(elt, onFeature) { + let internalData = this.getInternalData(elt); + var eventQueuesForElt = internalData.eventQueues; + if (eventQueuesForElt == null) { + eventQueuesForElt = new Map(); + internalData.eventQueues = eventQueuesForElt; + } + var eventQueueForFeature = eventQueuesForElt.get(onFeature); + if (eventQueueForFeature == null) { + eventQueueForFeature = {queue:[], executing:false}; + eventQueuesForElt.set(onFeature, eventQueueForFeature); + } + return eventQueueForFeature; + } + + beepValueToConsole(element, expression, value) { + if (this.triggerEvent(element, "hyperscript:beep", {element, expression, value})) { + var typeName; + if (value) { + if (value instanceof ElementCollection) { + typeName = "ElementCollection"; + } else if (value.constructor) { + typeName = value.constructor.name; + } else { + typeName = "unknown"; + } + } else { + typeName = "object (null)" + } + var logValue = value; + if (typeName === "String") { + logValue = '"' + logValue + '"'; + } else if (value instanceof ElementCollection) { + logValue = Array.from(value); + } + console.log("///_ BEEP! The expression (" + Tokens.sourceFor.call(expression).replace("beep! ", "") + ") evaluates to:", logValue, "of type " + typeName); + } + } + + + /** @type string | null */ + // @ts-ignore + hyperscriptUrl = "document" in globalScope && document.currentScript ? document.currentScript.src : null; + } + + + function getCookiesAsArray() { + let cookiesAsArray = document.cookie + .split("; ") + .map(cookieEntry => { + let strings = cookieEntry.split("="); + return {name: strings[0], value: decodeURIComponent(strings[1])} + }); + return cookiesAsArray; + } + + function clearCookie(name) { + document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT"; + } + + function clearAllCookies() { + for (const cookie of getCookiesAsArray()) { + clearCookie(cookie.name); + } + } + + const CookieJar = new Proxy({}, { + get(target, prop) { + if (prop === 'then' || prop === 'asyncWrapper') { // ignore special symbols + return null; + } else if (prop === 'length') { + return getCookiesAsArray().length + } else if (prop === 'clear') { + return clearCookie; + } else if (prop === 'clearAll') { + return clearAllCookies; + } else if (typeof prop === "string") { + if (!isNaN(prop)) { + return getCookiesAsArray()[parseInt(prop)]; + + } else { + let value = document.cookie + .split("; ") + .find((row) => row.startsWith(prop + "=")) + ?.split("=")[1]; + if(value) { + return decodeURIComponent(value); + } + } + } else if (prop === Symbol.iterator) { + return getCookiesAsArray()[prop]; + } + }, + set(target, prop, value) { + var finalValue = null; + if ('string' === typeof value) { + finalValue = encodeURIComponent(value) + finalValue += ";samesite=lax" + } else { + finalValue = encodeURIComponent(value.value); + if (value.expires) { + finalValue+=";expires=" + value.maxAge; + } + if (value.maxAge) { + finalValue+=";max-age=" + value.maxAge; + } + if (value.partitioned) { + finalValue+=";partitioned=" + value.partitioned; + } + if (value.path) { + finalValue+=";path=" + value.path; + } + if (value.samesite) { + finalValue+=";samesite=" + value.path; + } + if (value.secure) { + finalValue+=";secure=" + value.path; + } + } + document.cookie= prop + "=" + finalValue; + return true; + } + }) + + class Context { + /** + * @param {*} owner + * @param {*} feature + * @param {*} hyperscriptTarget + * @param {*} event + */ + constructor(owner, feature, hyperscriptTarget, event, runtime) { + this.meta = { + parser: runtime.parser, + lexer: runtime.lexer, + runtime, + owner: owner, + feature: feature, + iterators: {}, + ctx: this + } + this.locals = { + cookies:CookieJar + }; + this.me = hyperscriptTarget, + this.you = undefined + this.result = undefined + this.event = event; + this.target = event ? event.target : null; + this.detail = event ? event.detail : null; + this.sender = event ? event.detail ? event.detail.sender : null : null; + this.body = "document" in globalScope ? document.body : null; + runtime.addFeatures(owner, this); + } + } + + class ElementCollection { + constructor(css, relativeToElement, escape) { + this._css = css; + this.relativeToElement = relativeToElement; + this.escape = escape; + this[shouldAutoIterateSymbol] = true; + } + + get css() { + if (this.escape) { + return Runtime.prototype.escapeSelector(this._css); + } else { + return this._css; + } + } + + get className() { + return this._css.substr(1); + } + + get id() { + return this.className(); + } + + contains(elt) { + for (let element of this) { + if (element.contains(elt)) { + return true; + } + } + return false; + } + + get length() { + return this.selectMatches().length; + } + + [Symbol.iterator]() { + let query = this.selectMatches(); + return query [Symbol.iterator](); + } + + selectMatches() { + let query = Runtime.prototype.getRootNode(this.relativeToElement).querySelectorAll(this.css); + return query; + } + } + + /** + * @type {symbol} + */ + const shouldAutoIterateSymbol = Symbol() + + function getOrInitObject(root, prop) { + var value = root[prop]; + if (value) { + return value; + } else { + var newObj = {}; + root[prop] = newObj; + return newObj; + } + } + + /** + * parseJSON parses a JSON string into a corresponding value. If the + * value passed in is not valid JSON, then it logs an error and returns `null`. + * + * @param {string} jString + * @returns any + */ + function parseJSON(jString) { + try { + return JSON.parse(jString); + } catch (error) { + logError(error); + return null; + } + } + + /** + * logError writes an error message to the Javascript console. It can take any + * value, but msg should commonly be a simple string. + * @param {*} msg + */ + function logError(msg) { + if (console.error) { + console.error(msg); + } else if (console.log) { + console.log("ERROR: ", msg); + } + } + + // TODO: JSDoc description of what's happening here + function varargConstructor(Cls, args) { + return new (Cls.bind.apply(Cls, [Cls].concat(args)))(); + } + + // Grammar + + /** + * @param {Parser} parser + */ + function hyperscriptCoreGrammar(parser) { + parser.addLeafExpression("parenthesized", function (parser, _runtime, tokens) { + if (tokens.matchOpToken("(")) { + var follows = tokens.clearFollows(); + try { + var expr = parser.requireElement("expression", tokens); + } finally { + tokens.restoreFollows(follows); + } + tokens.requireOpToken(")"); + return expr; + } + }); + + parser.addLeafExpression("string", function (parser, runtime, tokens) { + var stringToken = tokens.matchTokenType("STRING"); + if (!stringToken) return; + var rawValue = /** @type {string} */ (stringToken.value); + /** @type {any[]} */ + var args; + if (stringToken.template) { + var innerTokens = Lexer.tokenize(rawValue, true); + args = parser.parseStringTemplate(innerTokens); + } else { + args = []; + } + return { + type: "string", + token: stringToken, + args: args, + op: function (context) { + var returnStr = ""; + for (var i = 1; i < arguments.length; i++) { + var val = arguments[i]; + if (val !== undefined) { + returnStr += val; + } + } + return returnStr; + }, + evaluate: function (context) { + if (args.length === 0) { + return rawValue; + } else { + return runtime.unifiedEval(this, context); + } + }, + }; + }); + + parser.addGrammarElement("nakedString", function (parser, runtime, tokens) { + if (tokens.hasMore()) { + var tokenArr = tokens.consumeUntilWhitespace(); + tokens.matchTokenType("WHITESPACE"); + return { + type: "nakedString", + tokens: tokenArr, + evaluate: function (context) { + return tokenArr + .map(function (t) { + return t.value; + }) + .join(""); + }, + }; + } + }); + + parser.addLeafExpression("number", function (parser, runtime, tokens) { + var number = tokens.matchTokenType("NUMBER"); + if (!number) return; + var numberToken = number; + var value = parseFloat(/** @type {string} */ (number.value)); + return { + type: "number", + value: value, + numberToken: numberToken, + evaluate: function () { + return value; + }, + }; + }); + + parser.addLeafExpression("idRef", function (parser, runtime, tokens) { + var elementId = tokens.matchTokenType("ID_REF"); + if (!elementId) return; + if (!elementId.value) return; + // TODO - unify these two expression types + if (elementId.template) { + var templateValue = elementId.value.substring(2); + var innerTokens = Lexer.tokenize(templateValue); + var innerExpression = parser.requireElement("expression", innerTokens); + return { + type: "idRefTemplate", + args: [innerExpression], + op: function (context, arg) { + return runtime.getRootNode(context.me).getElementById(arg); + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + } else { + const value = elementId.value.substring(1); + return { + type: "idRef", + css: elementId.value, + value: value, + evaluate: function (context) { + return ( + runtime.getRootNode(context.me).getElementById(value) + ); + }, + }; + } + }); + + parser.addLeafExpression("classRef", function (parser, runtime, tokens) { + var classRef = tokens.matchTokenType("CLASS_REF"); + + if (!classRef) return; + if (!classRef.value) return; + + // TODO - unify these two expression types + if (classRef.template) { + var templateValue = classRef.value.substring(2); + var innerTokens = Lexer.tokenize(templateValue); + var innerExpression = parser.requireElement("expression", innerTokens); + return { + type: "classRefTemplate", + args: [innerExpression], + op: function (context, arg) { + return new ElementCollection("." + arg, context.me, true) + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + } else { + const css = classRef.value; + return { + type: "classRef", + css: css, + evaluate: function (context) { + return new ElementCollection(css, context.me, true) + }, + }; + } + }); + + class TemplatedQueryElementCollection extends ElementCollection { + constructor(css, relativeToElement, templateParts) { + super(css, relativeToElement); + this.templateParts = templateParts; + this.elements = templateParts.filter(elt => elt instanceof Element); + } + + get css() { + let rv = "", i = 0 + for (const val of this.templateParts) { + if (val instanceof Element) { + rv += "[data-hs-query-id='" + i++ + "']"; + } else rv += val; + } + return rv; + } + + [Symbol.iterator]() { + this.elements.forEach((el, i) => el.dataset.hsQueryId = i); + const rv = super[Symbol.iterator](); + this.elements.forEach(el => el.removeAttribute('data-hs-query-id')); + return rv; + } + } + + parser.addLeafExpression("queryRef", function (parser, runtime, tokens) { + var queryStart = tokens.matchOpToken("<"); + if (!queryStart) return; + var queryTokens = tokens.consumeUntil("/"); + tokens.requireOpToken("/"); + tokens.requireOpToken(">"); + var queryValue = queryTokens + .map(function (t) { + if (t.type === "STRING") { + return '"' + t.value + '"'; + } else { + return t.value; + } + }) + .join(""); + + var template, innerTokens, args; + if (queryValue.indexOf("$") >= 0) { + template = true; + innerTokens = Lexer.tokenize(queryValue, true); + args = parser.parseStringTemplate(innerTokens); + } + + return { + type: "queryRef", + css: queryValue, + args: args, + op: function (context, ...args) { + if (template) { + return new TemplatedQueryElementCollection(queryValue, context.me, args) + } else { + return new ElementCollection(queryValue, context.me) + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + }); + + parser.addLeafExpression("attributeRef", function (parser, runtime, tokens) { + var attributeRef = tokens.matchTokenType("ATTRIBUTE_REF"); + if (!attributeRef) return; + if (!attributeRef.value) return; + var outerVal = attributeRef.value; + if (outerVal.indexOf("[") === 0) { + var innerValue = outerVal.substring(2, outerVal.length - 1); + } else { + var innerValue = outerVal.substring(1); + } + var css = "[" + innerValue + "]"; + var split = innerValue.split("="); + var name = split[0]; + var value = split[1]; + if (value) { + // strip quotes + if (value.indexOf('"') === 0) { + value = value.substring(1, value.length - 1); + } + } + return { + type: "attributeRef", + name: name, + css: css, + value: value, + op: function (context) { + var target = context.you || context.me; + if (target) { + return target.getAttribute(name); + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + }); + + parser.addLeafExpression("styleRef", function (parser, runtime, tokens) { + var styleRef = tokens.matchTokenType("STYLE_REF"); + if (!styleRef) return; + if (!styleRef.value) return; + var styleProp = styleRef.value.substr(1); + if (styleProp.startsWith("computed-")) { + styleProp = styleProp.substr("computed-".length); + return { + type: "computedStyleRef", + name: styleProp, + op: function (context) { + var target = context.you || context.me; + if (target) { + return runtime.resolveComputedStyle(target, styleProp); + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + } else { + return { + type: "styleRef", + name: styleProp, + op: function (context) { + var target = context.you || context.me; + if (target) { + return runtime.resolveStyle(target, styleProp); + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + } + }); + + parser.addGrammarElement("objectKey", function (parser, runtime, tokens) { + var token; + if ((token = tokens.matchTokenType("STRING"))) { + return { + type: "objectKey", + key: token.value, + evaluate: function () { + return token.value; + }, + }; + } else if (tokens.matchOpToken("[")) { + var expr = parser.parseElement("expression", tokens); + tokens.requireOpToken("]"); + return { + type: "objectKey", + expr: expr, + args: [expr], + op: function (ctx, expr) { + return expr; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + } else { + var key = ""; + do { + token = tokens.matchTokenType("IDENTIFIER") || tokens.matchOpToken("-"); + if (token) key += token.value; + } while (token); + return { + type: "objectKey", + key: key, + evaluate: function () { + return key; + }, + }; + } + }); + + parser.addLeafExpression("objectLiteral", function (parser, runtime, tokens) { + if (!tokens.matchOpToken("{")) return; + var keyExpressions = []; + var valueExpressions = []; + if (!tokens.matchOpToken("}")) { + do { + var name = parser.requireElement("objectKey", tokens); + tokens.requireOpToken(":"); + var value = parser.requireElement("expression", tokens); + valueExpressions.push(value); + keyExpressions.push(name); + } while (tokens.matchOpToken(",")); + tokens.requireOpToken("}"); + } + return { + type: "objectLiteral", + args: [keyExpressions, valueExpressions], + op: function (context, keys, values) { + var returnVal = {}; + for (var i = 0; i < keys.length; i++) { + returnVal[keys[i]] = values[i]; + } + return returnVal; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + }); + + parser.addGrammarElement("nakedNamedArgumentList", function (parser, runtime, tokens) { + var fields = []; + var valueExpressions = []; + if (tokens.currentToken().type === "IDENTIFIER") { + do { + var name = tokens.requireTokenType("IDENTIFIER"); + tokens.requireOpToken(":"); + var value = parser.requireElement("expression", tokens); + valueExpressions.push(value); + fields.push({ name: name, value: value }); + } while (tokens.matchOpToken(",")); + } + return { + type: "namedArgumentList", + fields: fields, + args: [valueExpressions], + op: function (context, values) { + var returnVal = { _namedArgList_: true }; + for (var i = 0; i < values.length; i++) { + var field = fields[i]; + returnVal[field.name.value] = values[i]; + } + return returnVal; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + }); + + parser.addGrammarElement("namedArgumentList", function (parser, runtime, tokens) { + if (!tokens.matchOpToken("(")) return; + var elt = parser.requireElement("nakedNamedArgumentList", tokens); + tokens.requireOpToken(")"); + return elt; + }); + + parser.addGrammarElement("symbol", function (parser, runtime, tokens) { + /** @scope {SymbolScope} */ + var scope = "default"; + if (tokens.matchToken("global")) { + scope = "global"; + } else if (tokens.matchToken("element") || tokens.matchToken("module")) { + scope = "element"; + // optional possessive + if (tokens.matchOpToken("'")) { + tokens.requireToken("s"); + } + } else if (tokens.matchToken("local")) { + scope = "local"; + } + + // TODO better look ahead here + let eltPrefix = tokens.matchOpToken(":"); + let identifier = tokens.matchTokenType("IDENTIFIER"); + if (identifier && identifier.value) { + var name = identifier.value; + if (eltPrefix) { + name = ":" + name; + } + if (scope === "default") { + if (name.indexOf("$") === 0) { + scope = "global"; + } + if (name.indexOf(":") === 0) { + scope = "element"; + } + } + return { + type: "symbol", + token: identifier, + scope: scope, + name: name, + evaluate: function (context) { + return runtime.resolveSymbol(name, context, scope); + }, + }; + } + }); + + parser.addGrammarElement("implicitMeTarget", function (parser, runtime, tokens) { + return { + type: "implicitMeTarget", + evaluate: function (context) { + return context.you || context.me; + }, + }; + }); + + parser.addLeafExpression("boolean", function (parser, runtime, tokens) { + var booleanLiteral = tokens.matchToken("true") || tokens.matchToken("false"); + if (!booleanLiteral) return; + const value = booleanLiteral.value === "true"; + return { + type: "boolean", + evaluate: function (context) { + return value; + }, + }; + }); + + parser.addLeafExpression("null", function (parser, runtime, tokens) { + if (tokens.matchToken("null")) { + return { + type: "null", + evaluate: function (context) { + return null; + }, + }; + } + }); + + parser.addLeafExpression("arrayLiteral", function (parser, runtime, tokens) { + if (!tokens.matchOpToken("[")) return; + var values = []; + if (!tokens.matchOpToken("]")) { + do { + var expr = parser.requireElement("expression", tokens); + values.push(expr); + } while (tokens.matchOpToken(",")); + tokens.requireOpToken("]"); + } + return { + type: "arrayLiteral", + values: values, + args: [values], + op: function (context, values) { + return values; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + }); + + parser.addLeafExpression("blockLiteral", function (parser, runtime, tokens) { + if (!tokens.matchOpToken("\\")) return; + var args = []; + var arg1 = tokens.matchTokenType("IDENTIFIER"); + if (arg1) { + args.push(arg1); + while (tokens.matchOpToken(",")) { + args.push(tokens.requireTokenType("IDENTIFIER")); + } + } + // TODO compound op token + tokens.requireOpToken("-"); + tokens.requireOpToken(">"); + var expr = parser.requireElement("expression", tokens); + return { + type: "blockLiteral", + args: args, + expr: expr, + evaluate: function (ctx) { + var returnFunc = function () { + //TODO - push scope + for (var i = 0; i < args.length; i++) { + ctx.locals[args[i].value] = arguments[i]; + } + return expr.evaluate(ctx); //OK + }; + return returnFunc; + }, + }; + }); + + parser.addIndirectExpression("propertyAccess", function (parser, runtime, tokens, root) { + if (!tokens.matchOpToken(".")) return; + var prop = tokens.requireTokenType("IDENTIFIER"); + var propertyAccess = { + type: "propertyAccess", + root: root, + prop: prop, + args: [root], + op: function (_context, rootVal) { + var value = runtime.resolveProperty(rootVal, prop.value); + return value; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + return parser.parseElement("indirectExpression", tokens, propertyAccess); + }); + + parser.addIndirectExpression("of", function (parser, runtime, tokens, root) { + if (!tokens.matchToken("of")) return; + var newRoot = parser.requireElement("unaryExpression", tokens); + // find the urroot + var childOfUrRoot = null; + var urRoot = root; + while (urRoot.root) { + childOfUrRoot = urRoot; + urRoot = urRoot.root; + } + if (urRoot.type !== "symbol" && urRoot.type !== "attributeRef" && urRoot.type !== "styleRef" && urRoot.type !== "computedStyleRef") { + parser.raiseParseError(tokens, "Cannot take a property of a non-symbol: " + urRoot.type); + } + var attribute = urRoot.type === "attributeRef"; + var style = urRoot.type === "styleRef" || urRoot.type === "computedStyleRef"; + if (attribute || style) { + var attributeElt = urRoot + } + var prop = urRoot.name; + + var propertyAccess = { + type: "ofExpression", + prop: urRoot.token, + root: newRoot, + attribute: attributeElt, + expression: root, + args: [newRoot], + op: function (context, rootVal) { + if (attribute) { + return runtime.resolveAttribute(rootVal, prop); + } else if (style) { + if (urRoot.type === "computedStyleRef") { + return runtime.resolveComputedStyle(rootVal, prop); + } else { + return runtime.resolveStyle(rootVal, prop); + } + } else { + return runtime.resolveProperty(rootVal, prop); + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + + if (urRoot.type === "attributeRef") { + propertyAccess.attribute = urRoot; + } + if (childOfUrRoot) { + childOfUrRoot.root = propertyAccess; + childOfUrRoot.args = [propertyAccess]; + } else { + root = propertyAccess; + } + + return parser.parseElement("indirectExpression", tokens, root); + }); + + parser.addIndirectExpression("possessive", function (parser, runtime, tokens, root) { + if (parser.possessivesDisabled) { + return; + } + var apostrophe = tokens.matchOpToken("'"); + if ( + apostrophe || + (root.type === "symbol" && + (root.name === "my" || root.name === "its" || root.name === "your") && + (tokens.currentToken().type === "IDENTIFIER" || tokens.currentToken().type === "ATTRIBUTE_REF" || tokens.currentToken().type === "STYLE_REF")) + ) { + if (apostrophe) { + tokens.requireToken("s"); + } + + var attribute, style, prop; + attribute = parser.parseElement("attributeRef", tokens); + if (attribute == null) { + style = parser.parseElement("styleRef", tokens); + if (style == null) { + prop = tokens.requireTokenType("IDENTIFIER"); + } + } + var propertyAccess = { + type: "possessive", + root: root, + attribute: attribute || style, + prop: prop, + args: [root], + op: function (context, rootVal) { + if (attribute) { + // @ts-ignore + var value = runtime.resolveAttribute(rootVal, attribute.name); + } else if (style) { + var value + if (style.type === 'computedStyleRef') { + value = runtime.resolveComputedStyle(rootVal, style['name']); + } else { + value = runtime.resolveStyle(rootVal, style['name']); + } + } else { + var value = runtime.resolveProperty(rootVal, prop.value); + } + return value; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + return parser.parseElement("indirectExpression", tokens, propertyAccess); + } + }); + + parser.addIndirectExpression("inExpression", function (parser, runtime, tokens, root) { + if (!tokens.matchToken("in")) return; + var target = parser.requireElement("unaryExpression", tokens); + var propertyAccess = { + type: "inExpression", + root: root, + args: [root, target], + op: function (context, rootVal, target) { + var returnArr = []; + if (rootVal.css) { + runtime.implicitLoop(target, function (targetElt) { + var results = targetElt.querySelectorAll(rootVal.css); + for (var i = 0; i < results.length; i++) { + returnArr.push(results[i]); + } + }); + } else if (rootVal instanceof Element) { + var within = false; + runtime.implicitLoop(target, function (targetElt) { + if (targetElt.contains(rootVal)) { + within = true; + } + }); + if(within) { + return rootVal; + } + } else { + runtime.implicitLoop(rootVal, function (rootElt) { + runtime.implicitLoop(target, function (targetElt) { + if (rootElt === targetElt) { + returnArr.push(rootElt); + } + }); + }); + } + return returnArr; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + return parser.parseElement("indirectExpression", tokens, propertyAccess); + }); + + parser.addIndirectExpression("asExpression", function (parser, runtime, tokens, root) { + if (!tokens.matchToken("as")) return; + tokens.matchToken("a") || tokens.matchToken("an"); + var conversion = parser.requireElement("dotOrColonPath", tokens).evaluate(); // OK No promise + var propertyAccess = { + type: "asExpression", + root: root, + args: [root], + op: function (context, rootVal) { + return runtime.convertValue(rootVal, conversion); + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + return parser.parseElement("indirectExpression", tokens, propertyAccess); + }); + + parser.addIndirectExpression("functionCall", function (parser, runtime, tokens, root) { + if (!tokens.matchOpToken("(")) return; + var args = []; + if (!tokens.matchOpToken(")")) { + do { + args.push(parser.requireElement("expression", tokens)); + } while (tokens.matchOpToken(",")); + tokens.requireOpToken(")"); + } + + if (root.root) { + var functionCall = { + type: "functionCall", + root: root, + argExressions: args, + args: [root.root, args], + op: function (context, rootRoot, args) { + runtime.nullCheck(rootRoot, root.root); + var func = rootRoot[root.prop.value]; + runtime.nullCheck(func, root); + if (func.hyperfunc) { + args.push(context); + } + return func.apply(rootRoot, args); + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + } else { + var functionCall = { + type: "functionCall", + root: root, + argExressions: args, + args: [root, args], + op: function (context, func, argVals) { + runtime.nullCheck(func, root); + if (func.hyperfunc) { + argVals.push(context); + } + var apply = func.apply(null, argVals); + return apply; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + } + return parser.parseElement("indirectExpression", tokens, functionCall); + }); + + parser.addIndirectExpression("attributeRefAccess", function (parser, runtime, tokens, root) { + var attribute = parser.parseElement("attributeRef", tokens); + if (!attribute) return; + var attributeAccess = { + type: "attributeRefAccess", + root: root, + attribute: attribute, + args: [root], + op: function (_ctx, rootVal) { + // @ts-ignore + var value = runtime.resolveAttribute(rootVal, attribute.name); + return value; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + return attributeAccess; + }); + + parser.addIndirectExpression("arrayIndex", function (parser, runtime, tokens, root) { + if (!tokens.matchOpToken("[")) return; + var andBefore = false; + var andAfter = false; + var firstIndex = null; + var secondIndex = null; + + if (tokens.matchOpToken("..")) { + andBefore = true; + firstIndex = parser.requireElement("expression", tokens); + } else { + firstIndex = parser.requireElement("expression", tokens); + + if (tokens.matchOpToken("..")) { + andAfter = true; + var current = tokens.currentToken(); + if (current.type !== "R_BRACKET") { + secondIndex = parser.parseElement("expression", tokens); + } + } + } + tokens.requireOpToken("]"); + + var arrayIndex = { + type: "arrayIndex", + root: root, + prop: firstIndex, + firstIndex: firstIndex, + secondIndex: secondIndex, + args: [root, firstIndex, secondIndex], + op: function (_ctx, root, firstIndex, secondIndex) { + if (root == null) { + return null; + } + if (andBefore) { + if (firstIndex < 0) { + firstIndex = root.length + firstIndex; + } + return root.slice(0, firstIndex + 1); // returns all items from beginning to firstIndex (inclusive) + } else if (andAfter) { + if (secondIndex != null) { + if (secondIndex < 0) { + secondIndex = root.length + secondIndex; + } + return root.slice(firstIndex, secondIndex + 1); // returns all items from firstIndex to secondIndex (inclusive) + } else { + return root.slice(firstIndex); // returns from firstIndex to end of array + } + } else { + return root[firstIndex]; + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + + return parser.parseElement("indirectExpression", tokens, arrayIndex); + }); + + // taken from https://drafts.csswg.org/css-values-4/#relative-length + // and https://drafts.csswg.org/css-values-4/#absolute-length + // (NB: we do not support `in` dues to conflicts w/ the hyperscript grammar) + var STRING_POSTFIXES = [ + 'em', 'ex', 'cap', 'ch', 'ic', 'rem', 'lh', 'rlh', 'vw', 'vh', 'vi', 'vb', 'vmin', 'vmax', + 'cm', 'mm', 'Q', 'pc', 'pt', 'px' + ]; + parser.addGrammarElement("postfixExpression", function (parser, runtime, tokens) { + var root = parser.parseElement("primaryExpression", tokens); + + let stringPosfix = tokens.matchAnyToken.apply(tokens, STRING_POSTFIXES) || tokens.matchOpToken("%"); + if (stringPosfix) { + return { + type: "stringPostfix", + postfix: stringPosfix.value, + args: [root], + op: function (context, val) { + return "" + val + stringPosfix.value; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + } + + var timeFactor = null; + if (tokens.matchToken("s") || tokens.matchToken("seconds")) { + timeFactor = 1000; + } else if (tokens.matchToken("ms") || tokens.matchToken("milliseconds")) { + timeFactor = 1; + } + if (timeFactor) { + return { + type: "timeExpression", + time: root, + factor: timeFactor, + args: [root], + op: function (_context, val) { + return val * timeFactor; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + } + + if (tokens.matchOpToken(":")) { + var typeName = tokens.requireTokenType("IDENTIFIER"); + if (!typeName.value) return; + var nullOk = !tokens.matchOpToken("!"); + return { + type: "typeCheck", + typeName: typeName, + nullOk: nullOk, + args: [root], + op: function (context, val) { + var passed = runtime.typeCheck(val, this.typeName.value, nullOk); + if (passed) { + return val; + } else { + throw new Error("Typecheck failed! Expected: " + typeName.value); + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + } else { + return root; + } + }); + + parser.addGrammarElement("logicalNot", function (parser, runtime, tokens) { + if (!tokens.matchToken("not")) return; + var root = parser.requireElement("unaryExpression", tokens); + return { + type: "logicalNot", + root: root, + args: [root], + op: function (context, val) { + return !val; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + }); + + parser.addGrammarElement("noExpression", function (parser, runtime, tokens) { + if (!tokens.matchToken("no")) return; + var root = parser.requireElement("unaryExpression", tokens); + return { + type: "noExpression", + root: root, + args: [root], + op: function (_context, val) { + return runtime.isEmpty(val); + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + }); + + parser.addLeafExpression("some", function (parser, runtime, tokens) { + if (!tokens.matchToken("some")) return; + var root = parser.requireElement("expression", tokens); + return { + type: "noExpression", + root: root, + args: [root], + op: function (_context, val) { + return !runtime.isEmpty(val); + }, + evaluate(context) { + return runtime.unifiedEval(this, context); + }, + }; + }); + + parser.addGrammarElement("negativeNumber", function (parser, runtime, tokens) { + if (!tokens.matchOpToken("-")) return; + var root = parser.requireElement("unaryExpression", tokens); + return { + type: "negativeNumber", + root: root, + args: [root], + op: function (context, value) { + return -1 * value; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + }); + + parser.addGrammarElement("unaryExpression", function (parser, runtime, tokens) { + tokens.matchToken("the"); // optional "the" + return parser.parseAnyOf( + ["beepExpression", "logicalNot", "relativePositionalExpression", "positionalExpression", "noExpression", "negativeNumber", "postfixExpression"], + tokens + ); + }); + + parser.addGrammarElement("beepExpression", function (parser, runtime, tokens) { + if (!tokens.matchToken("beep!")) return; + var expression = parser.parseElement("unaryExpression", tokens); + if (expression) { + expression['booped'] = true; + var originalEvaluate = expression.evaluate; + expression.evaluate = function(ctx){ + let value = originalEvaluate.apply(expression, arguments); + let element = ctx.me; + runtime.beepValueToConsole(element, expression, value); + return value; + } + return expression; + } + }); + + var scanForwardQuery = function(start, root, match, wrap) { + var results = root.querySelectorAll(match); + for (var i = 0; i < results.length; i++) { + var elt = results[i]; + if (elt.compareDocumentPosition(start) === Node.DOCUMENT_POSITION_PRECEDING) { + return elt; + } + } + if (wrap) { + return results[0]; + } + } + + var scanBackwardsQuery = function(start, root, match, wrap) { + var results = root.querySelectorAll(match); + for (var i = results.length - 1; i >= 0; i--) { + var elt = results[i]; + if (elt.compareDocumentPosition(start) === Node.DOCUMENT_POSITION_FOLLOWING) { + return elt; + } + } + if (wrap) { + return results[results.length - 1]; + } + } + + var scanForwardArray = function(start, array, match, wrap) { + var matches = []; + Runtime.prototype.forEach(array, function(elt){ + if (elt.matches(match) || elt === start) { + matches.push(elt); + } + }) + for (var i = 0; i < matches.length - 1; i++) { + var elt = matches[i]; + if (elt === start) { + return matches[i + 1]; + } + } + if (wrap) { + var first = matches[0]; + if (first && first.matches(match)) { + return first; + } + } + } + + var scanBackwardsArray = function(start, array, match, wrap) { + return scanForwardArray(start, Array.from(array).reverse(), match, wrap); + } + + parser.addGrammarElement("relativePositionalExpression", function (parser, runtime, tokens) { + var op = tokens.matchAnyToken("next", "previous"); + if (!op) return; + var forwardSearch = op.value === "next"; + + var thingElt = parser.parseElement("expression", tokens); + + if (tokens.matchToken("from")) { + tokens.pushFollow("in"); + try { + var from = parser.requireElement("unaryExpression", tokens); + } finally { + tokens.popFollow(); + } + } else { + var from = parser.requireElement("implicitMeTarget", tokens); + } + + var inSearch = false; + var withinElt; + if (tokens.matchToken("in")) { + inSearch = true; + var inElt = parser.requireElement("unaryExpression", tokens); + } else if (tokens.matchToken("within")) { + withinElt = parser.requireElement("unaryExpression", tokens); + } else { + withinElt = document.body; + } + + var wrapping = false; + if (tokens.matchToken("with")) { + tokens.requireToken("wrapping") + wrapping = true; + } + + return { + type: "relativePositionalExpression", + from: from, + forwardSearch: forwardSearch, + inSearch: inSearch, + wrapping: wrapping, + inElt: inElt, + withinElt: withinElt, + operator: op.value, + args: [thingElt, from, inElt, withinElt], + op: function (context, thing, from, inElt, withinElt) { + + var css = thing.css; + if (css == null) { + throw "Expected a CSS value to be returned by " + Tokens.sourceFor.apply(thingElt); + } + + if(inSearch) { + if (inElt) { + if (forwardSearch) { + return scanForwardArray(from, inElt, css, wrapping); + } else { + return scanBackwardsArray(from, inElt, css, wrapping); + } + } + } else { + if (withinElt) { + if (forwardSearch) { + return scanForwardQuery(from, withinElt, css, wrapping); + } else { + return scanBackwardsQuery(from, withinElt, css, wrapping); + } + } + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + } + + }); + + parser.addGrammarElement("positionalExpression", function (parser, runtime, tokens) { + var op = tokens.matchAnyToken("first", "last", "random"); + if (!op) return; + tokens.matchAnyToken("in", "from", "of"); + var rhs = parser.requireElement("unaryExpression", tokens); + const operator = op.value; + return { + type: "positionalExpression", + rhs: rhs, + operator: op.value, + args: [rhs], + op: function (context, rhsVal) { + if (rhsVal && !Array.isArray(rhsVal)) { + if (rhsVal.children) { + rhsVal = rhsVal.children; + } else { + rhsVal = Array.from(rhsVal); + } + } + if (rhsVal) { + if (operator === "first") { + return rhsVal[0]; + } else if (operator === "last") { + return rhsVal[rhsVal.length - 1]; + } else if (operator === "random") { + return rhsVal[Math.floor(Math.random() * rhsVal.length)]; + } + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + }); + + parser.addGrammarElement("mathOperator", function (parser, runtime, tokens) { + var expr = parser.parseElement("unaryExpression", tokens); + var mathOp, + initialMathOp = null; + mathOp = tokens.matchAnyOpToken("+", "-", "*", "/") || tokens.matchToken('mod'); + while (mathOp) { + initialMathOp = initialMathOp || mathOp; + var operator = mathOp.value; + if (initialMathOp.value !== operator) { + parser.raiseParseError(tokens, "You must parenthesize math operations with different operators"); + } + var rhs = parser.parseElement("unaryExpression", tokens); + expr = { + type: "mathOperator", + lhs: expr, + rhs: rhs, + operator: operator, + args: [expr, rhs], + op: function (context, lhsVal, rhsVal) { + if (operator === "+") { + return lhsVal + rhsVal; + } else if (operator === "-") { + return lhsVal - rhsVal; + } else if (operator === "*") { + return lhsVal * rhsVal; + } else if (operator === "/") { + return lhsVal / rhsVal; + } else if (operator === "mod") { + return lhsVal % rhsVal; + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + mathOp = tokens.matchAnyOpToken("+", "-", "*", "/") || tokens.matchToken('mod'); + } + return expr; + }); + + parser.addGrammarElement("mathExpression", function (parser, runtime, tokens) { + return parser.parseAnyOf(["mathOperator", "unaryExpression"], tokens); + }); + + function sloppyContains(src, container, value){ + if (container['contains']) { + return container.contains(value); + } else if (container['includes']) { + return container.includes(value); + } else { + throw Error("The value of " + src.sourceFor() + " does not have a contains or includes method on it"); + } + } + function sloppyMatches(src, target, toMatch){ + if (target['match']) { + return !!target.match(toMatch); + } else if (target['matches']) { + return target.matches(toMatch); + } else { + throw Error("The value of " + src.sourceFor() + " does not have a match or matches method on it"); + } + } + + parser.addGrammarElement("comparisonOperator", function (parser, runtime, tokens) { + var expr = parser.parseElement("mathExpression", tokens); + var comparisonToken = tokens.matchAnyOpToken("<", ">", "<=", ">=", "==", "===", "!=", "!=="); + var operator = comparisonToken ? comparisonToken.value : null; + var hasRightValue = true; // By default, most comparisons require two values, but there are some exceptions. + var typeCheck = false; + + if (operator == null) { + if (tokens.matchToken("is") || tokens.matchToken("am")) { + if (tokens.matchToken("not")) { + if (tokens.matchToken("in")) { + operator = "not in"; + } else if (tokens.matchToken("a")) { + operator = "not a"; + typeCheck = true; + } else if (tokens.matchToken("empty")) { + operator = "not empty"; + hasRightValue = false; + } else { + if (tokens.matchToken("really")) { + operator = "!=="; + } else { + operator = "!="; + } + // consume additional optional syntax + if (tokens.matchToken("equal")) { + tokens.matchToken("to"); + } + } + } else if (tokens.matchToken("in")) { + operator = "in"; + } else if (tokens.matchToken("a")) { + operator = "a"; + typeCheck = true; + } else if (tokens.matchToken("empty")) { + operator = "empty"; + hasRightValue = false; + } else if (tokens.matchToken("less")) { + tokens.requireToken("than"); + if (tokens.matchToken("or")) { + tokens.requireToken("equal"); + tokens.requireToken("to"); + operator = "<="; + } else { + operator = "<"; + } + } else if (tokens.matchToken("greater")) { + tokens.requireToken("than"); + if (tokens.matchToken("or")) { + tokens.requireToken("equal"); + tokens.requireToken("to"); + operator = ">="; + } else { + operator = ">"; + } + } else { + if (tokens.matchToken("really")) { + operator = "==="; + } else { + operator = "=="; + } + if (tokens.matchToken("equal")) { + tokens.matchToken("to"); + } + } + } else if (tokens.matchToken("equals")) { + operator = "=="; + } else if (tokens.matchToken("really")) { + tokens.requireToken("equals") + operator = "==="; + } else if (tokens.matchToken("exist") || tokens.matchToken("exists")) { + operator = "exist"; + hasRightValue = false; + } else if (tokens.matchToken("matches") || tokens.matchToken("match")) { + operator = "match"; + } else if (tokens.matchToken("contains") || tokens.matchToken("contain")) { + operator = "contain"; + } else if (tokens.matchToken("includes") || tokens.matchToken("include")) { + operator = "include"; + } else if (tokens.matchToken("do") || tokens.matchToken("does")) { + tokens.requireToken("not"); + if (tokens.matchToken("matches") || tokens.matchToken("match")) { + operator = "not match"; + } else if (tokens.matchToken("contains") || tokens.matchToken("contain")) { + operator = "not contain"; + } else if (tokens.matchToken("exist") || tokens.matchToken("exist")) { + operator = "not exist"; + hasRightValue = false; + } else if (tokens.matchToken("include")) { + operator = "not include"; + } else { + parser.raiseParseError(tokens, "Expected matches or contains"); + } + } + } + + if (operator) { + // Do not allow chained comparisons, which is dumb + var typeName, nullOk, rhs + if (typeCheck) { + typeName = tokens.requireTokenType("IDENTIFIER"); + nullOk = !tokens.matchOpToken("!"); + } else if (hasRightValue) { + rhs = parser.requireElement("mathExpression", tokens); + if (operator === "match" || operator === "not match") { + rhs = rhs.css ? rhs.css : rhs; + } + } + var lhs = expr; + expr = { + type: "comparisonOperator", + operator: operator, + typeName: typeName, + nullOk: nullOk, + lhs: expr, + rhs: rhs, + args: [expr, rhs], + op: function (context, lhsVal, rhsVal) { + if (operator === "==") { + return lhsVal == rhsVal; + } else if (operator === "!=") { + return lhsVal != rhsVal; + } + if (operator === "===") { + return lhsVal === rhsVal; + } else if (operator === "!==") { + return lhsVal !== rhsVal; + } + if (operator === "match") { + return lhsVal != null && sloppyMatches(lhs, lhsVal, rhsVal); + } + if (operator === "not match") { + return lhsVal == null || !sloppyMatches(lhs, lhsVal, rhsVal); + } + if (operator === "in") { + return rhsVal != null && sloppyContains(rhs, rhsVal, lhsVal); + } + if (operator === "not in") { + return rhsVal == null || !sloppyContains(rhs, rhsVal, lhsVal); + } + if (operator === "contain") { + return lhsVal != null && sloppyContains(lhs, lhsVal, rhsVal); + } + if (operator === "not contain") { + return lhsVal == null || !sloppyContains(lhs, lhsVal, rhsVal); + } + if (operator === "include") { + return lhsVal != null && sloppyContains(lhs, lhsVal, rhsVal); + } + if (operator === "not include") { + return lhsVal == null || !sloppyContains(lhs, lhsVal, rhsVal); + } + if (operator === "===") { + return lhsVal === rhsVal; + } else if (operator === "!==") { + return lhsVal !== rhsVal; + } else if (operator === "<") { + return lhsVal < rhsVal; + } else if (operator === ">") { + return lhsVal > rhsVal; + } else if (operator === "<=") { + return lhsVal <= rhsVal; + } else if (operator === ">=") { + return lhsVal >= rhsVal; + } else if (operator === "empty") { + return runtime.isEmpty(lhsVal); + } else if (operator === "not empty") { + return !runtime.isEmpty(lhsVal); + } else if (operator === "exist") { + return runtime.doesExist(lhsVal); + } else if (operator === "not exist") { + return !runtime.doesExist(lhsVal); + } else if (operator === "a") { + return runtime.typeCheck(lhsVal, typeName.value, nullOk); + } else if (operator === "not a") { + return !runtime.typeCheck(lhsVal, typeName.value, nullOk); + } else { + throw "Unknown comparison : " + operator; + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + } + return expr; + }); + + parser.addGrammarElement("comparisonExpression", function (parser, runtime, tokens) { + return parser.parseAnyOf(["comparisonOperator", "mathExpression"], tokens); + }); + + parser.addGrammarElement("logicalOperator", function (parser, runtime, tokens) { + var expr = parser.parseElement("comparisonExpression", tokens); + var logicalOp, + initialLogicalOp = null; + logicalOp = tokens.matchToken("and") || tokens.matchToken("or"); + while (logicalOp) { + initialLogicalOp = initialLogicalOp || logicalOp; + if (initialLogicalOp.value !== logicalOp.value) { + parser.raiseParseError(tokens, "You must parenthesize logical operations with different operators"); + } + var rhs = parser.requireElement("comparisonExpression", tokens); + const operator = logicalOp.value; + expr = { + type: "logicalOperator", + operator: operator, + lhs: expr, + rhs: rhs, + args: [expr, rhs], + op: function (context, lhsVal, rhsVal) { + if (operator === "and") { + return lhsVal && rhsVal; + } else { + return lhsVal || rhsVal; + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + logicalOp = tokens.matchToken("and") || tokens.matchToken("or"); + } + return expr; + }); + + parser.addGrammarElement("logicalExpression", function (parser, runtime, tokens) { + return parser.parseAnyOf(["logicalOperator", "mathExpression"], tokens); + }); + + parser.addGrammarElement("asyncExpression", function (parser, runtime, tokens) { + if (tokens.matchToken("async")) { + var value = parser.requireElement("logicalExpression", tokens); + var expr = { + type: "asyncExpression", + value: value, + evaluate: function (context) { + return { + asyncWrapper: true, + value: this.value.evaluate(context), //OK + }; + }, + }; + return expr; + } else { + return parser.parseElement("logicalExpression", tokens); + } + }); + + parser.addGrammarElement("expression", function (parser, runtime, tokens) { + tokens.matchToken("the"); // optional the + return parser.parseElement("asyncExpression", tokens); + }); + + parser.addGrammarElement("assignableExpression", function (parser, runtime, tokens) { + tokens.matchToken("the"); // optional the + + // TODO obviously we need to generalize this as a left hand side / targetable concept + var expr = parser.parseElement("primaryExpression", tokens); + if (expr && ( + expr.type === "symbol" || + expr.type === "ofExpression" || + expr.type === "propertyAccess" || + expr.type === "attributeRefAccess" || + expr.type === "attributeRef" || + expr.type === "styleRef" || + expr.type === "arrayIndex" || + expr.type === "possessive") + ) { + return expr; + } else { + parser.raiseParseError( + tokens, + "A target expression must be writable. The expression type '" + (expr && expr.type) + "' is not." + ); + } + return expr; + }); + + parser.addGrammarElement("hyperscript", function (parser, runtime, tokens) { + var features = []; + + if (tokens.hasMore()) { + while (parser.featureStart(tokens.currentToken()) || tokens.currentToken().value === "(") { + var feature = parser.requireElement("feature", tokens); + features.push(feature); + tokens.matchToken("end"); // optional end + } + } + return { + type: "hyperscript", + features: features, + apply: function (target, source, args) { + // no op + for (const feature of features) { + feature.install(target, source, args); + } + }, + }; + }); + + var parseEventArgs = function (tokens) { + var args = []; + // handle argument list (look ahead 3) + if ( + tokens.token(0).value === "(" && + (tokens.token(1).value === ")" || tokens.token(2).value === "," || tokens.token(2).value === ")") + ) { + tokens.matchOpToken("("); + do { + args.push(tokens.requireTokenType("IDENTIFIER")); + } while (tokens.matchOpToken(",")); + tokens.requireOpToken(")"); + } + return args; + }; + + parser.addFeature("on", function (parser, runtime, tokens) { + if (!tokens.matchToken("on")) return; + var every = false; + if (tokens.matchToken("every")) { + every = true; + } + var events = []; + var displayName = null; + do { + var on = parser.requireElement("eventName", tokens, "Expected event name"); + + var eventName = on.evaluate(); // OK No Promise + + if (displayName) { + displayName = displayName + " or " + eventName; + } else { + displayName = "on " + eventName; + } + var args = parseEventArgs(tokens); + + var filter = null; + if (tokens.matchOpToken("[")) { + filter = parser.requireElement("expression", tokens); + tokens.requireOpToken("]"); + } + + var startCount, endCount ,unbounded; + if (tokens.currentToken().type === "NUMBER") { + var startCountToken = tokens.consumeToken(); + if (!startCountToken.value) return; + startCount = parseInt(startCountToken.value); + if (tokens.matchToken("to")) { + var endCountToken = tokens.consumeToken(); + if (!endCountToken.value) return; + endCount = parseInt(endCountToken.value); + } else if (tokens.matchToken("and")) { + unbounded = true; + tokens.requireToken("on"); + } + } + + var intersectionSpec, mutationSpec; + if (eventName === "intersection") { + intersectionSpec = {}; + if (tokens.matchToken("with")) { + intersectionSpec["with"] = parser.requireElement("expression", tokens).evaluate(); + } + if (tokens.matchToken("having")) { + do { + if (tokens.matchToken("margin")) { + intersectionSpec["rootMargin"] = parser.requireElement("stringLike", tokens).evaluate(); + } else if (tokens.matchToken("threshold")) { + intersectionSpec["threshold"] = parser.requireElement("expression", tokens).evaluate(); + } else { + parser.raiseParseError(tokens, "Unknown intersection config specification"); + } + } while (tokens.matchToken("and")); + } + } else if (eventName === "mutation") { + mutationSpec = {}; + if (tokens.matchToken("of")) { + do { + if (tokens.matchToken("anything")) { + mutationSpec["attributes"] = true; + mutationSpec["subtree"] = true; + mutationSpec["characterData"] = true; + mutationSpec["childList"] = true; + } else if (tokens.matchToken("childList")) { + mutationSpec["childList"] = true; + } else if (tokens.matchToken("attributes")) { + mutationSpec["attributes"] = true; + mutationSpec["attributeOldValue"] = true; + } else if (tokens.matchToken("subtree")) { + mutationSpec["subtree"] = true; + } else if (tokens.matchToken("characterData")) { + mutationSpec["characterData"] = true; + mutationSpec["characterDataOldValue"] = true; + } else if (tokens.currentToken().type === "ATTRIBUTE_REF") { + var attribute = tokens.consumeToken(); + if (mutationSpec["attributeFilter"] == null) { + mutationSpec["attributeFilter"] = []; + } + if (attribute.value.indexOf("@") == 0) { + mutationSpec["attributeFilter"].push(attribute.value.substring(1)); + } else { + parser.raiseParseError( + tokens, + "Only shorthand attribute references are allowed here" + ); + } + } else { + parser.raiseParseError(tokens, "Unknown mutation config specification"); + } + } while (tokens.matchToken("or")); + } else { + mutationSpec["attributes"] = true; + mutationSpec["characterData"] = true; + mutationSpec["childList"] = true; + } + } + + var from = null; + var elsewhere = false; + if (tokens.matchToken("from")) { + if (tokens.matchToken("elsewhere")) { + elsewhere = true; + } else { + tokens.pushFollow("or"); + try { + from = parser.requireElement("expression", tokens) + } finally { + tokens.popFollow(); + } + if (!from) { + parser.raiseParseError(tokens, 'Expected either target value or "elsewhere".'); + } + } + } + // support both "elsewhere" and "from elsewhere" + if (from === null && elsewhere === false && tokens.matchToken("elsewhere")) { + elsewhere = true; + } + + if (tokens.matchToken("in")) { + var inExpr = parser.parseElement('unaryExpression', tokens); + } + + if (tokens.matchToken("debounced")) { + tokens.requireToken("at"); + var timeExpr = parser.requireElement("unaryExpression", tokens); + // @ts-ignore + var debounceTime = timeExpr.evaluate({}); // OK No promise TODO make a literal time expr + } else if (tokens.matchToken("throttled")) { + tokens.requireToken("at"); + var timeExpr = parser.requireElement("unaryExpression", tokens); + // @ts-ignore + var throttleTime = timeExpr.evaluate({}); // OK No promise TODO make a literal time expr + } + + events.push({ + execCount: 0, + every: every, + on: eventName, + args: args, + filter: filter, + from: from, + inExpr: inExpr, + elsewhere: elsewhere, + startCount: startCount, + endCount: endCount, + unbounded: unbounded, + debounceTime: debounceTime, + throttleTime: throttleTime, + mutationSpec: mutationSpec, + intersectionSpec: intersectionSpec, + debounced: undefined, + lastExec: undefined, + }); + } while (tokens.matchToken("or")); + + var queueLast = true; + if (!every) { + if (tokens.matchToken("queue")) { + if (tokens.matchToken("all")) { + var queueAll = true; + var queueLast = false; + } else if (tokens.matchToken("first")) { + var queueFirst = true; + } else if (tokens.matchToken("none")) { + var queueNone = true; + } else { + tokens.requireToken("last"); + } + } + } + + var start = parser.requireElement("commandList", tokens); + parser.ensureTerminated(start); + + var errorSymbol, errorHandler; + if (tokens.matchToken("catch")) { + errorSymbol = tokens.requireTokenType("IDENTIFIER").value; + errorHandler = parser.requireElement("commandList", tokens); + parser.ensureTerminated(errorHandler); + } + + if (tokens.matchToken("finally")) { + var finallyHandler = parser.requireElement("commandList", tokens); + parser.ensureTerminated(finallyHandler); + } + + var onFeature = { + displayName: displayName, + events: events, + start: start, + every: every, + execCount: 0, + errorHandler: errorHandler, + errorSymbol: errorSymbol, + execute: function (/** @type {Context} */ ctx) { + let eventQueueInfo = runtime.getEventQueueFor(ctx.me, onFeature); + if (eventQueueInfo.executing && every === false) { + if (queueNone || (queueFirst && eventQueueInfo.queue.length > 0)) { + return; + } + if (queueLast) { + eventQueueInfo.queue.length = 0; + } + eventQueueInfo.queue.push(ctx); + return; + } + onFeature.execCount++; + eventQueueInfo.executing = true; + ctx.meta.onHalt = function () { + eventQueueInfo.executing = false; + var queued = eventQueueInfo.queue.shift(); + if (queued) { + setTimeout(function () { + onFeature.execute(queued); + }, 1); + } + }; + ctx.meta.reject = function (err) { + console.error(err.message ? err.message : err); + var hypertrace = runtime.getHyperTrace(ctx, err); + if (hypertrace) { + hypertrace.print(); + } + runtime.triggerEvent(ctx.me, "exception", { + error: err, + }); + }; + start.execute(ctx); + }, + install: function (elt, source) { + for (const eventSpec of onFeature.events) { + var targets; + if (eventSpec.elsewhere) { + targets = [document]; + } else if (eventSpec.from) { + targets = eventSpec.from.evaluate(runtime.makeContext(elt, onFeature, elt, null)); + } else { + targets = [elt]; + } + runtime.implicitLoop(targets, function (target) { + // OK NO PROMISE + + var eventName = eventSpec.on; + if (target == null) { + console.warn("'%s' feature ignored because target does not exists:", displayName, elt); + return; + } + + if (eventSpec.mutationSpec) { + eventName = "hyperscript:mutation"; + const observer = new MutationObserver(function (mutationList, observer) { + if (!onFeature.executing) { + runtime.triggerEvent(target, eventName, { + mutationList: mutationList, + observer: observer, + }); + } + }); + observer.observe(target, eventSpec.mutationSpec); + } + + if (eventSpec.intersectionSpec) { + eventName = "hyperscript:intersection"; + const observer = new IntersectionObserver(function (entries) { + for (const entry of entries) { + var detail = { + observer: observer, + }; + detail = Object.assign(detail, entry); + detail["intersecting"] = entry.isIntersecting; + runtime.triggerEvent(target, eventName, detail); + } + }, eventSpec.intersectionSpec); + observer.observe(target); + } + + var addEventListener = target.addEventListener || target.on; + addEventListener.call(target, eventName, function listener(evt) { + // OK NO PROMISE + if (typeof Node !== 'undefined' && elt instanceof Node && target !== elt && !elt.isConnected) { + target.removeEventListener(eventName, listener); + return; + } + + var ctx = runtime.makeContext(elt, onFeature, elt, evt); + if (eventSpec.elsewhere && elt.contains(evt.target)) { + return; + } + if (eventSpec.from) { + ctx.result = target; + } + + // establish context + for (const arg of eventSpec.args) { + let eventValue = ctx.event[arg.value]; + if (eventValue !== undefined) { + ctx.locals[arg.value] = eventValue; + } else if ('detail' in ctx.event) { + ctx.locals[arg.value] = ctx.event['detail'][arg.value]; + } + } + + // install error handler if any + ctx.meta.errorHandler = errorHandler; + ctx.meta.errorSymbol = errorSymbol; + ctx.meta.finallyHandler = finallyHandler; + + // apply filter + if (eventSpec.filter) { + var initialCtx = ctx.meta.context; + ctx.meta.context = ctx.event; + try { + var value = eventSpec.filter.evaluate(ctx); //OK NO PROMISE + if (value) { + // match the javascript semantics for if statements + } else { + return; + } + } finally { + ctx.meta.context = initialCtx; + } + } + + if (eventSpec.inExpr) { + var inElement = evt.target; + while (true) { + if (inElement.matches && inElement.matches(eventSpec.inExpr.css)) { + ctx.result = inElement; + break; + } else { + inElement = inElement.parentElement; + if (inElement == null) { + return; // no match found + } + } + } + } + + // verify counts + eventSpec.execCount++; + if (eventSpec.startCount) { + if (eventSpec.endCount) { + if ( + eventSpec.execCount < eventSpec.startCount || + eventSpec.execCount > eventSpec.endCount + ) { + return; + } + } else if (eventSpec.unbounded) { + if (eventSpec.execCount < eventSpec.startCount) { + return; + } + } else if (eventSpec.execCount !== eventSpec.startCount) { + return; + } + } + + //debounce + if (eventSpec.debounceTime) { + if (eventSpec.debounced) { + clearTimeout(eventSpec.debounced); + } + eventSpec.debounced = setTimeout(function () { + onFeature.execute(ctx); + }, eventSpec.debounceTime); + return; + } + + // throttle + if (eventSpec.throttleTime) { + if ( + eventSpec.lastExec && + Date.now() < (eventSpec.lastExec + eventSpec.throttleTime) + ) { + return; + } else { + eventSpec.lastExec = Date.now(); + } + } + + // apply execute + onFeature.execute(ctx); + }); + }); + } + }, + }; + parser.setParent(start, onFeature); + return onFeature; + }); + + parser.addFeature("def", function (parser, runtime, tokens) { + if (!tokens.matchToken("def")) return; + var functionName = parser.requireElement("dotOrColonPath", tokens); + var nameVal = functionName.evaluate(); // OK + var nameSpace = nameVal.split("."); + var funcName = nameSpace.pop(); + + var args = []; + if (tokens.matchOpToken("(")) { + if (tokens.matchOpToken(")")) { + // empty args list + } else { + do { + args.push(tokens.requireTokenType("IDENTIFIER")); + } while (tokens.matchOpToken(",")); + tokens.requireOpToken(")"); + } + } + + var start = parser.requireElement("commandList", tokens); + + var errorSymbol, errorHandler; + if (tokens.matchToken("catch")) { + errorSymbol = tokens.requireTokenType("IDENTIFIER").value; + errorHandler = parser.parseElement("commandList", tokens); + } + + if (tokens.matchToken("finally")) { + var finallyHandler = parser.requireElement("commandList", tokens); + parser.ensureTerminated(finallyHandler); + } + + var functionFeature = { + displayName: + funcName + + "(" + + args + .map(function (arg) { + return arg.value; + }) + .join(", ") + + ")", + name: funcName, + args: args, + start: start, + errorHandler: errorHandler, + errorSymbol: errorSymbol, + finallyHandler: finallyHandler, + install: function (target, source) { + var func = function () { + // null, worker + var ctx = runtime.makeContext(source, functionFeature, target, null); + + // install error handler if any + ctx.meta.errorHandler = errorHandler; + ctx.meta.errorSymbol = errorSymbol; + ctx.meta.finallyHandler = finallyHandler; + + for (var i = 0; i < args.length; i++) { + var name = args[i]; + var argumentVal = arguments[i]; + if (name) { + ctx.locals[name.value] = argumentVal; + } + } + ctx.meta.caller = arguments[args.length]; + if (ctx.meta.caller) { + ctx.meta.callingCommand = ctx.meta.caller.meta.command; + } + var resolve, + reject = null; + var promise = new Promise(function (theResolve, theReject) { + resolve = theResolve; + reject = theReject; + }); + start.execute(ctx); + if (ctx.meta.returned) { + return ctx.meta.returnValue; + } else { + ctx.meta.resolve = resolve; + ctx.meta.reject = reject; + return promise; + } + }; + func.hyperfunc = true; + func.hypername = nameVal; + runtime.assignToNamespace(target, nameSpace, funcName, func); + }, + }; + + parser.ensureTerminated(start); + + // terminate error handler if any + if (errorHandler) { + parser.ensureTerminated(errorHandler); + } + + parser.setParent(start, functionFeature); + return functionFeature; + }); + + parser.addFeature("set", function (parser, runtime, tokens) { + let setCmd = parser.parseElement("setCommand", tokens); + if (setCmd) { + if (setCmd.target.scope !== "element") { + parser.raiseParseError(tokens, "variables declared at the feature level must be element scoped."); + } + let setFeature = { + start: setCmd, + install: function (target, source) { + setCmd && setCmd.execute(runtime.makeContext(target, setFeature, target, null)); + }, + }; + parser.ensureTerminated(setCmd); + return setFeature; + } + }); + + parser.addFeature("init", function (parser, runtime, tokens) { + if (!tokens.matchToken("init")) return; + + var immediately = tokens.matchToken("immediately"); + + var start = parser.requireElement("commandList", tokens); + var initFeature = { + start: start, + install: function (target, source) { + let handler = function () { + start && start.execute(runtime.makeContext(target, initFeature, target, null)); + }; + if (immediately) { + handler(); + } else { + setTimeout(handler, 0); + } + }, + }; + + // terminate body + parser.ensureTerminated(start); + parser.setParent(start, initFeature); + return initFeature; + }); + + parser.addFeature("worker", function (parser, runtime, tokens) { + if (tokens.matchToken("worker")) { + parser.raiseParseError( + tokens, + "In order to use the 'worker' feature, include " + + "the _hyperscript worker plugin. See " + + "https://hyperscript.org/features/worker/ for " + + "more info." + ); + return undefined + } + }); + + parser.addFeature("behavior", function (parser, runtime, tokens) { + if (!tokens.matchToken("behavior")) return; + var path = parser.requireElement("dotOrColonPath", tokens).evaluate(); + var nameSpace = path.split("."); + var name = nameSpace.pop(); + + var formalParams = []; + if (tokens.matchOpToken("(") && !tokens.matchOpToken(")")) { + do { + formalParams.push(tokens.requireTokenType("IDENTIFIER").value); + } while (tokens.matchOpToken(",")); + tokens.requireOpToken(")"); + } + var hs = parser.requireElement("hyperscript", tokens); + for (var i = 0; i < hs.features.length; i++) { + var feature = hs.features[i]; + feature.behavior = path; + } + + return { + install: function (target, source) { + runtime.assignToNamespace( + globalScope.document && globalScope.document.body, + nameSpace, + name, + function (target, source, innerArgs) { + var internalData = runtime.getInternalData(target); + var elementScope = getOrInitObject(internalData, path + "Scope"); + for (var i = 0; i < formalParams.length; i++) { + elementScope[formalParams[i]] = innerArgs[formalParams[i]]; + } + hs.apply(target, source); + } + ); + }, + }; + }); + + parser.addFeature("install", function (parser, runtime, tokens) { + if (!tokens.matchToken("install")) return; + var behaviorPath = parser.requireElement("dotOrColonPath", tokens).evaluate(); + var behaviorNamespace = behaviorPath.split("."); + var args = parser.parseElement("namedArgumentList", tokens); + + var installFeature; + return (installFeature = { + install: function (target, source) { + runtime.unifiedEval( + { + args: [args], + op: function (ctx, args) { + var behavior = globalScope; + for (var i = 0; i < behaviorNamespace.length; i++) { + behavior = behavior[behaviorNamespace[i]]; + if (typeof behavior !== "object" && typeof behavior !== "function") + throw new Error("No such behavior defined as " + behaviorPath); + } + + if (!(behavior instanceof Function)) + throw new Error(behaviorPath + " is not a behavior"); + + behavior(target, source, args); + }, + }, + runtime.makeContext(target, installFeature, target, null) + ); + }, + }); + }); + + parser.addGrammarElement("jsBody", function (parser, runtime, tokens) { + var jsSourceStart = tokens.currentToken().start; + var jsLastToken = tokens.currentToken(); + + var funcNames = []; + var funcName = ""; + var expectFunctionDeclaration = false; + while (tokens.hasMore()) { + jsLastToken = tokens.consumeToken(); + var peek = tokens.token(0, true); + if (peek.type === "IDENTIFIER" && peek.value === "end") { + break; + } + if (expectFunctionDeclaration) { + if (jsLastToken.type === "IDENTIFIER" || jsLastToken.type === "NUMBER") { + funcName += jsLastToken.value; + } else { + if (funcName !== "") funcNames.push(funcName); + funcName = ""; + expectFunctionDeclaration = false; + } + } else if (jsLastToken.type === "IDENTIFIER" && jsLastToken.value === "function") { + expectFunctionDeclaration = true; + } + } + var jsSourceEnd = jsLastToken.end + 1; + + return { + type: "jsBody", + exposedFunctionNames: funcNames, + jsSource: tokens.source.substring(jsSourceStart, jsSourceEnd), + }; + }); + + parser.addFeature("js", function (parser, runtime, tokens) { + if (!tokens.matchToken("js")) return; + var jsBody = parser.requireElement("jsBody", tokens); + + var jsSource = + jsBody.jsSource + + "\nreturn { " + + jsBody.exposedFunctionNames + .map(function (name) { + return name + ":" + name; + }) + .join(",") + + " } "; + var func = new Function(jsSource); + + return { + jsSource: jsSource, + function: func, + exposedFunctionNames: jsBody.exposedFunctionNames, + install: function () { + Object.assign(globalScope, func()); + }, + }; + }); + + parser.addCommand("js", function (parser, runtime, tokens) { + if (!tokens.matchToken("js")) return; + // Parse inputs + var inputs = []; + if (tokens.matchOpToken("(")) { + if (tokens.matchOpToken(")")) { + // empty input list + } else { + do { + var inp = tokens.requireTokenType("IDENTIFIER"); + inputs.push(inp.value); + } while (tokens.matchOpToken(",")); + tokens.requireOpToken(")"); + } + } + + var jsBody = parser.requireElement("jsBody", tokens); + tokens.matchToken("end"); + + var func = varargConstructor(Function, inputs.concat([jsBody.jsSource])); + + var command = { + jsSource: jsBody.jsSource, + function: func, + inputs: inputs, + op: function (context) { + var args = []; + inputs.forEach(function (input) { + args.push(runtime.resolveSymbol(input, context, 'default')); + }); + var result = func.apply(globalScope, args); + if (result && typeof result.then === "function") { + return new Promise(function (resolve) { + result.then(function (actualResult) { + context.result = actualResult; + resolve(runtime.findNext(this, context)); + }); + }); + } else { + context.result = result; + return runtime.findNext(this, context); + } + }, + }; + return command; + }); + + parser.addCommand("async", function (parser, runtime, tokens) { + if (!tokens.matchToken("async")) return; + if (tokens.matchToken("do")) { + var body = parser.requireElement("commandList", tokens); + + // Append halt + var end = body; + while (end.next) end = end.next; + end.next = runtime.HALT; + + tokens.requireToken("end"); + } else { + var body = parser.requireElement("command", tokens); + } + var command = { + body: body, + op: function (context) { + setTimeout(function () { + body.execute(context); + }); + return runtime.findNext(this, context); + }, + }; + parser.setParent(body, command); + return command; + }); + + parser.addCommand("tell", function (parser, runtime, tokens) { + var startToken = tokens.currentToken(); + if (!tokens.matchToken("tell")) return; + var value = parser.requireElement("expression", tokens); + var body = parser.requireElement("commandList", tokens); + if (tokens.hasMore() && !parser.featureStart(tokens.currentToken())) { + tokens.requireToken("end"); + } + var slot = "tell_" + startToken.start; + var tellCmd = { + value: value, + body: body, + args: [value], + resolveNext: function (context) { + var iterator = context.meta.iterators[slot]; + if (iterator.index < iterator.value.length) { + context.you = iterator.value[iterator.index++]; + return body; + } else { + // restore original me + context.you = iterator.originalYou; + if (this.next) { + return this.next; + } else { + return runtime.findNext(this.parent, context); + } + } + }, + op: function (context, value) { + if (value == null) { + value = []; + } else if (!(Array.isArray(value) || value instanceof NodeList)) { + value = [value]; + } + context.meta.iterators[slot] = { + originalYou: context.you, + index: 0, + value: value, + }; + return this.resolveNext(context); + }, + }; + parser.setParent(body, tellCmd); + return tellCmd; + }); + + parser.addCommand("wait", function (parser, runtime, tokens) { + if (!tokens.matchToken("wait")) return; + var command; + + // wait on event + if (tokens.matchToken("for")) { + tokens.matchToken("a"); // optional "a" + var events = []; + do { + var lookahead = tokens.token(0); + if (lookahead.type === 'NUMBER' || lookahead.type === 'L_PAREN') { + events.push({ + time: parser.requireElement('expression', tokens).evaluate() // TODO: do we want to allow async here? + }) + } else { + events.push({ + name: parser.requireElement("dotOrColonPath", tokens, "Expected event name").evaluate(), + args: parseEventArgs(tokens), + }); + } + } while (tokens.matchToken("or")); + + if (tokens.matchToken("from")) { + var on = parser.requireElement("expression", tokens); + } + + // wait on event + command = { + event: events, + on: on, + args: [on], + op: function (context, on) { + var target = on ? on : context.me; + if (!(target instanceof EventTarget)) + throw new Error("Not a valid event target: " + this.on.sourceFor()); + return new Promise((resolve) => { + var resolved = false; + for (const eventInfo of events) { + var listener = (event) => { + context.result = event; + if (eventInfo.args) { + for (const arg of eventInfo.args) { + context.locals[arg.value] = + event[arg.value] || (event.detail ? event.detail[arg.value] : null); + } + } + if (!resolved) { + resolved = true; + resolve(runtime.findNext(this, context)); + } + }; + if (eventInfo.name){ + target.addEventListener(eventInfo.name, listener, {once: true}); + } else if (eventInfo.time != null) { + setTimeout(listener, eventInfo.time, eventInfo.time) + } + } + }); + }, + }; + return command; + } else { + var time; + if (tokens.matchToken("a")) { + tokens.requireToken("tick"); + time = 0; + } else { + time = parser.requireElement("expression", tokens); + } + + command = { + type: "waitCmd", + time: time, + args: [time], + op: function (context, timeValue) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(runtime.findNext(this, context)); + }, timeValue); + }); + }, + execute: function (context) { + return runtime.unifiedExec(this, context); + }, + }; + return command; + } + }); + + // TODO - colon path needs to eventually become part of ruby-style symbols + parser.addGrammarElement("dotOrColonPath", function (parser, runtime, tokens) { + var root = tokens.matchTokenType("IDENTIFIER"); + if (root) { + var path = [root.value]; + + var separator = tokens.matchOpToken(".") || tokens.matchOpToken(":"); + if (separator) { + do { + path.push(tokens.requireTokenType("IDENTIFIER", "NUMBER").value); + } while (tokens.matchOpToken(separator.value)); + } + + return { + type: "dotOrColonPath", + path: path, + evaluate: function () { + return path.join(separator ? separator.value : ""); + }, + }; + } + }); + + + parser.addGrammarElement("eventName", function (parser, runtime, tokens) { + var token; + if ((token = tokens.matchTokenType("STRING"))) { + return { + evaluate: function() { + return token.value; + }, + }; + } + + return parser.parseElement("dotOrColonPath", tokens); + }); + + function parseSendCmd(cmdType, parser, runtime, tokens) { + var eventName = parser.requireElement("eventName", tokens); + + var details = parser.parseElement("namedArgumentList", tokens); + if ((cmdType === "send" && tokens.matchToken("to")) || + (cmdType === "trigger" && tokens.matchToken("on"))) { + var toExpr = parser.requireElement("expression", tokens); + } else { + var toExpr = parser.requireElement("implicitMeTarget", tokens); + } + + var sendCmd = { + eventName: eventName, + details: details, + to: toExpr, + args: [toExpr, eventName, details], + op: function (context, to, eventName, details) { + runtime.nullCheck(to, toExpr); + runtime.implicitLoop(to, function (target) { + runtime.triggerEvent(target, eventName, details, context.me); + }); + return runtime.findNext(sendCmd, context); + }, + }; + return sendCmd; + } + + parser.addCommand("trigger", function (parser, runtime, tokens) { + if (tokens.matchToken("trigger")) { + return parseSendCmd("trigger", parser, runtime, tokens); + } + }); + + parser.addCommand("send", function (parser, runtime, tokens) { + if (tokens.matchToken("send")) { + return parseSendCmd("send", parser, runtime, tokens); + } + }); + + var parseReturnFunction = function (parser, runtime, tokens, returnAValue) { + if (returnAValue) { + if (parser.commandBoundary(tokens.currentToken())) { + parser.raiseParseError(tokens, "'return' commands must return a value. If you do not wish to return a value, use 'exit' instead."); + } else { + var value = parser.requireElement("expression", tokens); + } + } + + var returnCmd = { + value: value, + args: [value], + op: function (context, value) { + var resolve = context.meta.resolve; + context.meta.returned = true; + context.meta.returnValue = value; + if (resolve) { + if (value) { + resolve(value); + } else { + resolve(); + } + } + return runtime.HALT; + }, + }; + return returnCmd; + }; + + parser.addCommand("return", function (parser, runtime, tokens) { + if (tokens.matchToken("return")) { + return parseReturnFunction(parser, runtime, tokens, true); + } + }); + + parser.addCommand("exit", function (parser, runtime, tokens) { + if (tokens.matchToken("exit")) { + return parseReturnFunction(parser, runtime, tokens, false); + } + }); + + parser.addCommand("halt", function (parser, runtime, tokens) { + if (tokens.matchToken("halt")) { + if (tokens.matchToken("the")) { + tokens.requireToken("event"); + // optional possessive + if (tokens.matchOpToken("'")) { + tokens.requireToken("s"); + } + var keepExecuting = true; + } + if (tokens.matchToken("bubbling")) { + var bubbling = true; + } else if (tokens.matchToken("default")) { + var haltDefault = true; + } + var exit = parseReturnFunction(parser, runtime, tokens, false); + + var haltCmd = { + keepExecuting: true, + bubbling: bubbling, + haltDefault: haltDefault, + exit: exit, + op: function (ctx) { + if (ctx.event) { + if (bubbling) { + ctx.event.stopPropagation(); + } else if (haltDefault) { + ctx.event.preventDefault(); + } else { + ctx.event.stopPropagation(); + ctx.event.preventDefault(); + } + if (keepExecuting) { + return runtime.findNext(this, ctx); + } else { + return exit; + } + } + }, + }; + return haltCmd; + } + }); + + parser.addCommand("log", function (parser, runtime, tokens) { + if (!tokens.matchToken("log")) return; + var exprs = [parser.parseElement("expression", tokens)]; + while (tokens.matchOpToken(",")) { + exprs.push(parser.requireElement("expression", tokens)); + } + if (tokens.matchToken("with")) { + var withExpr = parser.requireElement("expression", tokens); + } + var logCmd = { + exprs: exprs, + withExpr: withExpr, + args: [withExpr, exprs], + op: function (ctx, withExpr, values) { + if (withExpr) { + withExpr.apply(null, values); + } else { + console.log.apply(null, values); + } + return runtime.findNext(this, ctx); + }, + }; + return logCmd; + }); + + parser.addCommand("beep!", function (parser, runtime, tokens) { + if (!tokens.matchToken("beep!")) return; + var exprs = [parser.parseElement("expression", tokens)]; + while (tokens.matchOpToken(",")) { + exprs.push(parser.requireElement("expression", tokens)); + } + var beepCmd = { + exprs: exprs, + args: [exprs], + op: function (ctx, values) { + for (let i = 0; i < exprs.length; i++) { + const expr = exprs[i]; + const val = values[i]; + runtime.beepValueToConsole(ctx.me, expr, val); + } + return runtime.findNext(this, ctx); + }, + }; + return beepCmd; + }); + + parser.addCommand("throw", function (parser, runtime, tokens) { + if (!tokens.matchToken("throw")) return; + var expr = parser.requireElement("expression", tokens); + var throwCmd = { + expr: expr, + args: [expr], + op: function (ctx, expr) { + runtime.registerHyperTrace(ctx, expr); + throw expr; + }, + }; + return throwCmd; + }); + + var parseCallOrGet = function (parser, runtime, tokens) { + var expr = parser.requireElement("expression", tokens); + var callCmd = { + expr: expr, + args: [expr], + op: function (context, result) { + context.result = result; + return runtime.findNext(callCmd, context); + }, + }; + return callCmd; + }; + parser.addCommand("call", function (parser, runtime, tokens) { + if (!tokens.matchToken("call")) return; + var call = parseCallOrGet(parser, runtime, tokens); + if (call.expr && call.expr.type !== "functionCall") { + parser.raiseParseError(tokens, "Must be a function invocation"); + } + return call; + }); + parser.addCommand("get", function (parser, runtime, tokens) { + if (tokens.matchToken("get")) { + return parseCallOrGet(parser, runtime, tokens); + } + }); + + parser.addCommand("make", function (parser, runtime, tokens) { + if (!tokens.matchToken("make")) return; + tokens.matchToken("a") || tokens.matchToken("an"); + + var expr = parser.requireElement("expression", tokens); + + var args = []; + if (expr.type !== "queryRef" && tokens.matchToken("from")) { + do { + args.push(parser.requireElement("expression", tokens)); + } while (tokens.matchOpToken(",")); + } + + if (tokens.matchToken("called")) { + var target = parser.requireElement("symbol", tokens); + } + + var command; + if (expr.type === "queryRef") { + command = { + op: function (ctx) { + var match, + tagname = "div", + id, + classes = []; + var re = /(?:(^|#|\.)([^#\. ]+))/g; + while ((match = re.exec(expr.css))) { + if (match[1] === "") tagname = match[2].trim(); + else if (match[1] === "#") id = match[2].trim(); + else classes.push(match[2].trim()); + } + + var result = document.createElement(tagname); + if (id !== undefined) result.id = id; + for (var i = 0; i < classes.length; i++) { + var cls = classes[i]; + result.classList.add(cls) + } + + ctx.result = result; + if (target){ + runtime.setSymbol(target.name, ctx, target.scope, result); + } + + return runtime.findNext(this, ctx); + }, + }; + return command; + } else { + command = { + args: [expr, args], + op: function (ctx, expr, args) { + ctx.result = varargConstructor(expr, args); + if (target){ + runtime.setSymbol(target.name, ctx, target.scope, ctx.result); + } + + return runtime.findNext(this, ctx); + }, + }; + return command; + } + }); + + parser.addGrammarElement("pseudoCommand", function (parser, runtime, tokens) { + + let lookAhead = tokens.token(1); + if (!(lookAhead && lookAhead.op && (lookAhead.value === '.' || lookAhead.value === "("))) { + return null; + } + + var expr = parser.requireElement("primaryExpression", tokens); + + var rootRoot = expr.root; + var root = expr; + while (rootRoot.root != null) { + root = root.root; + rootRoot = rootRoot.root; + } + + if (expr.type !== "functionCall") { + parser.raiseParseError(tokens, "Pseudo-commands must be function calls"); + } + + if (root.type === "functionCall" && root.root.root == null) { + if (tokens.matchAnyToken("the", "to", "on", "with", "into", "from", "at")) { + var realRoot = parser.requireElement("expression", tokens); + } else if (tokens.matchToken("me")) { + var realRoot = parser.requireElement("implicitMeTarget", tokens); + } + } + + /** @type {ASTNode} */ + + var pseudoCommand + if(realRoot){ + pseudoCommand = { + type: "pseudoCommand", + root: realRoot, + argExressions: root.argExressions, + args: [realRoot, root.argExressions], + op: function (context, rootRoot, args) { + runtime.nullCheck(rootRoot, realRoot); + var func = rootRoot[root.root.name]; + runtime.nullCheck(func, root); + if (func.hyperfunc) { + args.push(context); + } + context.result = func.apply(rootRoot, args); + return runtime.findNext(pseudoCommand, context); + }, + execute: function (context) { + return runtime.unifiedExec(this, context); + }, + } + } else { + pseudoCommand = { + type: "pseudoCommand", + expr: expr, + args: [expr], + op: function (context, result) { + context.result = result; + return runtime.findNext(pseudoCommand, context); + }, + execute: function (context) { + return runtime.unifiedExec(this, context); + }, + }; + } + + return pseudoCommand; + }); + + /** + * @param {Parser} parser + * @param {Runtime} runtime + * @param {Tokens} tokens + * @param {*} target + * @param {*} value + * @returns + */ + var makeSetter = function (parser, runtime, tokens, target, value) { + + var symbolWrite = target.type === "symbol"; + var attributeWrite = target.type === "attributeRef"; + var styleWrite = target.type === "styleRef"; + var arrayWrite = target.type === "arrayIndex"; + + if (!(attributeWrite || styleWrite || symbolWrite) && target.root == null) { + parser.raiseParseError(tokens, "Can only put directly into symbols, not references"); + } + + var rootElt = null; + var prop = null; + if (symbolWrite) { + // rootElt is null + } else if (attributeWrite || styleWrite) { + rootElt = parser.requireElement("implicitMeTarget", tokens); + var attribute = target; + } else if(arrayWrite) { + prop = target.firstIndex; + rootElt = target.root; + } else { + prop = target.prop ? target.prop.value : null; + var attribute = target.attribute; + rootElt = target.root; + } + + /** @type {ASTNode} */ + var setCmd = { + target: target, + symbolWrite: symbolWrite, + value: value, + args: [rootElt, prop, value], + op: function (context, root, prop, valueToSet) { + if (symbolWrite) { + runtime.setSymbol(target.name, context, target.scope, valueToSet); + } else { + runtime.nullCheck(root, rootElt); + if (arrayWrite) { + root[prop] = valueToSet; + } else { + runtime.implicitLoop(root, function (elt) { + if (attribute) { + if (attribute.type === "attributeRef") { + if (valueToSet == null) { + elt.removeAttribute(attribute.name); + } else { + elt.setAttribute(attribute.name, valueToSet); + } + } else { + elt.style[attribute.name] = valueToSet; + } + } else { + elt[prop] = valueToSet; + } + }); + } + } + return runtime.findNext(this, context); + }, + }; + return setCmd; + }; + + parser.addCommand("default", function (parser, runtime, tokens) { + if (!tokens.matchToken("default")) return; + var target = parser.requireElement("assignableExpression", tokens); + tokens.requireToken("to"); + + var value = parser.requireElement("expression", tokens); + + /** @type {ASTNode} */ + var setter = makeSetter(parser, runtime, tokens, target, value); + var defaultCmd = { + target: target, + value: value, + setter: setter, + args: [target], + op: function (context, target) { + if (target) { + return runtime.findNext(this, context); + } else { + return setter; + } + }, + }; + setter.parent = defaultCmd; + return defaultCmd; + }); + + parser.addCommand("set", function (parser, runtime, tokens) { + if (!tokens.matchToken("set")) return; + if (tokens.currentToken().type === "L_BRACE") { + var obj = parser.requireElement("objectLiteral", tokens); + tokens.requireToken("on"); + var target = parser.requireElement("expression", tokens); + + var command = { + objectLiteral: obj, + target: target, + args: [obj, target], + op: function (ctx, obj, target) { + Object.assign(target, obj); + return runtime.findNext(this, ctx); + }, + }; + return command; + } + + try { + tokens.pushFollow("to"); + var target = parser.requireElement("assignableExpression", tokens); + } finally { + tokens.popFollow(); + } + tokens.requireToken("to"); + var value = parser.requireElement("expression", tokens); + return makeSetter(parser, runtime, tokens, target, value); + }); + + parser.addCommand("if", function (parser, runtime, tokens) { + if (!tokens.matchToken("if")) return; + var expr = parser.requireElement("expression", tokens); + tokens.matchToken("then"); // optional 'then' + var trueBranch = parser.parseElement("commandList", tokens); + var nestedIfStmt = false; + let elseToken = tokens.matchToken("else") || tokens.matchToken("otherwise"); + if (elseToken) { + let elseIfIfToken = tokens.peekToken("if"); + nestedIfStmt = elseIfIfToken != null && elseIfIfToken.line === elseToken.line; + if (nestedIfStmt) { + var falseBranch = parser.parseElement("command", tokens); + } else { + var falseBranch = parser.parseElement("commandList", tokens); + } + } + if (tokens.hasMore() && !nestedIfStmt) { + tokens.requireToken("end"); + } + + /** @type {ASTNode} */ + var ifCmd = { + expr: expr, + trueBranch: trueBranch, + falseBranch: falseBranch, + args: [expr], + op: function (context, exprValue) { + if (exprValue) { + return trueBranch; + } else if (falseBranch) { + return falseBranch; + } else { + return runtime.findNext(this, context); + } + }, + }; + parser.setParent(trueBranch, ifCmd); + parser.setParent(falseBranch, ifCmd); + return ifCmd; + }); + + var parseRepeatExpression = function (parser, tokens, runtime, startedWithForToken) { + var innerStartToken = tokens.currentToken(); + var identifier; + if (tokens.matchToken("for") || startedWithForToken) { + var identifierToken = tokens.requireTokenType("IDENTIFIER"); + identifier = identifierToken.value; + tokens.requireToken("in"); + var expression = parser.requireElement("expression", tokens); + } else if (tokens.matchToken("in")) { + identifier = "it"; + var expression = parser.requireElement("expression", tokens); + } else if (tokens.matchToken("while")) { + var whileExpr = parser.requireElement("expression", tokens); + } else if (tokens.matchToken("until")) { + var isUntil = true; + if (tokens.matchToken("event")) { + var evt = parser.requireElement("dotOrColonPath", tokens, "Expected event name"); + if (tokens.matchToken("from")) { + var on = parser.requireElement("expression", tokens); + } + } else { + var whileExpr = parser.requireElement("expression", tokens); + } + } else { + if (!parser.commandBoundary(tokens.currentToken()) && + tokens.currentToken().value !== 'forever') { + var times = parser.requireElement("expression", tokens); + tokens.requireToken("times"); + } else { + tokens.matchToken("forever"); // consume optional forever + var forever = true; + } + } + + if (tokens.matchToken("index")) { + var identifierToken = tokens.requireTokenType("IDENTIFIER"); + var indexIdentifier = identifierToken.value; + } + + var loop = parser.parseElement("commandList", tokens); + if (loop && evt) { + // if this is an event based loop, wait a tick at the end of the loop so that + // events have a chance to trigger in the loop condition o_O))) + var last = loop; + while (last.next) { + last = last.next; + } + var waitATick = { + type: "waitATick", + op: function () { + return new Promise(function (resolve) { + setTimeout(function () { + resolve(runtime.findNext(waitATick)); + }, 0); + }); + }, + }; + last.next = waitATick; + } + if (tokens.hasMore()) { + tokens.requireToken("end"); + } + + if (identifier == null) { + identifier = "_implicit_repeat_" + innerStartToken.start; + var slot = identifier; + } else { + var slot = identifier + "_" + innerStartToken.start; + } + + var repeatCmd = { + identifier: identifier, + indexIdentifier: indexIdentifier, + slot: slot, + expression: expression, + forever: forever, + times: times, + until: isUntil, + event: evt, + on: on, + whileExpr: whileExpr, + resolveNext: function () { + return this; + }, + loop: loop, + args: [whileExpr, times], + op: function (context, whileValue, times) { + var iteratorInfo = context.meta.iterators[slot]; + var keepLooping = false; + var loopVal = null; + if (this.forever) { + keepLooping = true; + } else if (this.until) { + if (evt) { + keepLooping = context.meta.iterators[slot].eventFired === false; + } else { + keepLooping = whileValue !== true; + } + } else if (whileExpr) { + keepLooping = whileValue; + } else if (times) { + keepLooping = iteratorInfo.index < times; + } else { + var nextValFromIterator = iteratorInfo.iterator.next(); + keepLooping = !nextValFromIterator.done; + loopVal = nextValFromIterator.value; + } + + if (keepLooping) { + if (iteratorInfo.value) { + context.result = context.locals[identifier] = loopVal; + } else { + context.result = iteratorInfo.index; + } + if (indexIdentifier) { + context.locals[indexIdentifier] = iteratorInfo.index; + } + iteratorInfo.index++; + return loop; + } else { + context.meta.iterators[slot] = null; + return runtime.findNext(this.parent, context); + } + }, + }; + parser.setParent(loop, repeatCmd); + var repeatInit = { + name: "repeatInit", + args: [expression, evt, on], + op: function (context, value, event, on) { + var iteratorInfo = { + index: 0, + value: value, + eventFired: false, + }; + context.meta.iterators[slot] = iteratorInfo; + if (value && value[Symbol.iterator]) { + iteratorInfo.iterator = value[Symbol.iterator](); + } + if (evt) { + var target = on || context.me; + target.addEventListener( + event, + function (e) { + context.meta.iterators[slot].eventFired = true; + }, + { once: true } + ); + } + return repeatCmd; // continue to loop + }, + execute: function (context) { + return runtime.unifiedExec(this, context); + }, + }; + parser.setParent(repeatCmd, repeatInit); + return repeatInit; + }; + + parser.addCommand("repeat", function (parser, runtime, tokens) { + if (tokens.matchToken("repeat")) { + return parseRepeatExpression(parser, tokens, runtime, false); + } + }); + + parser.addCommand("for", function (parser, runtime, tokens) { + if (tokens.matchToken("for")) { + return parseRepeatExpression(parser, tokens, runtime, true); + } + }); + + parser.addCommand("continue", function (parser, runtime, tokens) { + + if (!tokens.matchToken("continue")) return; + + var command = { + op: function (context) { + + // scan for the closest repeat statement + for (var parent = this.parent ; true ; parent = parent.parent) { + + if (parent == undefined) { + parser.raiseParseError(tokens, "Command `continue` cannot be used outside of a `repeat` loop.") + } + if (parent.loop != undefined) { + return parent.resolveNext(context) + } + } + } + }; + return command; + }); + + parser.addCommand("break", function (parser, runtime, tokens) { + + if (!tokens.matchToken("break")) return; + + var command = { + op: function (context) { + + // scan for the closest repeat statement + for (var parent = this.parent ; true ; parent = parent.parent) { + + if (parent == undefined) { + parser.raiseParseError(tokens, "Command `continue` cannot be used outside of a `repeat` loop.") + } + if (parent.loop != undefined) { + return runtime.findNext(parent.parent, context); + } + } + } + }; + return command; + }); + + parser.addGrammarElement("stringLike", function (parser, runtime, tokens) { + return parser.parseAnyOf(["string", "nakedString"], tokens); + }); + + parser.addCommand("append", function (parser, runtime, tokens) { + if (!tokens.matchToken("append")) return; + var targetExpr = null; + + var value = parser.requireElement("expression", tokens); + + /** @type {ASTNode} */ + var implicitResultSymbol = { + type: "symbol", + evaluate: function (context) { + return runtime.resolveSymbol("result", context); + }, + }; + + if (tokens.matchToken("to")) { + targetExpr = parser.requireElement("expression", tokens); + } else { + targetExpr = implicitResultSymbol; + } + + var setter = null; + if (targetExpr.type === "symbol" || targetExpr.type === "attributeRef" || targetExpr.root != null) { + setter = makeSetter(parser, runtime, tokens, targetExpr, implicitResultSymbol); + } + + var command = { + value: value, + target: targetExpr, + args: [targetExpr, value], + op: function (context, target, value) { + if (Array.isArray(target)) { + target.push(value); + return runtime.findNext(this, context); + } else if (target instanceof Element) { + target.innerHTML += value; + return runtime.findNext(this, context); + } else if(setter) { + context.result = (target || "") + value; + return setter; + } else { + throw Error("Unable to append a value!") + } + }, + execute: function (context) { + return runtime.unifiedExec(this, context/*, value, target*/); + }, + }; + + if (setter != null) { + setter.parent = command; + } + + return command; + }); + + function parsePickRange(parser, runtime, tokens) { + tokens.matchToken("at") || tokens.matchToken("from"); + const rv = { includeStart: true, includeEnd: false } + + rv.from = tokens.matchToken("start") ? 0 : parser.requireElement("expression", tokens) + + if (tokens.matchToken("to") || tokens.matchOpToken("..")) { + if (tokens.matchToken("end")) { + rv.toEnd = true; + } else { + rv.to = parser.requireElement("expression", tokens); + } + } + + if (tokens.matchToken("inclusive")) rv.includeEnd = true; + else if (tokens.matchToken("exclusive")) rv.includeStart = false; + + return rv; + } + + class RegExpIterator { + constructor(re, str) { + this.re = re; + this.str = str; + } + + next() { + const match = this.re.exec(this.str); + if (match === null) return { done: true }; + else return { value: match }; + } + } + + class RegExpIterable { + constructor(re, flags, str) { + this.re = re; + this.flags = flags; + this.str = str; + } + + [Symbol.iterator]() { + return new RegExpIterator(new RegExp(this.re, this.flags), this.str); + } + } + + parser.addCommand("pick", (parser, runtime, tokens) => { + if (!tokens.matchToken("pick")) return; + + tokens.matchToken("the"); + + if (tokens.matchToken("item") || tokens.matchToken("items") + || tokens.matchToken("character") || tokens.matchToken("characters")) { + const range = parsePickRange(parser, runtime, tokens); + + tokens.requireToken("from"); + const root = parser.requireElement("expression", tokens); + + return { + args: [root, range.from, range.to], + op(ctx, root, from, to) { + if (range.toEnd) to = root.length; + if (!range.includeStart) from++; + if (range.includeEnd) to++; + if (to == null || to == undefined) to = from + 1; + ctx.result = root.slice(from, to); + return runtime.findNext(this, ctx); + } + } + } + + if (tokens.matchToken("match")) { + tokens.matchToken("of"); + const re = parser.parseElement("expression", tokens); + let flags = "" + if (tokens.matchOpToken("|")) { + flags = tokens.requireToken("identifier").value; + } + + tokens.requireToken("from"); + const root = parser.parseElement("expression", tokens); + + return { + args: [root, re], + op(ctx, root, re) { + ctx.result = new RegExp(re, flags).exec(root); + return runtime.findNext(this, ctx); + } + } + } + + if (tokens.matchToken("matches")) { + tokens.matchToken("of"); + const re = parser.parseElement("expression", tokens); + let flags = "gu" + if (tokens.matchOpToken("|")) { + flags = 'g' + tokens.requireToken("identifier").value.replace('g', ''); + } + console.log('flags', flags) + + tokens.requireToken("from"); + const root = parser.parseElement("expression", tokens); + + return { + args: [root, re], + op(ctx, root, re) { + ctx.result = new RegExpIterable(re, flags, root); + return runtime.findNext(this, ctx); + } + } + } + }); + + parser.addCommand("increment", function (parser, runtime, tokens) { + if (!tokens.matchToken("increment")) return; + var amountExpr; + + // This is optional. Defaults to "result" + var target = parser.parseElement("assignableExpression", tokens); + + // This is optional. Defaults to 1. + if (tokens.matchToken("by")) { + amountExpr = parser.requireElement("expression", tokens); + } + + var implicitIncrementOp = { + type: "implicitIncrementOp", + target: target, + args: [target, amountExpr], + op: function (context, targetValue, amount) { + targetValue = targetValue ? parseFloat(targetValue) : 0; + amount = amountExpr ? parseFloat(amount) : 1; + var newValue = targetValue + amount; + context.result = newValue; + return newValue; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + } + }; + + return makeSetter(parser, runtime, tokens, target, implicitIncrementOp); + }); + + parser.addCommand("decrement", function (parser, runtime, tokens) { + if (!tokens.matchToken("decrement")) return; + var amountExpr; + + // This is optional. Defaults to "result" + var target = parser.parseElement("assignableExpression", tokens); + + // This is optional. Defaults to 1. + if (tokens.matchToken("by")) { + amountExpr = parser.requireElement("expression", tokens); + } + + var implicitDecrementOp = { + type: "implicitDecrementOp", + target: target, + args: [target, amountExpr], + op: function (context, targetValue, amount) { + targetValue = targetValue ? parseFloat(targetValue) : 0; + amount = amountExpr ? parseFloat(amount) : 1; + var newValue = targetValue - amount; + context.result = newValue; + return newValue; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + } + }; + + return makeSetter(parser, runtime, tokens, target, implicitDecrementOp); + }); + + function parseConversionInfo(tokens, parser) { + var type = "text"; + var conversion; + tokens.matchToken("a") || tokens.matchToken("an"); + if (tokens.matchToken("json") || tokens.matchToken("Object")) { + type = "json"; + } else if (tokens.matchToken("response")) { + type = "response"; + } else if (tokens.matchToken("html")) { + type = "html"; + } else if (tokens.matchToken("text")) { + // default, ignore + } else { + conversion = parser.requireElement("dotOrColonPath", tokens).evaluate(); + } + return {type, conversion}; + } + + parser.addCommand("fetch", function (parser, runtime, tokens) { + if (!tokens.matchToken("fetch")) return; + var url = parser.requireElement("stringLike", tokens); + + if (tokens.matchToken("as")) { + var conversionInfo = parseConversionInfo(tokens, parser); + } + + if (tokens.matchToken("with") && tokens.currentToken().value !== "{") { + var args = parser.parseElement("nakedNamedArgumentList", tokens); + } else { + var args = parser.parseElement("objectLiteral", tokens); + } + + if (conversionInfo == null && tokens.matchToken("as")) { + conversionInfo = parseConversionInfo(tokens, parser); + } + + var type = conversionInfo ? conversionInfo.type : "text"; + var conversion = conversionInfo ? conversionInfo.conversion : null + + /** @type {ASTNode} */ + var fetchCmd = { + url: url, + argExpressions: args, + args: [url, args], + op: function (context, url, args) { + var detail = args || {}; + detail["sender"] = context.me; + detail["headers"] = detail["headers"] || {} + var abortController = new AbortController(); + let abortListener = context.me.addEventListener('fetch:abort', function(){ + abortController.abort(); + }, {once: true}); + detail['signal'] = abortController.signal; + runtime.triggerEvent(context.me, "hyperscript:beforeFetch", detail); + runtime.triggerEvent(context.me, "fetch:beforeRequest", detail); + args = detail; + var finished = false; + if (args.timeout) { + setTimeout(function () { + if (!finished) { + abortController.abort(); + } + }, args.timeout); + } + return fetch(url, args) + .then(function (resp) { + let resultDetails = {response:resp}; + runtime.triggerEvent(context.me, "fetch:afterResponse", resultDetails); + resp = resultDetails.response; + + if (type === "response") { + context.result = resp; + runtime.triggerEvent(context.me, "fetch:afterRequest", {result:resp}); + finished = true; + return runtime.findNext(fetchCmd, context); + } + if (type === "json") { + return resp.json().then(function (result) { + context.result = result; + runtime.triggerEvent(context.me, "fetch:afterRequest", {result}); + finished = true; + return runtime.findNext(fetchCmd, context); + }); + } + return resp.text().then(function (result) { + if (conversion) result = runtime.convertValue(result, conversion); + + if (type === "html") result = runtime.convertValue(result, "Fragment"); + + context.result = result; + runtime.triggerEvent(context.me, "fetch:afterRequest", {result}); + finished = true; + return runtime.findNext(fetchCmd, context); + }); + }) + .catch(function (reason) { + runtime.triggerEvent(context.me, "fetch:error", { + reason: reason, + }); + throw reason; + }).finally(function(){ + context.me.removeEventListener('fetch:abort', abortListener); + }); + }, + }; + return fetchCmd; + }); + } + + function hyperscriptWebGrammar(parser) { + parser.addCommand("settle", function (parser, runtime, tokens) { + if (tokens.matchToken("settle")) { + if (!parser.commandBoundary(tokens.currentToken())) { + var onExpr = parser.requireElement("expression", tokens); + } else { + var onExpr = parser.requireElement("implicitMeTarget", tokens); + } + + var settleCommand = { + type: "settleCmd", + args: [onExpr], + op: function (context, on) { + runtime.nullCheck(on, onExpr); + var resolve = null; + var resolved = false; + var transitionStarted = false; + + var promise = new Promise(function (r) { + resolve = r; + }); + + // listen for a transition begin + on.addEventListener( + "transitionstart", + function () { + transitionStarted = true; + }, + { once: true } + ); + + // if no transition begins in 500ms, cancel + setTimeout(function () { + if (!transitionStarted && !resolved) { + resolve(runtime.findNext(settleCommand, context)); + } + }, 500); + + // continue on a transition emd + on.addEventListener( + "transitionend", + function () { + if (!resolved) { + resolve(runtime.findNext(settleCommand, context)); + } + }, + { once: true } + ); + return promise; + }, + execute: function (context) { + return runtime.unifiedExec(this, context); + }, + }; + return settleCommand; + } + }); + + parser.addCommand("add", function (parser, runtime, tokens) { + if (tokens.matchToken("add")) { + var classRef = parser.parseElement("classRef", tokens); + var attributeRef = null; + var cssDeclaration = null; + if (classRef == null) { + attributeRef = parser.parseElement("attributeRef", tokens); + if (attributeRef == null) { + cssDeclaration = parser.parseElement("styleLiteral", tokens); + if (cssDeclaration == null) { + parser.raiseParseError(tokens, "Expected either a class reference or attribute expression"); + } + } + } else { + var classRefs = [classRef]; + while ((classRef = parser.parseElement("classRef", tokens))) { + classRefs.push(classRef); + } + } + + if (tokens.matchToken("to")) { + var toExpr = parser.requireElement("expression", tokens); + } else { + var toExpr = parser.requireElement("implicitMeTarget", tokens); + } + + if (tokens.matchToken("when")) { + if (cssDeclaration) { + parser.raiseParseError(tokens, "Only class and properties are supported with a when clause") + } + var when = parser.requireElement("expression", tokens); + } + + if (classRefs) { + return { + classRefs: classRefs, + to: toExpr, + args: [toExpr, classRefs], + op: function (context, to, classRefs) { + runtime.nullCheck(to, toExpr); + runtime.forEach(classRefs, function (classRef) { + runtime.implicitLoop(to, function (target) { + if (when) { + context.result = target; + let whenResult = runtime.evaluateNoPromise(when, context); + if (whenResult) { + if (target instanceof Element) target.classList.add(classRef.className); + } else { + if (target instanceof Element) target.classList.remove(classRef.className); + } + context.result = null; + } else { + if (target instanceof Element) target.classList.add(classRef.className); + } + }); + }); + return runtime.findNext(this, context); + }, + }; + } else if (attributeRef) { + return { + type: "addCmd", + attributeRef: attributeRef, + to: toExpr, + args: [toExpr], + op: function (context, to, attrRef) { + runtime.nullCheck(to, toExpr); + runtime.implicitLoop(to, function (target) { + if (when) { + context.result = target; + let whenResult = runtime.evaluateNoPromise(when, context); + if (whenResult) { + target.setAttribute(attributeRef.name, attributeRef.value); + } else { + target.removeAttribute(attributeRef.name); + } + context.result = null; + } else { + target.setAttribute(attributeRef.name, attributeRef.value); + } + }); + return runtime.findNext(this, context); + }, + execute: function (ctx) { + return runtime.unifiedExec(this, ctx); + }, + }; + } else { + return { + type: "addCmd", + cssDeclaration: cssDeclaration, + to: toExpr, + args: [toExpr, cssDeclaration], + op: function (context, to, css) { + runtime.nullCheck(to, toExpr); + runtime.implicitLoop(to, function (target) { + target.style.cssText += css; + }); + return runtime.findNext(this, context); + }, + execute: function (ctx) { + return runtime.unifiedExec(this, ctx); + }, + }; + } + } + }); + + parser.addGrammarElement("styleLiteral", function (parser, runtime, tokens) { + if (!tokens.matchOpToken("{")) return; + + var stringParts = [""] + var exprs = [] + + while (tokens.hasMore()) { + if (tokens.matchOpToken("\\")) { + tokens.consumeToken(); + } else if (tokens.matchOpToken("}")) { + break; + } else if (tokens.matchToken("$")) { + var opencurly = tokens.matchOpToken("{"); + var expr = parser.parseElement("expression", tokens); + if (opencurly) tokens.requireOpToken("}"); + + exprs.push(expr) + stringParts.push("") + } else { + var tok = tokens.consumeToken(); + stringParts[stringParts.length-1] += tokens.source.substring(tok.start, tok.end); + } + + stringParts[stringParts.length-1] += tokens.lastWhitespace(); + } + + return { + type: "styleLiteral", + args: [exprs], + op: function (ctx, exprs) { + var rv = ""; + + stringParts.forEach(function (part, idx) { + rv += part; + if (idx in exprs) rv += exprs[idx]; + }); + + return rv; + }, + evaluate: function(ctx) { + return runtime.unifiedEval(this, ctx); + } + } + }) + + parser.addCommand("remove", function (parser, runtime, tokens) { + if (tokens.matchToken("remove")) { + var classRef = parser.parseElement("classRef", tokens); + var attributeRef = null; + var elementExpr = null; + if (classRef == null) { + attributeRef = parser.parseElement("attributeRef", tokens); + if (attributeRef == null) { + elementExpr = parser.parseElement("expression", tokens); + if (elementExpr == null) { + parser.raiseParseError( + tokens, + "Expected either a class reference, attribute expression or value expression" + ); + } + } + } else { + var classRefs = [classRef]; + while ((classRef = parser.parseElement("classRef", tokens))) { + classRefs.push(classRef); + } + } + + if (tokens.matchToken("from")) { + var fromExpr = parser.requireElement("expression", tokens); + } else { + if (elementExpr == null) { + var fromExpr = parser.requireElement("implicitMeTarget", tokens); + } + } + + if (elementExpr) { + return { + elementExpr: elementExpr, + from: fromExpr, + args: [elementExpr, fromExpr], + op: function (context, element, from) { + runtime.nullCheck(element, elementExpr); + runtime.implicitLoop(element, function (target) { + if (target.parentElement && (from == null || from.contains(target))) { + target.parentElement.removeChild(target); + } + }); + return runtime.findNext(this, context); + }, + }; + } else { + return { + classRefs: classRefs, + attributeRef: attributeRef, + elementExpr: elementExpr, + from: fromExpr, + args: [classRefs, fromExpr], + op: function (context, classRefs, from) { + runtime.nullCheck(from, fromExpr); + if (classRefs) { + runtime.forEach(classRefs, function (classRef) { + runtime.implicitLoop(from, function (target) { + target.classList.remove(classRef.className); + }); + }); + } else { + runtime.implicitLoop(from, function (target) { + target.removeAttribute(attributeRef.name); + }); + } + return runtime.findNext(this, context); + }, + }; + } + } + }); + + parser.addCommand("toggle", function (parser, runtime, tokens) { + if (tokens.matchToken("toggle")) { + tokens.matchAnyToken("the", "my"); + if (tokens.currentToken().type === "STYLE_REF") { + let styleRef = tokens.consumeToken(); + var name = styleRef.value.substr(1); + var visibility = true; + var hideShowStrategy = resolveHideShowStrategy(parser, tokens, name); + if (tokens.matchToken("of")) { + tokens.pushFollow("with"); + try { + var onExpr = parser.requireElement("expression", tokens); + } finally { + tokens.popFollow(); + } + } else { + var onExpr = parser.requireElement("implicitMeTarget", tokens); + } + } else if (tokens.matchToken("between")) { + var between = true; + var classRef = parser.parseElement("classRef", tokens); + tokens.requireToken("and"); + var classRef2 = parser.requireElement("classRef", tokens); + } else { + var classRef = parser.parseElement("classRef", tokens); + var attributeRef = null; + if (classRef == null) { + attributeRef = parser.parseElement("attributeRef", tokens); + if (attributeRef == null) { + parser.raiseParseError(tokens, "Expected either a class reference or attribute expression"); + } + } else { + var classRefs = [classRef]; + while ((classRef = parser.parseElement("classRef", tokens))) { + classRefs.push(classRef); + } + } + } + + if (visibility !== true) { + if (tokens.matchToken("on")) { + var onExpr = parser.requireElement("expression", tokens); + } else { + var onExpr = parser.requireElement("implicitMeTarget", tokens); + } + } + + if (tokens.matchToken("for")) { + var time = parser.requireElement("expression", tokens); + } else if (tokens.matchToken("until")) { + var evt = parser.requireElement("dotOrColonPath", tokens, "Expected event name"); + if (tokens.matchToken("from")) { + var from = parser.requireElement("expression", tokens); + } + } + + var toggleCmd = { + classRef: classRef, + classRef2: classRef2, + classRefs: classRefs, + attributeRef: attributeRef, + on: onExpr, + time: time, + evt: evt, + from: from, + toggle: function (on, classRef, classRef2, classRefs) { + runtime.nullCheck(on, onExpr); + if (visibility) { + runtime.implicitLoop(on, function (target) { + hideShowStrategy("toggle", target); + }); + } else if (between) { + runtime.implicitLoop(on, function (target) { + if (target.classList.contains(classRef.className)) { + target.classList.remove(classRef.className); + target.classList.add(classRef2.className); + } else { + target.classList.add(classRef.className); + target.classList.remove(classRef2.className); + } + }); + } else if (classRefs) { + runtime.forEach(classRefs, function (classRef) { + runtime.implicitLoop(on, function (target) { + target.classList.toggle(classRef.className); + }); + }); + } else { + runtime.forEach(on, function (target) { + if (target.hasAttribute(attributeRef.name)) { + target.removeAttribute(attributeRef.name); + } else { + target.setAttribute(attributeRef.name, attributeRef.value); + } + }); + } + }, + args: [onExpr, time, evt, from, classRef, classRef2, classRefs], + op: function (context, on, time, evt, from, classRef, classRef2, classRefs) { + if (time) { + return new Promise(function (resolve) { + toggleCmd.toggle(on, classRef, classRef2, classRefs); + setTimeout(function () { + toggleCmd.toggle(on, classRef, classRef2, classRefs); + resolve(runtime.findNext(toggleCmd, context)); + }, time); + }); + } else if (evt) { + return new Promise(function (resolve) { + var target = from || context.me; + target.addEventListener( + evt, + function () { + toggleCmd.toggle(on, classRef, classRef2, classRefs); + resolve(runtime.findNext(toggleCmd, context)); + }, + { once: true } + ); + toggleCmd.toggle(on, classRef, classRef2, classRefs); + }); + } else { + this.toggle(on, classRef, classRef2, classRefs); + return runtime.findNext(toggleCmd, context); + } + }, + }; + return toggleCmd; + } + }); + + var HIDE_SHOW_STRATEGIES = { + display: function (op, element, arg) { + if (arg) { + element.style.display = arg; + } else if (op === "toggle") { + if (getComputedStyle(element).display === "none") { + HIDE_SHOW_STRATEGIES.display("show", element, arg); + } else { + HIDE_SHOW_STRATEGIES.display("hide", element, arg); + } + } else if (op === "hide") { + const internalData = parser.runtime.getInternalData(element); + if (internalData.originalDisplay == null) { + internalData.originalDisplay = element.style.display; + } + element.style.display = "none"; + } else { + const internalData = parser.runtime.getInternalData(element); + if (internalData.originalDisplay && internalData.originalDisplay !== 'none') { + element.style.display = internalData.originalDisplay; + } else { + element.style.removeProperty('display'); + } + } + }, + visibility: function (op, element, arg) { + if (arg) { + element.style.visibility = arg; + } else if (op === "toggle") { + if (getComputedStyle(element).visibility === "hidden") { + HIDE_SHOW_STRATEGIES.visibility("show", element, arg); + } else { + HIDE_SHOW_STRATEGIES.visibility("hide", element, arg); + } + } else if (op === "hide") { + element.style.visibility = "hidden"; + } else { + element.style.visibility = "visible"; + } + }, + opacity: function (op, element, arg) { + if (arg) { + element.style.opacity = arg; + } else if (op === "toggle") { + if (getComputedStyle(element).opacity === "0") { + HIDE_SHOW_STRATEGIES.opacity("show", element, arg); + } else { + HIDE_SHOW_STRATEGIES.opacity("hide", element, arg); + } + } else if (op === "hide") { + element.style.opacity = "0"; + } else { + element.style.opacity = "1"; + } + }, + }; + + var parseShowHideTarget = function (parser, runtime, tokens) { + var target; + var currentTokenValue = tokens.currentToken(); + if (currentTokenValue.value === "when" || currentTokenValue.value === "with" || parser.commandBoundary(currentTokenValue)) { + target = parser.parseElement("implicitMeTarget", tokens); + } else { + target = parser.parseElement("expression", tokens); + } + return target; + }; + + var resolveHideShowStrategy = function (parser, tokens, name) { + var configDefault = config.defaultHideShowStrategy; + var strategies = HIDE_SHOW_STRATEGIES; + if (config.hideShowStrategies) { + strategies = Object.assign(strategies, config.hideShowStrategies); // merge in user provided strategies + } + name = name || configDefault || "display"; + var value = strategies[name]; + if (value == null) { + parser.raiseParseError(tokens, "Unknown show/hide strategy : " + name); + } + return value; + }; + + parser.addCommand("hide", function (parser, runtime, tokens) { + if (tokens.matchToken("hide")) { + var targetExpr = parseShowHideTarget(parser, runtime, tokens); + + var name = null; + if (tokens.matchToken("with")) { + name = tokens.requireTokenType("IDENTIFIER", "STYLE_REF").value; + if (name.indexOf("*") === 0) { + name = name.substr(1); + } + } + var hideShowStrategy = resolveHideShowStrategy(parser, tokens, name); + + return { + target: targetExpr, + args: [targetExpr], + op: function (ctx, target) { + runtime.nullCheck(target, targetExpr); + runtime.implicitLoop(target, function (elt) { + hideShowStrategy("hide", elt); + }); + return runtime.findNext(this, ctx); + }, + }; + } + }); + + parser.addCommand("show", function (parser, runtime, tokens) { + if (tokens.matchToken("show")) { + var targetExpr = parseShowHideTarget(parser, runtime, tokens); + + var name = null; + if (tokens.matchToken("with")) { + name = tokens.requireTokenType("IDENTIFIER", "STYLE_REF").value; + if (name.indexOf("*") === 0) { + name = name.substr(1); + } + } + var arg = null; + if (tokens.matchOpToken(":")) { + var tokenArr = tokens.consumeUntilWhitespace(); + tokens.matchTokenType("WHITESPACE"); + arg = tokenArr + .map(function (t) { + return t.value; + }) + .join(""); + } + + if (tokens.matchToken("when")) { + var when = parser.requireElement("expression", tokens); + } + + var hideShowStrategy = resolveHideShowStrategy(parser, tokens, name); + + return { + target: targetExpr, + when: when, + args: [targetExpr], + op: function (ctx, target) { + runtime.nullCheck(target, targetExpr); + runtime.implicitLoop(target, function (elt) { + if (when) { + ctx.result = elt; + let whenResult = runtime.evaluateNoPromise(when, ctx); + if (whenResult) { + hideShowStrategy("show", elt, arg); + } else { + hideShowStrategy("hide", elt); + } + ctx.result = null; + } else { + hideShowStrategy("show", elt, arg); + } + }); + return runtime.findNext(this, ctx); + }, + }; + } + }); + + parser.addCommand("take", function (parser, runtime, tokens) { + if (tokens.matchToken("take")) { + let classRef = null; + let classRefs = []; + while ((classRef = parser.parseElement("classRef", tokens))) { + classRefs.push(classRef); + } + + var attributeRef = null; + var replacementValue = null; + + let weAreTakingClasses = classRefs.length > 0; + if (!weAreTakingClasses) { + attributeRef = parser.parseElement("attributeRef", tokens); + if (attributeRef == null) { + parser.raiseParseError(tokens, "Expected either a class reference or attribute expression"); + } + + if (tokens.matchToken("with")) { + replacementValue = parser.requireElement("expression", tokens); + } + } + + if (tokens.matchToken("from")) { + var fromExpr = parser.requireElement("expression", tokens); + } + + if (tokens.matchToken("for")) { + var forExpr = parser.requireElement("expression", tokens); + } else { + var forExpr = parser.requireElement("implicitMeTarget", tokens); + } + + if (weAreTakingClasses) { + var takeCmd = { + classRefs: classRefs, + from: fromExpr, + forElt: forExpr, + args: [classRefs, fromExpr, forExpr], + op: function (context, classRefs, from, forElt) { + runtime.nullCheck(forElt, forExpr); + runtime.implicitLoop(classRefs, function(classRef){ + var clazz = classRef.className; + if (from) { + runtime.implicitLoop(from, function (target) { + target.classList.remove(clazz); + }); + } else { + runtime.implicitLoop(classRef, function (target) { + target.classList.remove(clazz); + }); + } + runtime.implicitLoop(forElt, function (target) { + target.classList.add(clazz); + }); + }) + return runtime.findNext(this, context); + }, + }; + return takeCmd; + } else { + var takeCmd = { + attributeRef: attributeRef, + from: fromExpr, + forElt: forExpr, + args: [fromExpr, forExpr, replacementValue], + op: function (context, from, forElt, replacementValue) { + runtime.nullCheck(from, fromExpr); + runtime.nullCheck(forElt, forExpr); + runtime.implicitLoop(from, function (target) { + if (!replacementValue) { + target.removeAttribute(attributeRef.name); + } else { + target.setAttribute(attributeRef.name, replacementValue) + } + }); + runtime.implicitLoop(forElt, function (target) { + target.setAttribute(attributeRef.name, attributeRef.value || "") + }); + return runtime.findNext(this, context); + }, + }; + return takeCmd; + } + } + }); + + function putInto(runtime, context, prop, valueToPut) { + if (prop != null) { + var value = runtime.resolveSymbol(prop, context); + } else { + var value = context; + } + if (value instanceof Element || value instanceof HTMLDocument) { + while (value.firstChild) value.removeChild(value.firstChild); + value.append(parser.runtime.convertValue(valueToPut, "Fragment")); + runtime.processNode(value); + } else { + if (prop != null) { + runtime.setSymbol(prop, context, null, valueToPut); + } else { + throw "Don't know how to put a value into " + typeof context; + } + } + } + + parser.addCommand("put", function (parser, runtime, tokens) { + if (tokens.matchToken("put")) { + var value = parser.requireElement("expression", tokens); + + var operationToken = tokens.matchAnyToken("into", "before", "after"); + + if (operationToken == null && tokens.matchToken("at")) { + tokens.matchToken("the"); // optional "the" + operationToken = tokens.matchAnyToken("start", "end"); + tokens.requireToken("of"); + } + + if (operationToken == null) { + parser.raiseParseError(tokens, "Expected one of 'into', 'before', 'at start of', 'at end of', 'after'"); + } + var target = parser.requireElement("expression", tokens); + + var operation = operationToken.value; + + var arrayIndex = false; + var symbolWrite = false; + var rootExpr = null; + var prop = null; + + if (target.type === "arrayIndex" && operation === "into") { + arrayIndex = true; + prop = target.prop; + rootExpr = target.root; + } else if (target.prop && target.root && operation === "into") { + prop = target.prop.value; + rootExpr = target.root; + } else if (target.type === "symbol" && operation === "into") { + symbolWrite = true; + prop = target.name; + } else if (target.type === "attributeRef" && operation === "into") { + var attributeWrite = true; + prop = target.name; + rootExpr = parser.requireElement("implicitMeTarget", tokens); + } else if (target.type === "styleRef" && operation === "into") { + var styleWrite = true; + prop = target.name; + rootExpr = parser.requireElement("implicitMeTarget", tokens); + } else if (target.attribute && operation === "into") { + var attributeWrite = target.attribute.type === "attributeRef"; + var styleWrite = target.attribute.type === "styleRef"; + prop = target.attribute.name; + rootExpr = target.root; + } else { + rootExpr = target; + } + + var putCmd = { + target: target, + operation: operation, + symbolWrite: symbolWrite, + value: value, + args: [rootExpr, prop, value], + op: function (context, root, prop, valueToPut) { + if (symbolWrite) { + putInto(runtime, context, prop, valueToPut); + } else { + runtime.nullCheck(root, rootExpr); + if (operation === "into") { + if (attributeWrite) { + runtime.implicitLoop(root, function (elt) { + elt.setAttribute(prop, valueToPut); + }); + } else if (styleWrite) { + runtime.implicitLoop(root, function (elt) { + elt.style[prop] = valueToPut; + }); + } else if (arrayIndex) { + root[prop] = valueToPut; + } else { + runtime.implicitLoop(root, function (elt) { + putInto(runtime, elt, prop, valueToPut); + }); + } + } else { + var op = + operation === "before" + ? Element.prototype.before + : operation === "after" + ? Element.prototype.after + : operation === "start" + ? Element.prototype.prepend + : operation === "end" + ? Element.prototype.append + : Element.prototype.append; // unreachable + + runtime.implicitLoop(root, function (elt) { + op.call( + elt, + valueToPut instanceof Node + ? valueToPut + : runtime.convertValue(valueToPut, "Fragment") + ); + // process any new content + if (elt.parentElement) { + runtime.processNode(elt.parentElement); + } else { + runtime.processNode(elt); + } + }); + } + } + return runtime.findNext(this, context); + }, + }; + return putCmd; + } + }); + + function parsePseudopossessiveTarget(parser, runtime, tokens) { + var targets; + if ( + tokens.matchToken("the") || + tokens.matchToken("element") || + tokens.matchToken("elements") || + tokens.currentToken().type === "CLASS_REF" || + tokens.currentToken().type === "ID_REF" || + (tokens.currentToken().op && tokens.currentToken().value === "<") + ) { + parser.possessivesDisabled = true; + try { + targets = parser.parseElement("expression", tokens); + } finally { + delete parser.possessivesDisabled; + } + // optional possessive + if (tokens.matchOpToken("'")) { + tokens.requireToken("s"); + } + } else if (tokens.currentToken().type === "IDENTIFIER" && tokens.currentToken().value === "its") { + var identifier = tokens.matchToken("its"); + targets = { + type: "pseudopossessiveIts", + token: identifier, + name: identifier.value, + evaluate: function (context) { + return runtime.resolveSymbol("it", context); + }, + }; + } else { + tokens.matchToken("my") || tokens.matchToken("me"); // consume optional 'my' + targets = parser.parseElement("implicitMeTarget", tokens); + } + return targets; + } + + parser.addCommand("transition", function (parser, runtime, tokens) { + if (tokens.matchToken("transition")) { + var targetsExpr = parsePseudopossessiveTarget(parser, runtime, tokens); + + var properties = []; + var from = []; + var to = []; + var currentToken = tokens.currentToken(); + while ( + !parser.commandBoundary(currentToken) && + currentToken.value !== "over" && + currentToken.value !== "using" + ) { + if (tokens.currentToken().type === "STYLE_REF") { + let styleRef = tokens.consumeToken(); + let styleProp = styleRef.value.substr(1); + properties.push({ + type: "styleRefValue", + evaluate: function () { + return styleProp; + }, + }); + } else { + properties.push(parser.requireElement("stringLike", tokens)); + } + + if (tokens.matchToken("from")) { + from.push(parser.requireElement("expression", tokens)); + } else { + from.push(null); + } + tokens.requireToken("to"); + if (tokens.matchToken("initial")) { + to.push({ + type: "initial_literal", + evaluate : function(){ + return "initial"; + } + }); + } else { + to.push(parser.requireElement("expression", tokens)); + } + currentToken = tokens.currentToken(); + } + if (tokens.matchToken("over")) { + var over = parser.requireElement("expression", tokens); + } else if (tokens.matchToken("using")) { + var using = parser.requireElement("expression", tokens); + } + + var transition = { + to: to, + args: [targetsExpr, properties, from, to, using, over], + op: function (context, targets, properties, from, to, using, over) { + runtime.nullCheck(targets, targetsExpr); + var promises = []; + runtime.implicitLoop(targets, function (target) { + var promise = new Promise(function (resolve, reject) { + var initialTransition = target.style.transition; + if (over) { + target.style.transition = "all " + over + "ms ease-in"; + } else if (using) { + target.style.transition = using; + } else { + target.style.transition = config.defaultTransition; + } + var internalData = runtime.getInternalData(target); + var computedStyles = getComputedStyle(target); + + var initialStyles = {}; + for (var i = 0; i < computedStyles.length; i++) { + var name = computedStyles[i]; + var initialValue = computedStyles[name]; + initialStyles[name] = initialValue; + } + + // store initial values + if (!internalData.initialStyles) { + internalData.initialStyles = initialStyles; + } + + for (var i = 0; i < properties.length; i++) { + var property = properties[i]; + var fromVal = from[i]; + if (fromVal === "computed" || fromVal == null) { + target.style[property] = initialStyles[property]; + } else { + target.style[property] = fromVal; + } + } + //console.log("transition started", transition); + + var transitionStarted = false; + var resolved = false; + + target.addEventListener( + "transitionend", + function () { + if (!resolved) { + //console.log("transition ended", transition); + target.style.transition = initialTransition; + resolved = true; + resolve(); + } + }, + { once: true } + ); + + target.addEventListener( + "transitionstart", + function () { + transitionStarted = true; + }, + { once: true } + ); + + // it no transition has started in 100ms, continue + setTimeout(function () { + if (!resolved && !transitionStarted) { + //console.log("transition ended", transition); + target.style.transition = initialTransition; + resolved = true; + resolve(); + } + }, 100); + + setTimeout(function () { + var autoProps = []; + for (var i = 0; i < properties.length; i++) { + var property = properties[i]; + var toVal = to[i]; + if (toVal === "initial") { + var propertyValue = internalData.initialStyles[property]; + target.style[property] = propertyValue; + } else { + target.style[property] = toVal; + } + //console.log("set", property, "to", target.style[property], "on", target, "value passed in : ", toVal); + } + }, 0); + }); + promises.push(promise); + }); + return Promise.all(promises).then(function () { + return runtime.findNext(transition, context); + }); + }, + }; + return transition; + } + }); + + parser.addCommand("measure", function (parser, runtime, tokens) { + if (!tokens.matchToken("measure")) return; + + var targetExpr = parsePseudopossessiveTarget(parser, runtime, tokens); + + var propsToMeasure = []; + if (!parser.commandBoundary(tokens.currentToken())) + do { + propsToMeasure.push(tokens.matchTokenType("IDENTIFIER").value); + } while (tokens.matchOpToken(",")); + + return { + properties: propsToMeasure, + args: [targetExpr], + op: function (ctx, target) { + runtime.nullCheck(target, targetExpr); + if (0 in target) target = target[0]; // not measuring multiple elts + var rect = target.getBoundingClientRect(); + var scroll = { + top: target.scrollTop, + left: target.scrollLeft, + topMax: target.scrollTopMax, + leftMax: target.scrollLeftMax, + height: target.scrollHeight, + width: target.scrollWidth, + }; + + ctx.result = { + x: rect.x, + y: rect.y, + left: rect.left, + top: rect.top, + right: rect.right, + bottom: rect.bottom, + width: rect.width, + height: rect.height, + bounds: rect, + + scrollLeft: scroll.left, + scrollTop: scroll.top, + scrollLeftMax: scroll.leftMax, + scrollTopMax: scroll.topMax, + scrollWidth: scroll.width, + scrollHeight: scroll.height, + scroll: scroll, + }; + + runtime.forEach(propsToMeasure, function (prop) { + if (prop in ctx.result) ctx.locals[prop] = ctx.result[prop]; + else throw "No such measurement as " + prop; + }); + + return runtime.findNext(this, ctx); + }, + }; + }); + + parser.addLeafExpression("closestExpr", function (parser, runtime, tokens) { + if (tokens.matchToken("closest")) { + if (tokens.matchToken("parent")) { + var parentSearch = true; + } + + var css = null; + if (tokens.currentToken().type === "ATTRIBUTE_REF") { + var attributeRef = parser.requireElement("attributeRefAccess", tokens, null); + css = "[" + attributeRef.attribute.name + "]"; + } + + if (css == null) { + var expr = parser.requireElement("expression", tokens); + if (expr.css == null) { + parser.raiseParseError(tokens, "Expected a CSS expression"); + } else { + css = expr.css; + } + } + + if (tokens.matchToken("to")) { + var to = parser.parseElement("expression", tokens); + } else { + var to = parser.parseElement("implicitMeTarget", tokens); + } + + var closestExpr = { + type: "closestExpr", + parentSearch: parentSearch, + expr: expr, + css: css, + to: to, + args: [to], + op: function (ctx, to) { + if (to == null) { + return null; + } else { + let result = []; + runtime.implicitLoop(to, function(to){ + if (parentSearch) { + result.push(to.parentElement ? to.parentElement.closest(css) : null); + } else { + result.push(to.closest(css)); + } + }) + if (runtime.shouldAutoIterate(to)) { + return result; + } else { + return result[0]; + } + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + + if (attributeRef) { + attributeRef.root = closestExpr; + attributeRef.args = [closestExpr]; + return attributeRef; + } else { + return closestExpr; + } + } + }); + + parser.addCommand("go", function (parser, runtime, tokens) { + if (tokens.matchToken("go")) { + if (tokens.matchToken("back")) { + var back = true; + } else { + tokens.matchToken("to"); + if (tokens.matchToken("url")) { + var target = parser.requireElement("stringLike", tokens); + var url = true; + if (tokens.matchToken("in")) { + tokens.requireToken("new"); + tokens.requireToken("window"); + var newWindow = true; + } + } else { + tokens.matchToken("the"); // optional the + var verticalPosition = tokens.matchAnyToken("top", "middle", "bottom"); + var horizontalPosition = tokens.matchAnyToken("left", "center", "right"); + if (verticalPosition || horizontalPosition) { + tokens.requireToken("of"); + } + var target = parser.requireElement("unaryExpression", tokens); + + var plusOrMinus = tokens.matchAnyOpToken("+", "-"); + if (plusOrMinus) { + tokens.pushFollow("px"); + try { + var offset = parser.requireElement("expression", tokens); + } finally { + tokens.popFollow(); + } + } + tokens.matchToken("px"); // optional px + + var smoothness = tokens.matchAnyToken("smoothly", "instantly"); + + var scrollOptions = { + block: "start", + inline: "nearest" + }; + + if (verticalPosition) { + if (verticalPosition.value === "top") { + scrollOptions.block = "start"; + } else if (verticalPosition.value === "bottom") { + scrollOptions.block = "end"; + } else if (verticalPosition.value === "middle") { + scrollOptions.block = "center"; + } + } + + if (horizontalPosition) { + if (horizontalPosition.value === "left") { + scrollOptions.inline = "start"; + } else if (horizontalPosition.value === "center") { + scrollOptions.inline = "center"; + } else if (horizontalPosition.value === "right") { + scrollOptions.inline = "end"; + } + } + + if (smoothness) { + if (smoothness.value === "smoothly") { + scrollOptions.behavior = "smooth"; + } else if (smoothness.value === "instantly") { + scrollOptions.behavior = "instant"; + } + } + } + } + + var goCmd = { + target: target, + args: [target, offset], + op: function (ctx, to, offset) { + if (back) { + window.history.back(); + } else if (url) { + if (to) { + if (newWindow) { + window.open(to); + } else { + window.location.href = to; + } + } + } else { + runtime.implicitLoop(to, function (target) { + + if (target === window) { + target = document.body; + } + + if(plusOrMinus) { + // a scroll w/ an offset of some sort + let boundingRect = target.getBoundingClientRect(); + + let scrollShim = document.createElement("div"); + + let actualOffset = plusOrMinus.value === "+" ? offset : offset * -1; + + let offsetX = scrollOptions.inline == "start" || scrollOptions.inline == "end" ? actualOffset : 0; + + let offsetY = scrollOptions.block == "start" || scrollOptions.block == "end" ? actualOffset : 0; + + scrollShim.style.position = "absolute"; + scrollShim.style.top = (boundingRect.top + window.scrollY + offsetY) + "px"; + scrollShim.style.left = (boundingRect.left + window.scrollX + offsetX) + "px"; + scrollShim.style.height = boundingRect.height + "px"; + scrollShim.style.width = boundingRect.width + "px"; + scrollShim.style.zIndex = "" + Number.MIN_SAFE_INTEGER; + scrollShim.style.opacity = "0"; + + document.body.appendChild(scrollShim); + setTimeout(function () { + document.body.removeChild(scrollShim); + }, 100); + + target = scrollShim; + } + + target.scrollIntoView(scrollOptions); + }); + } + return runtime.findNext(goCmd, ctx); + }, + }; + return goCmd; + } + }); + + config.conversions.dynamicResolvers.push(function (str, node) { + if (!(str === "Values" || str.indexOf("Values:") === 0)) { + return; + } + var conversion = str.split(":")[1]; + /** @type Object */ + var result = {}; + + var implicitLoop = parser.runtime.implicitLoop.bind(parser.runtime); + + implicitLoop(node, function (/** @type HTMLInputElement */ node) { + // Try to get a value directly from this node + var input = getInputInfo(node); + + if (input !== undefined) { + result[input.name] = input.value; + return; + } + + // Otherwise, try to query all child elements of this node that *should* contain values. + if (node.querySelectorAll != undefined) { + /** @type {NodeListOf} */ + var children = node.querySelectorAll("input,select,textarea"); + children.forEach(appendValue); + } + }); + + if (conversion) { + if (conversion === "JSON") { + return JSON.stringify(result); + } else if (conversion === "Form") { + /** @ts-ignore */ + // TODO: does this work with multiple inputs of the same name? + return new URLSearchParams(result).toString(); + } else { + throw "Unknown conversion: " + conversion; + } + } else { + return result; + } + + /** + * @param {HTMLInputElement} node + */ + function appendValue(node) { + var info = getInputInfo(node); + + if (info == undefined) { + return; + } + + // If there is no value already stored in this space. + if (result[info.name] == undefined) { + result[info.name] = info.value; + return; + } + + if (Array.isArray(result[info.name]) && Array.isArray(info.value)) { + result[info.name] = [].concat(result[info.name], info.value); + return; + } + } + + /** + * @param {HTMLInputElement} node + * @returns {{name:string, value:string | string[]} | undefined} + */ + function getInputInfo(node) { + try { + /** @type {{name: string, value: string | string[]}}*/ + var result = { + name: node.name, + value: node.value, + }; + + if (result.name == undefined || result.value == undefined) { + return undefined; + } + + if (node.type == "radio" && node.checked == false) { + return undefined; + } + + if (node.type == "checkbox") { + if (node.checked == false) { + result.value = undefined; + } else if (typeof result.value === "string") { + result.value = [result.value]; + } + } + + if (node.type == "select-multiple") { + /** @type {NodeListOf} */ + var selected = node.querySelectorAll("option[selected]"); + + result.value = []; + for (var index = 0; index < selected.length; index++) { + result.value.push(selected[index].value); + } + } + return result; + } catch (e) { + return undefined; + } + } + }); + + config.conversions["HTML"] = function (value) { + var toHTML = /** @returns {string}*/ function (/** @type any*/ value) { + if (value instanceof Array) { + return value + .map(function (item) { + return toHTML(item); + }) + .join(""); + } + + if (value instanceof HTMLElement) { + return value.outerHTML; + } + + if (value instanceof NodeList) { + var result = ""; + for (var i = 0; i < value.length; i++) { + var node = value[i]; + if (node instanceof HTMLElement) { + result += node.outerHTML; + } + } + return result; + } + + if (value.toString) { + return value.toString(); + } + + return ""; + }; + + return toHTML(value); + }; + + config.conversions["Fragment"] = function (val) { + var frag = document.createDocumentFragment(); + parser.runtime.implicitLoop(val, function (val) { + if (val instanceof Node) frag.append(val); + else { + var temp = document.createElement("template"); + temp.innerHTML = val; + frag.append(temp.content); + } + }); + return frag; + }; + } + + + // Public API + + const runtime_ = new Runtime(), lexer_ = runtime_.lexer, parser_ = runtime_.parser + + /** + * + * @param {string} src + * @param {Partial} [ctx] + */ + function run(src, ctx) { + return runtime_.evaluate(src, ctx) + } + + function browserInit() { + /** @type {HTMLScriptElement[]} */ + var scripts = Array.from(globalScope.document.querySelectorAll("script[type='text/hyperscript'][src]")) + Promise.all( + scripts.map(function (script) { + return fetch(script.src) + .then(function (res) { + return res.text(); + }); + }) + ) + .then(script_values => script_values.forEach(sc => _hyperscript(sc))) + .then(() => ready(function () { + mergeMetaConfig(); + runtime_.processNode(document.documentElement); + globalScope.document.addEventListener("htmx:load", function (/** @type {CustomEvent} */ evt) { + runtime_.processNode(evt.detail.elt); + }); + })); + + function ready(fn) { + if (document.readyState !== "loading") { + setTimeout(fn); + } else { + document.addEventListener("DOMContentLoaded", fn); + } + } + + function getMetaConfig() { + /** @type {HTMLMetaElement} */ + var element = document.querySelector('meta[name="htmx-config"]'); + if (element) { + return parseJSON(element.content); + } else { + return null; + } + } + + function mergeMetaConfig() { + var metaConfig = getMetaConfig(); + if (metaConfig) { + Object.assign(config, metaConfig); + } + } + } + + /** + * @typedef {Object} HyperscriptAPI + * + * @property {Object} config + * @property {string} config.attributes + * @property {string} config.defaultTransition + * @property {string} config.disableSelector + * @property {typeof conversions} config.conversions + * + * @property {Object} internals + * @property {Lexer} internals.lexer + * @property {typeof Lexer} internals.Lexer + * @property {Parser} internals.parser + * @property {typeof Parser} internals.Parser + * @property {Runtime} internals.runtime + * @property {typeof Runtime} internals.Runtime + * + * @property {typeof ElementCollection} ElementCollection + * + * @property {(keyword: string, definition: ParseRule) => void} addFeature + * @property {(keyword: string, definition: ParseRule) => void} addCommand + * @property {(keyword: string, definition: ParseRule) => void} addLeafExpression + * @property {(keyword: string, definition: ParseRule) => void} addIndirectExpression + * + * @property {(src: string, ctx?: Partial) => any} evaluate + * @property {(src: string) => ASTNode} parse + * @property {(node: Element) => void} processNode + * + * @property {() => void} browserInit + * + * + * @typedef {HyperscriptAPI & ((src: string, ctx?: Partial) => any)} Hyperscript + */ + + /** + * @type {Hyperscript} + */ + const _hyperscript = Object.assign( + run, + { + config, + + use(plugin) { plugin(_hyperscript) }, + + internals: { + lexer: lexer_, parser: parser_, runtime: runtime_, + Lexer, Tokens, Parser, Runtime, + }, + ElementCollection, + + addFeature: parser_.addFeature.bind(parser_), + addCommand: parser_.addCommand.bind(parser_), + addLeafExpression: parser_.addLeafExpression.bind(parser_), + addIndirectExpression: parser_.addIndirectExpression.bind(parser_), + + evaluate: runtime_.evaluate.bind(runtime_), + parse: runtime_.parse.bind(runtime_), + processNode: runtime_.processNode.bind(runtime_), + version: "0.9.12", + browserInit, + } + ) + + return _hyperscript +}) + + +_hyperscript.addCommand('sort', function(parser, runtime, tokens) { + if (!tokens.matchToken("sort")) return; + + var sortables = parser.requireElement("expression", tokens); + + var operationToken = tokens.matchToken("by"); + + var sorting = parser.requireElement("expression", tokens); + + if (sorting.type != "symbol" && sorting.type != "attributeRef") { + return; + } + + var sortCmd = { + value: sortables, + args: [sortables], + op: function (context, sortables) { + if (sortables == null) { + sortables = []; + } else if (!(Array.isArray(sortables) || sortables instanceof NodeList)) { + sortables = [sortables]; + } + + unsortedGroup = [] + sortables.forEach(function (part, idx) { + unsortedGroup[idx] = [] + sortables.forEach(function (potentialSibling) { + if (part.parentNode == potentialSibling.parentNode) { + positionInParent = Array.prototype.indexOf.call(part.parentNode.children, potentialSibling) + unsortedGroup[idx].push(potentialSibling) + if (sorting.type === "attributeRef") { + console.log(sorting.name) + console.log(potentialSibling.getAttribute(sorting.name)) + } + if (sorting.type === "symbol") { + console.log(potentialSibling[sorting.name]) + } + } + + }); + sortables.shift() + }); + + return runtime.findNext(this) + }, + }; + return sortCmd; +}) diff --git a/components/web/static_files/js/_hyperscript/_hyperscript.template.js b/components/web/static_files/js/_hyperscript/_hyperscript.template.js new file mode 100644 index 0000000..a4e60ba --- /dev/null +++ b/components/web/static_files/js/_hyperscript/_hyperscript.template.js @@ -0,0 +1,86 @@ +(function (self, factory) { + const plugin = factory(self) + + if (typeof exports === 'object' && typeof exports['nodeName'] !== 'string') { + module.exports = plugin + } else { + if ('_hyperscript' in self) self._hyperscript.use(plugin) + } +})(typeof self !== 'undefined' ? self : this, self => { + + function compileTemplate(template) { + return template.replace(/(?:^|\n)([^@]*)@?/gm, function (match, p1) { + var templateStr = (" " + p1).replace(/([^\\])\$\{/g, "$1$${escape html ").substring(1); + return "\ncall meta.__ht_template_result.push(`" + templateStr + "`)\n"; + }); + } + + /** + * @param {HyperscriptObject} _hyperscript + */ + return _hyperscript => { + + function renderTemplate(template, ctx) { + var buf = []; + const renderCtx = Object.assign({}, ctx); + renderCtx.meta = Object.assign({ __ht_template_result: buf }, ctx.meta); + _hyperscript.evaluate(template, renderCtx); + return buf.join(""); + } + + _hyperscript.addCommand("render", function (parser, runtime, tokens) { + if (!tokens.matchToken("render")) return; + var template_ = parser.requireElement("expression", tokens); + var templateArgs = {}; + if (tokens.matchToken("with")) { + templateArgs = parser.parseElement("namedArgumentList", tokens); + } + return { + args: [template_, templateArgs], + op: function (ctx, template, templateArgs) { + if (!(template instanceof Element)) throw new Error(template_.sourceFor() + " is not an element"); + const context = _hyperscript.internals.runtime.makeContext() + context.locals = templateArgs; + ctx.result = renderTemplate(compileTemplate(template.innerHTML), context); + return runtime.findNext(this, ctx); + }, + }; + }); + + function escapeHTML(html) { + return String(html) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/\x22/g, """) + .replace(/\x27/g, "'"); + } + + _hyperscript.addLeafExpression("escape", function (parser, runtime, tokens) { + if (!tokens.matchToken("escape")) return; + var escapeType = tokens.matchTokenType("IDENTIFIER").value; + + // hidden! for use in templates + var unescaped = tokens.matchToken("unescaped"); + + var arg = parser.requireElement("expression", tokens); + + return { + args: [arg], + op: function (ctx, arg) { + if (unescaped) return arg; + if (arg === undefined) return ""; + switch (escapeType) { + case "html": + return escapeHTML(arg); + default: + throw new Error("Unknown escape: " + escapeType); + } + }, + evaluate: function (ctx) { + return runtime.unifiedEval(this, ctx); + }, + }; + }); + } +}) \ No newline at end of file diff --git a/components/web/static_files/js/htmx.org/htmx.org.js b/components/web/static_files/js/htmx.org/htmx.org.js new file mode 100644 index 0000000..370cc0f --- /dev/null +++ b/components/web/static_files/js/htmx.org/htmx.org.js @@ -0,0 +1,5261 @@ +var htmx = (function() { + 'use strict' + + // Public API + const htmx = { + // Tsc madness here, assigning the functions directly results in an invalid TypeScript output, but reassigning is fine + /* Event processing */ + /** @type {typeof onLoadHelper} */ + onLoad: null, + /** @type {typeof processNode} */ + process: null, + /** @type {typeof addEventListenerImpl} */ + on: null, + /** @type {typeof removeEventListenerImpl} */ + off: null, + /** @type {typeof triggerEvent} */ + trigger: null, + /** @type {typeof ajaxHelper} */ + ajax: null, + /* DOM querying helpers */ + /** @type {typeof find} */ + find: null, + /** @type {typeof findAll} */ + findAll: null, + /** @type {typeof closest} */ + closest: null, + /** + * Returns the input values that would resolve for a given element via the htmx value resolution mechanism + * + * @see https://htmx.org/api/#values + * + * @param {Element} elt the element to resolve values on + * @param {HttpVerb} type the request type (e.g. **get** or **post**) non-GET's will include the enclosing form of the element. Defaults to **post** + * @returns {Object} + */ + values: function(elt, type) { + const inputValues = getInputValues(elt, type || 'post') + return inputValues.values + }, + /* DOM manipulation helpers */ + /** @type {typeof removeElement} */ + remove: null, + /** @type {typeof addClassToElement} */ + addClass: null, + /** @type {typeof removeClassFromElement} */ + removeClass: null, + /** @type {typeof toggleClassOnElement} */ + toggleClass: null, + /** @type {typeof takeClassForElement} */ + takeClass: null, + /** @type {typeof swap} */ + swap: null, + /* Extension entrypoints */ + /** @type {typeof defineExtension} */ + defineExtension: null, + /** @type {typeof removeExtension} */ + removeExtension: null, + /* Debugging */ + /** @type {typeof logAll} */ + logAll: null, + /** @type {typeof logNone} */ + logNone: null, + /* Debugging */ + /** + * The logger htmx uses to log with + * + * @see https://htmx.org/api/#logger + */ + logger: null, + /** + * A property holding the configuration htmx uses at runtime. + * + * Note that using a [meta tag](https://htmx.org/docs/#config) is the preferred mechanism for setting these properties. + * + * @see https://htmx.org/api/#config + */ + config: { + /** + * Whether to use history. + * @type boolean + * @default true + */ + historyEnabled: true, + /** + * The number of pages to keep in **localStorage** for history support. + * @type number + * @default 10 + */ + historyCacheSize: 10, + /** + * @type boolean + * @default false + */ + refreshOnHistoryMiss: false, + /** + * The default swap style to use if **[hx-swap](https://htmx.org/attributes/hx-swap)** is omitted. + * @type HtmxSwapStyle + * @default 'innerHTML' + */ + defaultSwapStyle: 'innerHTML', + /** + * The default delay between receiving a response from the server and doing the swap. + * @type number + * @default 0 + */ + defaultSwapDelay: 0, + /** + * The default delay between completing the content swap and settling attributes. + * @type number + * @default 20 + */ + defaultSettleDelay: 20, + /** + * If true, htmx will inject a small amount of CSS into the page to make indicators invisible unless the **htmx-indicator** class is present. + * @type boolean + * @default true + */ + includeIndicatorStyles: true, + /** + * The class to place on indicators when a request is in flight. + * @type string + * @default 'htmx-indicator' + */ + indicatorClass: 'htmx-indicator', + /** + * The class to place on triggering elements when a request is in flight. + * @type string + * @default 'htmx-request' + */ + requestClass: 'htmx-request', + /** + * The class to temporarily place on elements that htmx has added to the DOM. + * @type string + * @default 'htmx-added' + */ + addedClass: 'htmx-added', + /** + * The class to place on target elements when htmx is in the settling phase. + * @type string + * @default 'htmx-settling' + */ + settlingClass: 'htmx-settling', + /** + * The class to place on target elements when htmx is in the swapping phase. + * @type string + * @default 'htmx-swapping' + */ + swappingClass: 'htmx-swapping', + /** + * Allows the use of eval-like functionality in htmx, to enable **hx-vars**, trigger conditions & script tag evaluation. Can be set to **false** for CSP compatibility. + * @type boolean + * @default true + */ + allowEval: true, + /** + * If set to false, disables the interpretation of script tags. + * @type boolean + * @default true + */ + allowScriptTags: true, + /** + * If set, the nonce will be added to inline scripts. + * @type string + * @default '' + */ + inlineScriptNonce: '', + /** + * If set, the nonce will be added to inline styles. + * @type string + * @default '' + */ + inlineStyleNonce: '', + /** + * The attributes to settle during the settling phase. + * @type string[] + * @default ['class', 'style', 'width', 'height'] + */ + attributesToSettle: ['class', 'style', 'width', 'height'], + /** + * Allow cross-site Access-Control requests using credentials such as cookies, authorization headers or TLS client certificates. + * @type boolean + * @default false + */ + withCredentials: false, + /** + * @type number + * @default 0 + */ + timeout: 0, + /** + * The default implementation of **getWebSocketReconnectDelay** for reconnecting after unexpected connection loss by the event code **Abnormal Closure**, **Service Restart** or **Try Again Later**. + * @type {'full-jitter' | ((retryCount:number) => number)} + * @default "full-jitter" + */ + wsReconnectDelay: 'full-jitter', + /** + * The type of binary data being received over the WebSocket connection + * @type BinaryType + * @default 'blob' + */ + wsBinaryType: 'blob', + /** + * @type string + * @default '[hx-disable], [data-hx-disable]' + */ + disableSelector: '[hx-disable], [data-hx-disable]', + /** + * @type {'auto' | 'instant' | 'smooth'} + * @default 'instant' + */ + scrollBehavior: 'instant', + /** + * If the focused element should be scrolled into view. + * @type boolean + * @default false + */ + defaultFocusScroll: false, + /** + * If set to true htmx will include a cache-busting parameter in GET requests to avoid caching partial responses by the browser + * @type boolean + * @default false + */ + getCacheBusterParam: false, + /** + * If set to true, htmx will use the View Transition API when swapping in new content. + * @type boolean + * @default false + */ + globalViewTransitions: false, + /** + * htmx will format requests with these methods by encoding their parameters in the URL, not the request body + * @type {(HttpVerb)[]} + * @default ['get', 'delete'] + */ + methodsThatUseUrlParams: ['get', 'delete'], + /** + * If set to true, disables htmx-based requests to non-origin hosts. + * @type boolean + * @default false + */ + selfRequestsOnly: true, + /** + * If set to true htmx will not update the title of the document when a title tag is found in new content + * @type boolean + * @default false + */ + ignoreTitle: false, + /** + * Whether the target of a boosted element is scrolled into the viewport. + * @type boolean + * @default true + */ + scrollIntoViewOnBoost: true, + /** + * The cache to store evaluated trigger specifications into. + * You may define a simple object to use a never-clearing cache, or implement your own system using a [proxy object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy) + * @type {Object|null} + * @default null + */ + triggerSpecsCache: null, + /** @type boolean */ + disableInheritance: false, + /** @type HtmxResponseHandlingConfig[] */ + responseHandling: [ + { code: '204', swap: false }, + { code: '[23]..', swap: true }, + { code: '[45]..', swap: false, error: true } + ], + /** + * Whether to process OOB swaps on elements that are nested within the main response element. + * @type boolean + * @default true + */ + allowNestedOobSwaps: true + }, + /** @type {typeof parseInterval} */ + parseInterval: null, + /** @type {typeof internalEval} */ + _: null, + version: '2.0.4' + } + // Tsc madness part 2 + htmx.onLoad = onLoadHelper + htmx.process = processNode + htmx.on = addEventListenerImpl + htmx.off = removeEventListenerImpl + htmx.trigger = triggerEvent + htmx.ajax = ajaxHelper + htmx.find = find + htmx.findAll = findAll + htmx.closest = closest + htmx.remove = removeElement + htmx.addClass = addClassToElement + htmx.removeClass = removeClassFromElement + htmx.toggleClass = toggleClassOnElement + htmx.takeClass = takeClassForElement + htmx.swap = swap + htmx.defineExtension = defineExtension + htmx.removeExtension = removeExtension + htmx.logAll = logAll + htmx.logNone = logNone + htmx.parseInterval = parseInterval + htmx._ = internalEval + + const internalAPI = { + addTriggerHandler, + bodyContains, + canAccessLocalStorage, + findThisElement, + filterValues, + swap, + hasAttribute, + getAttributeValue, + getClosestAttributeValue, + getClosestMatch, + getExpressionVars, + getHeaders, + getInputValues, + getInternalData, + getSwapSpecification, + getTriggerSpecs, + getTarget, + makeFragment, + mergeObjects, + makeSettleInfo, + oobSwap, + querySelectorExt, + settleImmediately, + shouldCancel, + triggerEvent, + triggerErrorEvent, + withExtensions + } + + const VERBS = ['get', 'post', 'put', 'delete', 'patch'] + const VERB_SELECTOR = VERBS.map(function(verb) { + return '[hx-' + verb + '], [data-hx-' + verb + ']' + }).join(', ') + + //= =================================================================== + // Utilities + //= =================================================================== + + /** + * Parses an interval string consistent with the way htmx does. Useful for plugins that have timing-related attributes. + * + * Caution: Accepts an int followed by either **s** or **ms**. All other values use **parseFloat** + * + * @see https://htmx.org/api/#parseInterval + * + * @param {string} str timing string + * @returns {number|undefined} + */ + function parseInterval(str) { + if (str == undefined) { + return undefined + } + + let interval = NaN + if (str.slice(-2) == 'ms') { + interval = parseFloat(str.slice(0, -2)) + } else if (str.slice(-1) == 's') { + interval = parseFloat(str.slice(0, -1)) * 1000 + } else if (str.slice(-1) == 'm') { + interval = parseFloat(str.slice(0, -1)) * 1000 * 60 + } else { + interval = parseFloat(str) + } + return isNaN(interval) ? undefined : interval + } + + /** + * @param {Node} elt + * @param {string} name + * @returns {(string | null)} + */ + function getRawAttribute(elt, name) { + return elt instanceof Element && elt.getAttribute(name) + } + + /** + * @param {Element} elt + * @param {string} qualifiedName + * @returns {boolean} + */ + // resolve with both hx and data-hx prefixes + function hasAttribute(elt, qualifiedName) { + return !!elt.hasAttribute && (elt.hasAttribute(qualifiedName) || + elt.hasAttribute('data-' + qualifiedName)) + } + + /** + * + * @param {Node} elt + * @param {string} qualifiedName + * @returns {(string | null)} + */ + function getAttributeValue(elt, qualifiedName) { + return getRawAttribute(elt, qualifiedName) || getRawAttribute(elt, 'data-' + qualifiedName) + } + + /** + * @param {Node} elt + * @returns {Node | null} + */ + function parentElt(elt) { + const parent = elt.parentElement + if (!parent && elt.parentNode instanceof ShadowRoot) return elt.parentNode + return parent + } + + /** + * @returns {Document} + */ + function getDocument() { + return document + } + + /** + * @param {Node} elt + * @param {boolean} global + * @returns {Node|Document} + */ + function getRootNode(elt, global) { + return elt.getRootNode ? elt.getRootNode({ composed: global }) : getDocument() + } + + /** + * @param {Node} elt + * @param {(e:Node) => boolean} condition + * @returns {Node | null} + */ + function getClosestMatch(elt, condition) { + while (elt && !condition(elt)) { + elt = parentElt(elt) + } + + return elt || null + } + + /** + * @param {Element} initialElement + * @param {Element} ancestor + * @param {string} attributeName + * @returns {string|null} + */ + function getAttributeValueWithDisinheritance(initialElement, ancestor, attributeName) { + const attributeValue = getAttributeValue(ancestor, attributeName) + const disinherit = getAttributeValue(ancestor, 'hx-disinherit') + var inherit = getAttributeValue(ancestor, 'hx-inherit') + if (initialElement !== ancestor) { + if (htmx.config.disableInheritance) { + if (inherit && (inherit === '*' || inherit.split(' ').indexOf(attributeName) >= 0)) { + return attributeValue + } else { + return null + } + } + if (disinherit && (disinherit === '*' || disinherit.split(' ').indexOf(attributeName) >= 0)) { + return 'unset' + } + } + return attributeValue + } + + /** + * @param {Element} elt + * @param {string} attributeName + * @returns {string | null} + */ + function getClosestAttributeValue(elt, attributeName) { + let closestAttr = null + getClosestMatch(elt, function(e) { + return !!(closestAttr = getAttributeValueWithDisinheritance(elt, asElement(e), attributeName)) + }) + if (closestAttr !== 'unset') { + return closestAttr + } + } + + /** + * @param {Node} elt + * @param {string} selector + * @returns {boolean} + */ + function matches(elt, selector) { + // @ts-ignore: non-standard properties for browser compatibility + // noinspection JSUnresolvedVariable + const matchesFunction = elt instanceof Element && (elt.matches || elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector || elt.webkitMatchesSelector || elt.oMatchesSelector) + return !!matchesFunction && matchesFunction.call(elt, selector) + } + + /** + * @param {string} str + * @returns {string} + */ + function getStartTag(str) { + const tagMatcher = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i + const match = tagMatcher.exec(str) + if (match) { + return match[1].toLowerCase() + } else { + return '' + } + } + + /** + * @param {string} resp + * @returns {Document} + */ + function parseHTML(resp) { + const parser = new DOMParser() + return parser.parseFromString(resp, 'text/html') + } + + /** + * @param {DocumentFragment} fragment + * @param {Node} elt + */ + function takeChildrenFor(fragment, elt) { + while (elt.childNodes.length > 0) { + fragment.append(elt.childNodes[0]) + } + } + + /** + * @param {HTMLScriptElement} script + * @returns {HTMLScriptElement} + */ + function duplicateScript(script) { + const newScript = getDocument().createElement('script') + forEach(script.attributes, function(attr) { + newScript.setAttribute(attr.name, attr.value) + }) + newScript.textContent = script.textContent + newScript.async = false + if (htmx.config.inlineScriptNonce) { + newScript.nonce = htmx.config.inlineScriptNonce + } + return newScript + } + + /** + * @param {HTMLScriptElement} script + * @returns {boolean} + */ + function isJavaScriptScriptNode(script) { + return script.matches('script') && (script.type === 'text/javascript' || script.type === 'module' || script.type === '') + } + + /** + * we have to make new copies of script tags that we are going to insert because + * SOME browsers (not saying who, but it involves an element and an animal) don't + * execute scripts created in