├── .gitignore
├── .pre-commit-config.yaml
├── Dockerfile
├── README.md
├── demo
├── __init__.py
├── asgi.py
├── models.py
├── notifications.py
├── settings.py
├── static
│ ├── bs
│ │ ├── css
│ │ │ ├── bootstrap-grid.css
│ │ │ ├── bootstrap-grid.css.map
│ │ │ ├── bootstrap-grid.min.css
│ │ │ ├── bootstrap-grid.min.css.map
│ │ │ ├── bootstrap-grid.rtl.css
│ │ │ ├── bootstrap-grid.rtl.css.map
│ │ │ ├── bootstrap-grid.rtl.min.css
│ │ │ ├── bootstrap-grid.rtl.min.css.map
│ │ │ ├── bootstrap-reboot.css
│ │ │ ├── bootstrap-reboot.css.map
│ │ │ ├── bootstrap-reboot.min.css
│ │ │ ├── bootstrap-reboot.min.css.map
│ │ │ ├── bootstrap-reboot.rtl.css
│ │ │ ├── bootstrap-reboot.rtl.css.map
│ │ │ ├── bootstrap-reboot.rtl.min.css
│ │ │ ├── bootstrap-reboot.rtl.min.css.map
│ │ │ ├── bootstrap-utilities.css
│ │ │ ├── bootstrap-utilities.css.map
│ │ │ ├── bootstrap-utilities.min.css
│ │ │ ├── bootstrap-utilities.min.css.map
│ │ │ ├── bootstrap-utilities.rtl.css
│ │ │ ├── bootstrap-utilities.rtl.css.map
│ │ │ ├── bootstrap-utilities.rtl.min.css
│ │ │ ├── bootstrap-utilities.rtl.min.css.map
│ │ │ ├── bootstrap.css
│ │ │ ├── bootstrap.css.map
│ │ │ ├── bootstrap.min.css
│ │ │ ├── bootstrap.min.css.map
│ │ │ ├── bootstrap.rtl.css
│ │ │ ├── bootstrap.rtl.css.map
│ │ │ ├── bootstrap.rtl.min.css
│ │ │ └── bootstrap.rtl.min.css.map
│ │ └── js
│ │ │ ├── bootstrap.bundle.js
│ │ │ ├── bootstrap.bundle.js.map
│ │ │ ├── bootstrap.bundle.min.js
│ │ │ ├── bootstrap.bundle.min.js.map
│ │ │ ├── bootstrap.esm.js
│ │ │ ├── bootstrap.esm.js.map
│ │ │ ├── bootstrap.esm.min.js
│ │ │ ├── bootstrap.esm.min.js.map
│ │ │ ├── bootstrap.js
│ │ │ ├── bootstrap.js.map
│ │ │ ├── bootstrap.min.js
│ │ │ └── bootstrap.min.js.map
│ ├── ext
│ │ ├── debug.js
│ │ └── sse.js
│ ├── htmx.min.js
│ ├── main.css
│ └── main.js
├── templates
│ └── demo
│ │ ├── _base.html
│ │ ├── index.html
│ │ ├── send_event.html
│ │ └── toast.html
├── urls.py
└── wsgi.py
├── manage.py
├── pdm.lock
└── pyproject.toml
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 | db.sqlite
64 | db.sqlite-journal
65 |
66 | # Flask stuff:
67 | instance/
68 | .webassets-cache
69 |
70 | # Scrapy stuff:
71 | .scrapy
72 |
73 | # Sphinx documentation
74 | docs/_build/
75 |
76 | # PyBuilder
77 | .pybuilder/
78 | target/
79 |
80 | # Jupyter Notebook
81 | .ipynb_checkpoints
82 |
83 | # IPython
84 | profile_default/
85 | ipython_config.py
86 |
87 | # pyenv
88 | # For a library or package, you might want to ignore these files since the code is
89 | # intended to run in multiple environments; otherwise, check them in:
90 | # .python-version
91 |
92 | # pipenv
93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
96 | # install all needed dependencies.
97 | #Pipfile.lock
98 |
99 | # poetry
100 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
101 | # This is especially recommended for binary packages to ensure reproducibility, and is more
102 | # commonly ignored for libraries.
103 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
104 | #poetry.lock
105 |
106 | # pdm
107 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
108 | #pdm.lock
109 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
110 | # in version control.
111 | # https://pdm.fming.dev/#use-with-ide
112 | .pdm.toml
113 |
114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
115 | __pypackages__/
116 |
117 | # Celery stuff
118 | celerybeat-schedule
119 | celerybeat.pid
120 |
121 | # SageMath parsed files
122 | *.sage.py
123 |
124 | # Environments
125 | .env
126 | .venv
127 | env/
128 | venv/
129 | ENV/
130 | env.bak/
131 | venv.bak/
132 |
133 | # Spyder project settings
134 | .spyderproject
135 | .spyproject
136 |
137 | # Rope project settings
138 | .ropeproject
139 |
140 | # mkdocs documentation
141 | /site
142 |
143 | # mypy
144 | .mypy_cache/
145 | .dmypy.json
146 | dmypy.json
147 |
148 | # Pyre type checker
149 | .pyre/
150 |
151 | # pytype static type analyzer
152 | .pytype/
153 |
154 | # Cython debug symbols
155 | cython_debug/
156 |
157 | # PyCharm
158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
160 | # and can be added to the global gitignore or merged into this file. For a more nuclear
161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
162 | #.idea/
163 | .pdm-python
164 |
165 | node_modules/
166 | .local/
167 | vite_dist/
168 | staticfiles/
169 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | default_language_version:
2 | python: python3.11
3 |
4 | repos:
5 | - repo: https://github.com/pre-commit/pre-commit-hooks
6 | rev: v4.4.0
7 | hooks:
8 | - id: check-added-large-files
9 | - id: check-case-conflict
10 | - id: check-json
11 | - id: check-merge-conflict
12 | - id: check-symlinks
13 | - id: check-toml
14 | - id: check-yaml
15 | - id: end-of-file-fixer
16 | - id: trailing-whitespace
17 | - repo: https://github.com/rtts/djhtml
18 | rev: '3.0.6'
19 | hooks:
20 | - id: djhtml
21 | entry: djhtml --tabwidth 4
22 | alias: autoformat
23 | # - id: djcss
24 | # alias: autoformat
25 | # - id: djjs
26 | # alias: autoformat
27 | - repo: https://github.com/adamchainz/django-upgrade
28 | rev: 1.13.0
29 | hooks:
30 | - id: django-upgrade
31 | args: [--target-version, "4.2"]
32 | alias: autoformat
33 | - repo: https://github.com/psf/black
34 | rev: 23.3.0
35 | hooks:
36 | - id: black
37 | alias: autoformat
38 | - repo: https://github.com/charliermarsh/ruff-pre-commit
39 | rev: v0.0.261
40 | hooks:
41 | - id: ruff
42 | alias: autoformat
43 | - repo: https://github.com/asottile/blacken-docs
44 | rev: 1.13.0
45 | hooks:
46 | - id: blacken-docs
47 | alias: autoformat
48 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # #######################################################################
2 | # Docker image definitions
3 | # #######################################################################
4 | ARG PYTHON_VERSION=3.11.4
5 |
6 | # #######################################################################
7 | # wheelbuilder - build wheels we need
8 | # #######################################################################
9 | # Intermediate target that builds any wheels we need. These are then
10 | # grabbed into the django image to be used in the virtualenv. We then
11 | # avoid installing any dev tools (gcc, etc) into our main docker image
12 | # This also eliminates most of the need to think about things like using
13 | # any -binary packages such as psycopg2-binary/psycopg[binary].
14 | # #######################################################################
15 | FROM python:${PYTHON_VERSION}-slim AS wheelbuilder
16 |
17 | RUN \
18 | apt-get update --yes --quiet \
19 | && apt-get dist-upgrade --yes \
20 | && apt-get install --yes --quiet --no-install-recommends git build-essential curl \
21 | && python -m pip install --user pipx \
22 | && python -m pipx ensurepath
23 |
24 | COPY pyproject.toml pdm.lock ./
25 |
26 | # We use pdm export, because it eliminates the need for pdm to be installed later at all. While it's useful for dev
27 | # environments, less packaging tools is better IMO.
28 | RUN \
29 | --mount=type=cache,id=pipcache,target=/root/.cache/pip \
30 | python -m pipx run pdm export -f requirements -o requirements.txt --prod \
31 | && python -m pip wheel --no-deps --no-input -r requirements.txt --wheel-dir /wheels
32 |
33 |
34 | # #######################################################################
35 | # basedjango - image for django
36 | # #######################################################################
37 | # Provides the base image for both dev and production deployment. Does not
38 | # set the USER, that's handled in the dev and prod images which inherit
39 | # this one.
40 | # #######################################################################
41 | FROM python:${PYTHON_VERSION}-slim AS django
42 |
43 | EXPOSE 8000/tcp
44 |
45 | RUN groupadd --gid 1181 --system django \
46 | && useradd --uid 1181 --system -g django --home /home/django django
47 |
48 | ENV PATH=$PATH:/home/django/.local/bin PYTHONPYCACHEPREFIX=/home/django/pycache
49 |
50 | RUN \
51 | apt-get update --yes --quiet \
52 | && apt-get install --yes --quiet --no-install-recommends git postgresql-client curl \
53 | && mkdir -p "$PYTHONPYCACHEPREFIX" \
54 | && mkdir /app \
55 | && chown django:django /app /home/django
56 |
57 | WORKDIR /app
58 |
59 | # Copy package spec lock file and install our packages
60 | COPY manage.py ./
61 |
62 | # The RUN command below temporarily mounts the wheels from wheelbuilder, so we don't have to
63 | # keep them in our image.
64 | RUN \
65 | --mount=type=cache,id=pipcache,target=/home/django/.cache/pip \
66 | --mount=type=bind,from=wheelbuilder,source=/wheels,target=/tmp/wheels,rw \
67 | python -m pip install --no-cache-dir --no-input /tmp/wheels/* \
68 | && mkdir -p /app/staticfiles /app/.local
69 |
70 | # manage.py for managing the django project, pyproject because we use some data (esp project.version)
71 | COPY pyproject.toml ./
72 |
73 | # Pretty much the whole app lives in one directory
74 | COPY demo ./demo
75 |
76 | # We use our own management command to launch the ASGI server.
77 | # It uses --insecure because we're likely running behind a traefik or similar SSL proxy
78 | CMD ["python", "manage.py", "runasgi", "--insecure", "--noreload"]
79 |
80 | RUN SECRET_KEY=notimportant ALLOWED_HOSTS='*' python ./manage.py collectstatic --noinput --clear --no-color
81 |
82 | RUN chown -R django:django /app /home/django
83 |
84 | USER django
85 |
86 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | htmx-notifications
2 | ------------------
3 |
4 | This project provides a sample django project called 'demo', designed to showcase some functionality from the DjangoCon AU
5 | talk "Using Django 4.2's StreamingHttpResponse and HTMX SSE to provide real time notifications".
6 |
7 | In particular, it demonstrates three different functionalities:
8 | 1. A demonstration of how StreamingHttpResponse can be used to deliver broadcast messages for a particular session.
9 | 2. A demonstration of how StreamingHttpResponse can be used with a simple model to keep track of notifications for a user, and see updates in real time.
10 | 3. A demonstration of how an existing StreamingHttpResponse can leverage multi-target htmx to update other things on the page, like a form when a notification is received.
11 |
12 | The project has been set up to also demonstrate several ways of handling enqueued messages:
13 | 1. Simplistic asyncio Queue(); only works in a single-threaded, single-process ASGI web server.
14 | 2. Redis pubsub; works with multiple processes, scales fairly well.
15 | 3. Postgres LISTEN/NOTIFY; works with multiple processes; does not scale very well unless you have a big PG server/bouncer.
16 | 4. DB model for notifications, with a cache flag to indicate when there is new data to send. Scales much better.
17 |
18 | Other challenges:
19 | - Every active user that leaves a page open maintains an SSE connection. There's no real way to detect that the page is
20 | inactive...so you can easily end up with too many connections open for all the (possibly idle) pages.
21 | - Some error handling is a good idea. You might even need/want some javascript, despite the use of HTMX.
22 | - Acknowledgement that a notification was rendered to the user, not just received over SSE but never displayed, is tricky. Despite the extra load, it might be worth POST-ing to indicate that the message is being received and rendered.
23 | - If you need to run under WSGI for some reason, you're (mostly?) out of luck.
24 | -
25 |
--------------------------------------------------------------------------------
/demo/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LucidDan/htmx-notifications/30bff0e22dc7e6a9388a37ef4b5175edd5ea7fd7/demo/__init__.py
--------------------------------------------------------------------------------
/demo/asgi.py:
--------------------------------------------------------------------------------
1 | """
2 | ASGI config for demo project.
3 |
4 | It exposes the ASGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.asgi import get_asgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings")
15 |
16 | application = get_asgi_application()
17 |
--------------------------------------------------------------------------------
/demo/models.py:
--------------------------------------------------------------------------------
1 | # from typing import Any
2 | #
3 | # from django.contrib.auth import get_user_model
4 | # from django.db import models
5 | # from django.db.models import TextChoices
6 | # from django.db.models.signals import post_save
7 | # from django.dispatch import receiver
8 | #
9 | #
10 | # User = get_user_model()
11 | #
12 | #
13 | # class UserProfile(models.Model):
14 | # user = models.OneToOneField(User, related_name="profile", on_delete=models.CASCADE, null=False)
15 | # location = models.CharField(max_length=64, blank=True)
16 | #
17 | #
18 | # @receiver(post_save, sender=User)
19 | # def create_user_profile(sender: Any, instance: User, created: bool, **kwargs) -> None:
20 | # if created or instance.profile is None:
21 | # UserProfile.objects.create(user=instance)
22 | # else:
23 | # instance.profile.save()
24 | #
25 | #
26 | # class Trip(models.Model):
27 | # name = models.CharField(max_length=255, unique=True)
28 | # location = models.CharField(max_length=64)
29 | # starts_on = models.DateField(null=False)
30 | # ends_on = models.DateField(null=False)
31 | #
32 | #
33 | # class Booking(models.Model):
34 | # trip = models.ForeignKey(Trip, on_delete=models.RESTRICT, null=False)
35 | # user_profile = models.ForeignKey(UserProfile, on_delete=models.RESTRICT, null=False)
36 | # state = models.CharField(max_length=32, blank=False, default="new", choices=[("new", "New"), ("ordered", "Ordered"), ("cancelled", "Cancelled"), ("paid", "Paid")])
37 | # room_type = models.CharField(max_length=32, blank=False, default="private", choices=[("shared", "Shared"), ("private", "Private"), ("ensuite", "Ensuite")])
38 | # arrives_on = models.DateField(null=False)
39 | # leaves_on = models.DateField(null=False)
40 | #
41 | #
42 | # class Notification(models.Model):
43 | # class NotificationState(TextChoices):
44 | # UNREAD = "unread"
45 | # READ = "read"
46 | # ARCHIVED = "archived"
47 | # TRASH = "trash"
48 | #
49 | # user_profile = models.ForeignKey(UserProfile, on_delete=models.RESTRICT, null=False)
50 | # state = models.CharField(max_length=32, choices=NotificationState.choices, default=NotificationState.UNREAD)
51 | # content = models.TextField()
52 |
--------------------------------------------------------------------------------
/demo/notifications.py:
--------------------------------------------------------------------------------
1 | """
2 | A small module handling notifications via redis pubsub.
3 | """
4 | import json
5 | from datetime import datetime
6 | from functools import cache
7 | import redis
8 | import redis.asyncio as aredis
9 | from django.conf import settings
10 | from django.utils.timezone import now
11 |
12 |
13 | @cache
14 | def get_async_client() -> aredis.Redis:
15 | return aredis.from_url(settings.REDIS_URL)
16 |
17 |
18 | @cache
19 | def get_client() -> redis.Redis:
20 | return redis.from_url(settings.REDIS_URL)
21 |
22 |
23 | def send_notification(event: str, subject: str, message: str, ts: datetime, template: str = "demo/toast.html",):
24 | get_client().publish(
25 | event,
26 | json.dumps({
27 | "template": template,
28 | "context": {
29 | "subject": subject,
30 | "message": message,
31 | "message_time": ts.timestamp(),
32 | },
33 | })
34 | )
35 |
--------------------------------------------------------------------------------
/demo/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Minimal settings for project 'demo'. This is NOT designed to be used in production.
3 | """
4 | from os import environ
5 | from pathlib import Path
6 | from django.utils.crypto import get_random_string
7 |
8 | BASE_DIR = Path(__file__).resolve().parent.parent
9 | DEBUG = (environ.get("DEBUG", "") == "1")
10 | INTERNAL_IPS = ["127.0.0.1", ]
11 | ALLOWED_HOSTS = ["*", ]
12 | ROOT_URLCONF = "demo.urls"
13 | SECRET_KEY = get_random_string(50)
14 | INSTALLED_APPS = [
15 | "daphne",
16 | # "django.contrib.admin",
17 | # "django.contrib.auth",
18 | # "django.contrib.contenttypes",
19 | # "django.contrib.sessions",
20 | # "django.contrib.messages",
21 | "django.contrib.staticfiles",
22 | "django_bootstrap5",
23 | "django_htmx",
24 | "demo",
25 | ]
26 | MIDDLEWARE = [
27 | "django.middleware.security.SecurityMiddleware",
28 | # "django.contrib.sessions.middleware.SessionMiddleware",
29 | "django.middleware.common.CommonMiddleware",
30 | "django.middleware.csrf.CsrfViewMiddleware",
31 | # "django.contrib.auth.middleware.AuthenticationMiddleware",
32 | "django_htmx.middleware.HtmxMiddleware",
33 | # "django.contrib.messages.middleware.MessageMiddleware",
34 | "django.middleware.clickjacking.XFrameOptionsMiddleware",
35 | ]
36 | TEMPLATES = [
37 | {
38 | "BACKEND": "django.template.backends.django.DjangoTemplates",
39 | "DIRS": [],
40 | "APP_DIRS": True,
41 | "OPTIONS": {
42 | "context_processors": [
43 | "django.template.context_processors.debug",
44 | "django.template.context_processors.request",
45 | # "django.contrib.auth.context_processors.auth",
46 | # "django.contrib.messages.context_processors.messages",
47 | ],
48 | },
49 | },
50 | ]
51 | ASGI_APPLICATION = "demo.asgi.application"
52 | REDIS_URL = "redis://localhost:6379"
53 | # Database
54 | DATABASES = {
55 | "default": {
56 | "ENGINE": "django.db.backends.sqlite3",
57 | "NAME": BASE_DIR / "db.sqlite3",
58 | }
59 | }
60 | USE_TZ = True
61 | TIME_ZONE = "UTC"
62 | STATIC_URL = "static/"
63 | STATIC_ROOT = BASE_DIR / "staticfiles"
64 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
65 | BOOTSTRAP5 = {
66 | }
67 | if DEBUG:
68 | BOOTSTRAP5["css_url"] = {
69 | "url": "/static/bs/css/bootstrap.min.css",
70 | }
71 | BOOTSTRAP5["javascript_url"] = {
72 | "url": "/static/bs/js/bootstrap.bundle.min.js",
73 | }
74 |
--------------------------------------------------------------------------------
/demo/static/bs/css/bootstrap-reboot.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v5.2.3 (https://getbootstrap.com/)
3 | * Copyright 2011-2022 The Bootstrap Authors
4 | * Copyright 2011-2022 Twitter, Inc.
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
6 | */
7 | :root {
8 | --bs-blue: #0d6efd;
9 | --bs-indigo: #6610f2;
10 | --bs-purple: #6f42c1;
11 | --bs-pink: #d63384;
12 | --bs-red: #dc3545;
13 | --bs-orange: #fd7e14;
14 | --bs-yellow: #ffc107;
15 | --bs-green: #198754;
16 | --bs-teal: #20c997;
17 | --bs-cyan: #0dcaf0;
18 | --bs-black: #000;
19 | --bs-white: #fff;
20 | --bs-gray: #6c757d;
21 | --bs-gray-dark: #343a40;
22 | --bs-gray-100: #f8f9fa;
23 | --bs-gray-200: #e9ecef;
24 | --bs-gray-300: #dee2e6;
25 | --bs-gray-400: #ced4da;
26 | --bs-gray-500: #adb5bd;
27 | --bs-gray-600: #6c757d;
28 | --bs-gray-700: #495057;
29 | --bs-gray-800: #343a40;
30 | --bs-gray-900: #212529;
31 | --bs-primary: #0d6efd;
32 | --bs-secondary: #6c757d;
33 | --bs-success: #198754;
34 | --bs-info: #0dcaf0;
35 | --bs-warning: #ffc107;
36 | --bs-danger: #dc3545;
37 | --bs-light: #f8f9fa;
38 | --bs-dark: #212529;
39 | --bs-primary-rgb: 13, 110, 253;
40 | --bs-secondary-rgb: 108, 117, 125;
41 | --bs-success-rgb: 25, 135, 84;
42 | --bs-info-rgb: 13, 202, 240;
43 | --bs-warning-rgb: 255, 193, 7;
44 | --bs-danger-rgb: 220, 53, 69;
45 | --bs-light-rgb: 248, 249, 250;
46 | --bs-dark-rgb: 33, 37, 41;
47 | --bs-white-rgb: 255, 255, 255;
48 | --bs-black-rgb: 0, 0, 0;
49 | --bs-body-color-rgb: 33, 37, 41;
50 | --bs-body-bg-rgb: 255, 255, 255;
51 | --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
52 | --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
53 | --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
54 | --bs-body-font-family: var(--bs-font-sans-serif);
55 | --bs-body-font-size: 1rem;
56 | --bs-body-font-weight: 400;
57 | --bs-body-line-height: 1.5;
58 | --bs-body-color: #212529;
59 | --bs-body-bg: #fff;
60 | --bs-border-width: 1px;
61 | --bs-border-style: solid;
62 | --bs-border-color: #dee2e6;
63 | --bs-border-color-translucent: rgba(0, 0, 0, 0.175);
64 | --bs-border-radius: 0.375rem;
65 | --bs-border-radius-sm: 0.25rem;
66 | --bs-border-radius-lg: 0.5rem;
67 | --bs-border-radius-xl: 1rem;
68 | --bs-border-radius-2xl: 2rem;
69 | --bs-border-radius-pill: 50rem;
70 | --bs-link-color: #0d6efd;
71 | --bs-link-hover-color: #0a58ca;
72 | --bs-code-color: #d63384;
73 | --bs-highlight-bg: #fff3cd;
74 | }
75 |
76 | *,
77 | *::before,
78 | *::after {
79 | box-sizing: border-box;
80 | }
81 |
82 | @media (prefers-reduced-motion: no-preference) {
83 | :root {
84 | scroll-behavior: smooth;
85 | }
86 | }
87 |
88 | body {
89 | margin: 0;
90 | font-family: var(--bs-body-font-family);
91 | font-size: var(--bs-body-font-size);
92 | font-weight: var(--bs-body-font-weight);
93 | line-height: var(--bs-body-line-height);
94 | color: var(--bs-body-color);
95 | text-align: var(--bs-body-text-align);
96 | background-color: var(--bs-body-bg);
97 | -webkit-text-size-adjust: 100%;
98 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
99 | }
100 |
101 | hr {
102 | margin: 1rem 0;
103 | color: inherit;
104 | border: 0;
105 | border-top: 1px solid;
106 | opacity: 0.25;
107 | }
108 |
109 | h6, h5, h4, h3, h2, h1 {
110 | margin-top: 0;
111 | margin-bottom: 0.5rem;
112 | font-weight: 500;
113 | line-height: 1.2;
114 | }
115 |
116 | h1 {
117 | font-size: calc(1.375rem + 1.5vw);
118 | }
119 | @media (min-width: 1200px) {
120 | h1 {
121 | font-size: 2.5rem;
122 | }
123 | }
124 |
125 | h2 {
126 | font-size: calc(1.325rem + 0.9vw);
127 | }
128 | @media (min-width: 1200px) {
129 | h2 {
130 | font-size: 2rem;
131 | }
132 | }
133 |
134 | h3 {
135 | font-size: calc(1.3rem + 0.6vw);
136 | }
137 | @media (min-width: 1200px) {
138 | h3 {
139 | font-size: 1.75rem;
140 | }
141 | }
142 |
143 | h4 {
144 | font-size: calc(1.275rem + 0.3vw);
145 | }
146 | @media (min-width: 1200px) {
147 | h4 {
148 | font-size: 1.5rem;
149 | }
150 | }
151 |
152 | h5 {
153 | font-size: 1.25rem;
154 | }
155 |
156 | h6 {
157 | font-size: 1rem;
158 | }
159 |
160 | p {
161 | margin-top: 0;
162 | margin-bottom: 1rem;
163 | }
164 |
165 | abbr[title] {
166 | -webkit-text-decoration: underline dotted;
167 | text-decoration: underline dotted;
168 | cursor: help;
169 | -webkit-text-decoration-skip-ink: none;
170 | text-decoration-skip-ink: none;
171 | }
172 |
173 | address {
174 | margin-bottom: 1rem;
175 | font-style: normal;
176 | line-height: inherit;
177 | }
178 |
179 | ol,
180 | ul {
181 | padding-left: 2rem;
182 | }
183 |
184 | ol,
185 | ul,
186 | dl {
187 | margin-top: 0;
188 | margin-bottom: 1rem;
189 | }
190 |
191 | ol ol,
192 | ul ul,
193 | ol ul,
194 | ul ol {
195 | margin-bottom: 0;
196 | }
197 |
198 | dt {
199 | font-weight: 700;
200 | }
201 |
202 | dd {
203 | margin-bottom: 0.5rem;
204 | margin-left: 0;
205 | }
206 |
207 | blockquote {
208 | margin: 0 0 1rem;
209 | }
210 |
211 | b,
212 | strong {
213 | font-weight: bolder;
214 | }
215 |
216 | small {
217 | font-size: 0.875em;
218 | }
219 |
220 | mark {
221 | padding: 0.1875em;
222 | background-color: var(--bs-highlight-bg);
223 | }
224 |
225 | sub,
226 | sup {
227 | position: relative;
228 | font-size: 0.75em;
229 | line-height: 0;
230 | vertical-align: baseline;
231 | }
232 |
233 | sub {
234 | bottom: -0.25em;
235 | }
236 |
237 | sup {
238 | top: -0.5em;
239 | }
240 |
241 | a {
242 | color: var(--bs-link-color);
243 | text-decoration: underline;
244 | }
245 | a:hover {
246 | color: var(--bs-link-hover-color);
247 | }
248 |
249 | a:not([href]):not([class]), a:not([href]):not([class]):hover {
250 | color: inherit;
251 | text-decoration: none;
252 | }
253 |
254 | pre,
255 | code,
256 | kbd,
257 | samp {
258 | font-family: var(--bs-font-monospace);
259 | font-size: 1em;
260 | }
261 |
262 | pre {
263 | display: block;
264 | margin-top: 0;
265 | margin-bottom: 1rem;
266 | overflow: auto;
267 | font-size: 0.875em;
268 | }
269 | pre code {
270 | font-size: inherit;
271 | color: inherit;
272 | word-break: normal;
273 | }
274 |
275 | code {
276 | font-size: 0.875em;
277 | color: var(--bs-code-color);
278 | word-wrap: break-word;
279 | }
280 | a > code {
281 | color: inherit;
282 | }
283 |
284 | kbd {
285 | padding: 0.1875rem 0.375rem;
286 | font-size: 0.875em;
287 | color: var(--bs-body-bg);
288 | background-color: var(--bs-body-color);
289 | border-radius: 0.25rem;
290 | }
291 | kbd kbd {
292 | padding: 0;
293 | font-size: 1em;
294 | }
295 |
296 | figure {
297 | margin: 0 0 1rem;
298 | }
299 |
300 | img,
301 | svg {
302 | vertical-align: middle;
303 | }
304 |
305 | table {
306 | caption-side: bottom;
307 | border-collapse: collapse;
308 | }
309 |
310 | caption {
311 | padding-top: 0.5rem;
312 | padding-bottom: 0.5rem;
313 | color: #6c757d;
314 | text-align: left;
315 | }
316 |
317 | th {
318 | text-align: inherit;
319 | text-align: -webkit-match-parent;
320 | }
321 |
322 | thead,
323 | tbody,
324 | tfoot,
325 | tr,
326 | td,
327 | th {
328 | border-color: inherit;
329 | border-style: solid;
330 | border-width: 0;
331 | }
332 |
333 | label {
334 | display: inline-block;
335 | }
336 |
337 | button {
338 | border-radius: 0;
339 | }
340 |
341 | button:focus:not(:focus-visible) {
342 | outline: 0;
343 | }
344 |
345 | input,
346 | button,
347 | select,
348 | optgroup,
349 | textarea {
350 | margin: 0;
351 | font-family: inherit;
352 | font-size: inherit;
353 | line-height: inherit;
354 | }
355 |
356 | button,
357 | select {
358 | text-transform: none;
359 | }
360 |
361 | [role=button] {
362 | cursor: pointer;
363 | }
364 |
365 | select {
366 | word-wrap: normal;
367 | }
368 | select:disabled {
369 | opacity: 1;
370 | }
371 |
372 | [list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
373 | display: none !important;
374 | }
375 |
376 | button,
377 | [type=button],
378 | [type=reset],
379 | [type=submit] {
380 | -webkit-appearance: button;
381 | }
382 | button:not(:disabled),
383 | [type=button]:not(:disabled),
384 | [type=reset]:not(:disabled),
385 | [type=submit]:not(:disabled) {
386 | cursor: pointer;
387 | }
388 |
389 | ::-moz-focus-inner {
390 | padding: 0;
391 | border-style: none;
392 | }
393 |
394 | textarea {
395 | resize: vertical;
396 | }
397 |
398 | fieldset {
399 | min-width: 0;
400 | padding: 0;
401 | margin: 0;
402 | border: 0;
403 | }
404 |
405 | legend {
406 | float: left;
407 | width: 100%;
408 | padding: 0;
409 | margin-bottom: 0.5rem;
410 | font-size: calc(1.275rem + 0.3vw);
411 | line-height: inherit;
412 | }
413 | @media (min-width: 1200px) {
414 | legend {
415 | font-size: 1.5rem;
416 | }
417 | }
418 | legend + * {
419 | clear: left;
420 | }
421 |
422 | ::-webkit-datetime-edit-fields-wrapper,
423 | ::-webkit-datetime-edit-text,
424 | ::-webkit-datetime-edit-minute,
425 | ::-webkit-datetime-edit-hour-field,
426 | ::-webkit-datetime-edit-day-field,
427 | ::-webkit-datetime-edit-month-field,
428 | ::-webkit-datetime-edit-year-field {
429 | padding: 0;
430 | }
431 |
432 | ::-webkit-inner-spin-button {
433 | height: auto;
434 | }
435 |
436 | [type=search] {
437 | outline-offset: -2px;
438 | -webkit-appearance: textfield;
439 | }
440 |
441 | /* rtl:raw:
442 | [type="tel"],
443 | [type="url"],
444 | [type="email"],
445 | [type="number"] {
446 | direction: ltr;
447 | }
448 | */
449 | ::-webkit-search-decoration {
450 | -webkit-appearance: none;
451 | }
452 |
453 | ::-webkit-color-swatch-wrapper {
454 | padding: 0;
455 | }
456 |
457 | ::-webkit-file-upload-button {
458 | font: inherit;
459 | -webkit-appearance: button;
460 | }
461 |
462 | ::file-selector-button {
463 | font: inherit;
464 | -webkit-appearance: button;
465 | }
466 |
467 | output {
468 | display: inline-block;
469 | }
470 |
471 | iframe {
472 | border: 0;
473 | }
474 |
475 | summary {
476 | display: list-item;
477 | cursor: pointer;
478 | }
479 |
480 | progress {
481 | vertical-align: baseline;
482 | }
483 |
484 | [hidden] {
485 | display: none !important;
486 | }
487 |
488 | /*# sourceMappingURL=bootstrap-reboot.css.map */
--------------------------------------------------------------------------------
/demo/static/bs/css/bootstrap-reboot.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v5.2.3 (https://getbootstrap.com/)
3 | * Copyright 2011-2022 The Bootstrap Authors
4 | * Copyright 2011-2022 Twitter, Inc.
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
6 | */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-2xl:2rem;--bs-border-radius-pill:50rem;--bs-link-color:#0d6efd;--bs-link-hover-color:#0a58ca;--bs-code-color:#d63384;--bs-highlight-bg:#fff3cd}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:1px solid;opacity:.25}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.1875em;background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:var(--bs-link-color);text-decoration:underline}a:hover{color:var(--bs-link-hover-color)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
7 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */
--------------------------------------------------------------------------------
/demo/static/bs/css/bootstrap-reboot.min.css.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["../../scss/mixins/_banner.scss","../../scss/_root.scss","../../scss/vendor/_rfs.scss","../../scss/_reboot.scss","dist/css/bootstrap-reboot.css","../../scss/mixins/_border-radius.scss"],"names":[],"mappings":"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","sourcesContent":["@mixin bsBanner($file) {\n /*!\n * Bootstrap #{$file} v5.2.3 (https://getbootstrap.com/)\n * Copyright 2011-2022 The Bootstrap Authors\n * Copyright 2011-2022 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n}\n\n",":root {\n // Note: Custom variable values only support SassScript inside `#{}`.\n\n // Colors\n //\n // Generate palettes for full colors, grays, and theme colors.\n\n @each $color, $value in $colors {\n --#{$prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $grays {\n --#{$prefix}gray-#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors-rgb {\n --#{$prefix}#{$color}-rgb: #{$value};\n }\n\n --#{$prefix}white-rgb: #{to-rgb($white)};\n --#{$prefix}black-rgb: #{to-rgb($black)};\n --#{$prefix}body-color-rgb: #{to-rgb($body-color)};\n --#{$prefix}body-bg-rgb: #{to-rgb($body-bg)};\n\n // Fonts\n\n // Note: Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --#{$prefix}font-sans-serif: #{inspect($font-family-sans-serif)};\n --#{$prefix}font-monospace: #{inspect($font-family-monospace)};\n --#{$prefix}gradient: #{$gradient};\n\n // Root and body\n // scss-docs-start root-body-variables\n @if $font-size-root != null {\n --#{$prefix}root-font-size: #{$font-size-root};\n }\n --#{$prefix}body-font-family: #{$font-family-base};\n @include rfs($font-size-base, --#{$prefix}body-font-size);\n --#{$prefix}body-font-weight: #{$font-weight-base};\n --#{$prefix}body-line-height: #{$line-height-base};\n --#{$prefix}body-color: #{$body-color};\n @if $body-text-align != null {\n --#{$prefix}body-text-align: #{$body-text-align};\n }\n --#{$prefix}body-bg: #{$body-bg};\n // scss-docs-end root-body-variables\n\n // scss-docs-start root-border-var\n --#{$prefix}border-width: #{$border-width};\n --#{$prefix}border-style: #{$border-style};\n --#{$prefix}border-color: #{$border-color};\n --#{$prefix}border-color-translucent: #{$border-color-translucent};\n\n --#{$prefix}border-radius: #{$border-radius};\n --#{$prefix}border-radius-sm: #{$border-radius-sm};\n --#{$prefix}border-radius-lg: #{$border-radius-lg};\n --#{$prefix}border-radius-xl: #{$border-radius-xl};\n --#{$prefix}border-radius-2xl: #{$border-radius-2xl};\n --#{$prefix}border-radius-pill: #{$border-radius-pill};\n // scss-docs-end root-border-var\n\n --#{$prefix}link-color: #{$link-color};\n --#{$prefix}link-hover-color: #{$link-hover-color};\n\n --#{$prefix}code-color: #{$code-color};\n\n --#{$prefix}highlight-bg: #{$mark-bg};\n}\n","// stylelint-disable property-blacklist, scss/dollar-variable-default\n\n// SCSS RFS mixin\n//\n// Automated responsive values for font sizes, paddings, margins and much more\n//\n// Licensed under MIT (https://github.com/twbs/rfs/blob/main/LICENSE)\n\n// Configuration\n\n// Base value\n$rfs-base-value: 1.25rem !default;\n$rfs-unit: rem !default;\n\n@if $rfs-unit != rem and $rfs-unit != px {\n @error \"`#{$rfs-unit}` is not a valid unit for $rfs-unit. Use `px` or `rem`.\";\n}\n\n// Breakpoint at where values start decreasing if screen width is smaller\n$rfs-breakpoint: 1200px !default;\n$rfs-breakpoint-unit: px !default;\n\n@if $rfs-breakpoint-unit != px and $rfs-breakpoint-unit != em and $rfs-breakpoint-unit != rem {\n @error \"`#{$rfs-breakpoint-unit}` is not a valid unit for $rfs-breakpoint-unit. Use `px`, `em` or `rem`.\";\n}\n\n// Resize values based on screen height and width\n$rfs-two-dimensional: false !default;\n\n// Factor of decrease\n$rfs-factor: 10 !default;\n\n@if type-of($rfs-factor) != number or $rfs-factor <= 1 {\n @error \"`#{$rfs-factor}` is not a valid $rfs-factor, it must be greater than 1.\";\n}\n\n// Mode. Possibilities: \"min-media-query\", \"max-media-query\"\n$rfs-mode: min-media-query !default;\n\n// Generate enable or disable classes. Possibilities: false, \"enable\" or \"disable\"\n$rfs-class: false !default;\n\n// 1 rem = $rfs-rem-value px\n$rfs-rem-value: 16 !default;\n\n// Safari iframe resize bug: https://github.com/twbs/rfs/issues/14\n$rfs-safari-iframe-resize-bug-fix: false !default;\n\n// Disable RFS by setting $enable-rfs to false\n$enable-rfs: true !default;\n\n// Cache $rfs-base-value unit\n$rfs-base-value-unit: unit($rfs-base-value);\n\n@function divide($dividend, $divisor, $precision: 10) {\n $sign: if($dividend > 0 and $divisor > 0 or $dividend < 0 and $divisor < 0, 1, -1);\n $dividend: abs($dividend);\n $divisor: abs($divisor);\n @if $dividend == 0 {\n @return 0;\n }\n @if $divisor == 0 {\n @error \"Cannot divide by 0\";\n }\n $remainder: $dividend;\n $result: 0;\n $factor: 10;\n @while ($remainder > 0 and $precision >= 0) {\n $quotient: 0;\n @while ($remainder >= $divisor) {\n $remainder: $remainder - $divisor;\n $quotient: $quotient + 1;\n }\n $result: $result * 10 + $quotient;\n $factor: $factor * .1;\n $remainder: $remainder * 10;\n $precision: $precision - 1;\n @if ($precision < 0 and $remainder >= $divisor * 5) {\n $result: $result + 1;\n }\n }\n $result: $result * $factor * $sign;\n $dividend-unit: unit($dividend);\n $divisor-unit: unit($divisor);\n $unit-map: (\n \"px\": 1px,\n \"rem\": 1rem,\n \"em\": 1em,\n \"%\": 1%\n );\n @if ($dividend-unit != $divisor-unit and map-has-key($unit-map, $dividend-unit)) {\n $result: $result * map-get($unit-map, $dividend-unit);\n }\n @return $result;\n}\n\n// Remove px-unit from $rfs-base-value for calculations\n@if $rfs-base-value-unit == px {\n $rfs-base-value: divide($rfs-base-value, $rfs-base-value * 0 + 1);\n}\n@else if $rfs-base-value-unit == rem {\n $rfs-base-value: divide($rfs-base-value, divide($rfs-base-value * 0 + 1, $rfs-rem-value));\n}\n\n// Cache $rfs-breakpoint unit to prevent multiple calls\n$rfs-breakpoint-unit-cache: unit($rfs-breakpoint);\n\n// Remove unit from $rfs-breakpoint for calculations\n@if $rfs-breakpoint-unit-cache == px {\n $rfs-breakpoint: divide($rfs-breakpoint, $rfs-breakpoint * 0 + 1);\n}\n@else if $rfs-breakpoint-unit-cache == rem or $rfs-breakpoint-unit-cache == \"em\" {\n $rfs-breakpoint: divide($rfs-breakpoint, divide($rfs-breakpoint * 0 + 1, $rfs-rem-value));\n}\n\n// Calculate the media query value\n$rfs-mq-value: if($rfs-breakpoint-unit == px, #{$rfs-breakpoint}px, #{divide($rfs-breakpoint, $rfs-rem-value)}#{$rfs-breakpoint-unit});\n$rfs-mq-property-width: if($rfs-mode == max-media-query, max-width, min-width);\n$rfs-mq-property-height: if($rfs-mode == max-media-query, max-height, min-height);\n\n// Internal mixin used to determine which media query needs to be used\n@mixin _rfs-media-query {\n @if $rfs-two-dimensional {\n @if $rfs-mode == max-media-query {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}), (#{$rfs-mq-property-height}: #{$rfs-mq-value}) {\n @content;\n }\n }\n @else {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}) and (#{$rfs-mq-property-height}: #{$rfs-mq-value}) {\n @content;\n }\n }\n }\n @else {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}) {\n @content;\n }\n }\n}\n\n// Internal mixin that adds disable classes to the selector if needed.\n@mixin _rfs-rule {\n @if $rfs-class == disable and $rfs-mode == max-media-query {\n // Adding an extra class increases specificity, which prevents the media query to override the property\n &,\n .disable-rfs &,\n &.disable-rfs {\n @content;\n }\n }\n @else if $rfs-class == enable and $rfs-mode == min-media-query {\n .enable-rfs &,\n &.enable-rfs {\n @content;\n }\n }\n @else {\n @content;\n }\n}\n\n// Internal mixin that adds enable classes to the selector if needed.\n@mixin _rfs-media-query-rule {\n\n @if $rfs-class == enable {\n @if $rfs-mode == min-media-query {\n @content;\n }\n\n @include _rfs-media-query {\n .enable-rfs &,\n &.enable-rfs {\n @content;\n }\n }\n }\n @else {\n @if $rfs-class == disable and $rfs-mode == min-media-query {\n .disable-rfs &,\n &.disable-rfs {\n @content;\n }\n }\n @include _rfs-media-query {\n @content;\n }\n }\n}\n\n// Helper function to get the formatted non-responsive value\n@function rfs-value($values) {\n // Convert to list\n $values: if(type-of($values) != list, ($values,), $values);\n\n $val: '';\n\n // Loop over each value and calculate value\n @each $value in $values {\n @if $value == 0 {\n $val: $val + ' 0';\n }\n @else {\n // Cache $value unit\n $unit: if(type-of($value) == \"number\", unit($value), false);\n\n @if $unit == px {\n // Convert to rem if needed\n $val: $val + ' ' + if($rfs-unit == rem, #{divide($value, $value * 0 + $rfs-rem-value)}rem, $value);\n }\n @else if $unit == rem {\n // Convert to px if needed\n $val: $val + ' ' + if($rfs-unit == px, #{divide($value, $value * 0 + 1) * $rfs-rem-value}px, $value);\n }\n @else {\n // If $value isn't a number (like inherit) or $value has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n $val: $val + ' ' + $value;\n }\n }\n }\n\n // Remove first space\n @return unquote(str-slice($val, 2));\n}\n\n// Helper function to get the responsive value calculated by RFS\n@function rfs-fluid-value($values) {\n // Convert to list\n $values: if(type-of($values) != list, ($values,), $values);\n\n $val: '';\n\n // Loop over each value and calculate value\n @each $value in $values {\n @if $value == 0 {\n $val: $val + ' 0';\n }\n\n @else {\n // Cache $value unit\n $unit: if(type-of($value) == \"number\", unit($value), false);\n\n // If $value isn't a number (like inherit) or $value has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n @if not $unit or $unit != px and $unit != rem {\n $val: $val + ' ' + $value;\n }\n\n @else {\n // Remove unit from $value for calculations\n $value: divide($value, $value * 0 + if($unit == px, 1, divide(1, $rfs-rem-value)));\n\n // Only add the media query if the value is greater than the minimum value\n @if abs($value) <= $rfs-base-value or not $enable-rfs {\n $val: $val + ' ' + if($rfs-unit == rem, #{divide($value, $rfs-rem-value)}rem, #{$value}px);\n }\n @else {\n // Calculate the minimum value\n $value-min: $rfs-base-value + divide(abs($value) - $rfs-base-value, $rfs-factor);\n\n // Calculate difference between $value and the minimum value\n $value-diff: abs($value) - $value-min;\n\n // Base value formatting\n $min-width: if($rfs-unit == rem, #{divide($value-min, $rfs-rem-value)}rem, #{$value-min}px);\n\n // Use negative value if needed\n $min-width: if($value < 0, -$min-width, $min-width);\n\n // Use `vmin` if two-dimensional is enabled\n $variable-unit: if($rfs-two-dimensional, vmin, vw);\n\n // Calculate the variable width between 0 and $rfs-breakpoint\n $variable-width: #{divide($value-diff * 100, $rfs-breakpoint)}#{$variable-unit};\n\n // Return the calculated value\n $val: $val + ' calc(' + $min-width + if($value < 0, ' - ', ' + ') + $variable-width + ')';\n }\n }\n }\n }\n\n // Remove first space\n @return unquote(str-slice($val, 2));\n}\n\n// RFS mixin\n@mixin rfs($values, $property: font-size) {\n @if $values != null {\n $val: rfs-value($values);\n $fluidVal: rfs-fluid-value($values);\n\n // Do not print the media query if responsive & non-responsive values are the same\n @if $val == $fluidVal {\n #{$property}: $val;\n }\n @else {\n @include _rfs-rule {\n #{$property}: if($rfs-mode == max-media-query, $val, $fluidVal);\n\n // Include safari iframe resize fix if needed\n min-width: if($rfs-safari-iframe-resize-bug-fix, (0 * 1vw), null);\n }\n\n @include _rfs-media-query-rule {\n #{$property}: if($rfs-mode == max-media-query, $fluidVal, $val);\n }\n }\n }\n}\n\n// Shorthand helper mixins\n@mixin font-size($value) {\n @include rfs($value);\n}\n\n@mixin padding($value) {\n @include rfs($value, padding);\n}\n\n@mixin padding-top($value) {\n @include rfs($value, padding-top);\n}\n\n@mixin padding-right($value) {\n @include rfs($value, padding-right);\n}\n\n@mixin padding-bottom($value) {\n @include rfs($value, padding-bottom);\n}\n\n@mixin padding-left($value) {\n @include rfs($value, padding-left);\n}\n\n@mixin margin($value) {\n @include rfs($value, margin);\n}\n\n@mixin margin-top($value) {\n @include rfs($value, margin-top);\n}\n\n@mixin margin-right($value) {\n @include rfs($value, margin-right);\n}\n\n@mixin margin-bottom($value) {\n @include rfs($value, margin-bottom);\n}\n\n@mixin margin-left($value) {\n @include rfs($value, margin-left);\n}\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n\n// Root\n//\n// Ability to the value of the root font sizes, affecting the value of `rem`.\n// null by default, thus nothing is generated.\n\n:root {\n @if $font-size-root != null {\n @include font-size(var(--#{$prefix}root-font-size));\n }\n\n @if $enable-smooth-scroll {\n @media (prefers-reduced-motion: no-preference) {\n scroll-behavior: smooth;\n }\n }\n}\n\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Prevent adjustments of font size after orientation changes in iOS.\n// 4. Change the default tap highlight to be completely transparent in iOS.\n\n// scss-docs-start reboot-body-rules\nbody {\n margin: 0; // 1\n font-family: var(--#{$prefix}body-font-family);\n @include font-size(var(--#{$prefix}body-font-size));\n font-weight: var(--#{$prefix}body-font-weight);\n line-height: var(--#{$prefix}body-line-height);\n color: var(--#{$prefix}body-color);\n text-align: var(--#{$prefix}body-text-align);\n background-color: var(--#{$prefix}body-bg); // 2\n -webkit-text-size-adjust: 100%; // 3\n -webkit-tap-highlight-color: rgba($black, 0); // 4\n}\n// scss-docs-end reboot-body-rules\n\n\n// Content grouping\n//\n// 1. Reset Firefox's gray color\n\nhr {\n margin: $hr-margin-y 0;\n color: $hr-color; // 1\n border: 0;\n border-top: $hr-border-width solid $hr-border-color;\n opacity: $hr-opacity;\n}\n\n\n// Typography\n//\n// 1. Remove top margins from headings\n// By default, `
`-`
` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n\n%heading {\n margin-top: 0; // 1\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-style: $headings-font-style;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: $headings-color;\n}\n\nh1 {\n @extend %heading;\n @include font-size($h1-font-size);\n}\n\nh2 {\n @extend %heading;\n @include font-size($h2-font-size);\n}\n\nh3 {\n @extend %heading;\n @include font-size($h3-font-size);\n}\n\nh4 {\n @extend %heading;\n @include font-size($h4-font-size);\n}\n\nh5 {\n @extend %heading;\n @include font-size($h5-font-size);\n}\n\nh6 {\n @extend %heading;\n @include font-size($h6-font-size);\n}\n\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `
`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\n\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n\n// Abbreviations\n//\n// 1. Add the correct text decoration in Chrome, Edge, Opera, and Safari.\n// 2. Add explicit cursor to indicate changed behavior.\n// 3. Prevent the text-decoration to be skipped.\n\nabbr[title] {\n text-decoration: underline dotted; // 1\n cursor: help; // 2\n text-decoration-skip-ink: none; // 3\n}\n\n\n// Address\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\n\n// Lists\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\n// 1. Undo browser default\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // 1\n}\n\n\n// Blockquote\n\nblockquote {\n margin: 0 0 1rem;\n}\n\n\n// Strong\n//\n// Add the correct font weight in Chrome, Edge, and Safari\n\nb,\nstrong {\n font-weight: $font-weight-bolder;\n}\n\n\n// Small\n//\n// Add the correct font size in all browsers\n\nsmall {\n @include font-size($small-font-size);\n}\n\n\n// Mark\n\nmark {\n padding: $mark-padding;\n background-color: var(--#{$prefix}highlight-bg);\n}\n\n\n// Sub and Sup\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n\nsub,\nsup {\n position: relative;\n @include font-size($sub-sup-font-size);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n// Links\n\na {\n color: var(--#{$prefix}link-color);\n text-decoration: $link-decoration;\n\n &:hover {\n color: var(--#{$prefix}link-hover-color);\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n &,\n &:hover {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n// Code\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-code;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\n// 1. Remove browser default top margin\n// 2. Reset browser default of `1em` to use `rem`s\n// 3. Don't allow content to break outside\n\npre {\n display: block;\n margin-top: 0; // 1\n margin-bottom: 1rem; // 2\n overflow: auto; // 3\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\ncode {\n @include font-size($code-font-size);\n color: var(--#{$prefix}code-color);\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n\n kbd {\n padding: 0;\n @include font-size(1em);\n font-weight: $nested-kbd-font-weight;\n }\n}\n\n\n// Figures\n//\n// Apply a consistent margin strategy (matches our type styles).\n\nfigure {\n margin: 0 0 1rem;\n}\n\n\n// Images and content\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\n\n// Tables\n//\n// Prevent double borders\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: $table-cell-padding-y;\n padding-bottom: $table-cell-padding-y;\n color: $table-caption-color;\n text-align: left;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `
` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-`