diff --git a/components/cluster/cli.py b/components/cluster/cli.py
index 193f18d..8a37137 100644
--- a/components/cluster/cli.py
+++ b/components/cluster/cli.py
@@ -27,23 +27,26 @@ async def cli_processor(streams: tuple[asyncio.StreamReader, asyncio.StreamWrite
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
+ tokens = (
+ IN_MEMORY_DB["TOKENS"]["LOGIN"] | IN_MEMORY_DB["TOKENS"]["REGISTER"]
+ )
+ for idx, (k, v) in enumerate(tokens.items(), start=1):
+ awaiting[idx] = (k, v["intention"])
+
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}
- )
+ if confirmed in IN_MEMORY_DB["TOKENS"]["LOGIN"]:
+ IN_MEMORY_DB["TOKENS"]["LOGIN"].get(confirmed, {}).update(
+ {"status": "confirmed", "code": code}
+ )
+ elif confirmed in IN_MEMORY_DB["TOKENS"]["REGISTER"]:
+ IN_MEMORY_DB["TOKENS"]["REGISTER"].get(confirmed, {}).update(
+ {"status": "confirmed", "code": code}
+ )
writer.write(f"{code}\n".encode("ascii"))
await writer.drain()
except Exception as e:
diff --git a/components/web/__init__.py b/components/web/__init__.py
index e12268f..c54017d 100644
--- a/components/web/__init__.py
+++ b/components/web/__init__.py
@@ -23,6 +23,7 @@ app.register_blueprint(objects.blueprint)
app.register_blueprint(profile.blueprint)
app.register_blueprint(system.blueprint)
app.register_blueprint(users.blueprint)
+app.register_blueprint(groups.blueprint)
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = defaults.SEND_FILE_MAX_AGE_DEFAULT
app.config["SECRET_KEY"] = defaults.SECRET_KEY
@@ -35,6 +36,10 @@ 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()
+IN_MEMORY_DB["TOKENS"] = {
+ "REGISTER": dict(),
+ "LOGIN": dict(),
+}
modifying_request_limiter = asyncio.Semaphore(app.config["MOD_REQ_LIMIT"])
diff --git a/components/web/blueprints/__init__.py b/components/web/blueprints/__init__.py
index f786e6a..a1a7d36 100644
--- a/components/web/blueprints/__init__.py
+++ b/components/web/blueprints/__init__.py
@@ -4,3 +4,4 @@ from components.web.blueprints import profile
from components.web.blueprints import root
from components.web.blueprints import system
from components.web.blueprints import users
+from components.web.blueprints import groups
diff --git a/components/web/blueprints/auth.py b/components/web/blueprints/auth.py
index fa2d15a..02ef80d 100644
--- a/components/web/blueprints/auth.py
+++ b/components/web/blueprints/auth.py
@@ -51,11 +51,13 @@ async def login_request_confirm(request_token: str):
except:
return "", 200, {"HX-Redirect": "/"}
- token_status = IN_MEMORY_DB.get(request_token, {}).get("status")
+ token_status = IN_MEMORY_DB["TOKENS"]["LOGIN"].get(request_token, {}).get("status")
if token_status == "awaiting":
session["request_token"] = request_token
- requested_login = IN_MEMORY_DB[request_token]["requested_login"]
+ requested_login = IN_MEMORY_DB["TOKENS"]["LOGIN"][request_token][
+ "requested_login"
+ ]
return await render_template(
"auth/login/request/confirm.html",
@@ -87,10 +89,10 @@ async def login_request_confirm_modal(request_token: str):
if request.method == "POST":
if (
- request_token in IN_MEMORY_DB
- and IN_MEMORY_DB[request_token]["status"] == "awaiting"
+ request_token in IN_MEMORY_DB["TOKENS"]["LOGIN"]
+ and IN_MEMORY_DB["TOKENS"]["LOGIN"][request_token]["status"] == "awaiting"
):
- IN_MEMORY_DB[request_token].update(
+ IN_MEMORY_DB["TOKENS"]["LOGIN"][request_token].update(
{
"status": "confirmed",
"credential_id": "",
@@ -98,7 +100,7 @@ async def login_request_confirm_modal(request_token: str):
)
current_app.add_background_task(
expire_key,
- IN_MEMORY_DB,
+ IN_MEMORY_DB["TOKENS"]["LOGIN"],
request_token,
10,
)
@@ -136,16 +138,16 @@ async def login_request_start():
request_token = token_urlsafe()
- IN_MEMORY_DB[request_token] = {
+ IN_MEMORY_DB["TOKENS"]["LOGIN"][request_token] = {
"intention": f"Authenticate user: {request_data.login}",
+ "created": utc_now_as_str(),
"status": "awaiting",
- "token_type": "web_confirmation",
"requested_login": request_data.login,
}
current_app.add_background_task(
expire_key,
- IN_MEMORY_DB,
+ IN_MEMORY_DB["TOKENS"]["LOGIN"],
request_token,
defaults.AUTH_REQUEST_TIMEOUT,
)
@@ -177,7 +179,7 @@ async def login_request_check(request_token: str):
return "", 200, {"HX-Redirect": "/"}
token_status, requested_login, credential_id = map(
- IN_MEMORY_DB.get(request_token, {}).get,
+ IN_MEMORY_DB["TOKENS"]["LOGIN"].get(request_token, {}).get,
["status", "requested_login", "credential_id"],
)
@@ -216,15 +218,15 @@ async def login_token():
try:
request_data = AuthToken.parse_obj(request.form_parsed)
token = request_data.token
- IN_MEMORY_DB[token] = {
+ IN_MEMORY_DB["TOKENS"]["LOGIN"][token] = {
"intention": f"Authenticate user: {request_data.login}",
+ "created": utc_now_as_str(),
"status": "awaiting",
- "token_type": "cli_confirmation",
"login": request_data.login,
}
current_app.add_background_task(
expire_key,
- IN_MEMORY_DB,
+ IN_MEMORY_DB["TOKENS"]["LOGIN"],
token,
120,
)
@@ -244,10 +246,10 @@ async def login_token_verify():
request_data = TokenConfirmation.parse_obj(request.form_parsed)
token_status, token_login, token_confirmation_code = map(
- IN_MEMORY_DB.get(request_data.token, {}).get,
+ IN_MEMORY_DB["TOKENS"]["LOGIN"].get(request_data.token, {}).get,
["status", "login", "code"],
)
- IN_MEMORY_DB.pop(request_data.token, None)
+ IN_MEMORY_DB["TOKENS"]["LOGIN"].pop(request_data.token, None)
if (
token_status != "confirmed"
@@ -328,17 +330,17 @@ async def register_token():
try:
request_data = AuthToken.parse_obj(request.form_parsed)
token = request_data.token
- IN_MEMORY_DB[token] = {
+ IN_MEMORY_DB["TOKENS"]["REGISTER"][token] = {
"intention": f"Register user: {request_data.login}",
+ "created": utc_now_as_str(),
"status": "awaiting",
- "token_type": "cli_confirmation",
"login": request_data.login,
}
current_app.add_background_task(
expire_key,
- IN_MEMORY_DB,
+ IN_MEMORY_DB["TOKENS"]["REGISTER"],
token,
- 120,
+ defaults.REGISTER_REQUEST_TIMEOUT,
)
await ws_htmx(
"_system",
@@ -365,10 +367,10 @@ async def register_webauthn_options():
return validation_error(e.errors())
token_status, token_login, token_confirmation_code = map(
- IN_MEMORY_DB.get(request_data.token, {}).get,
+ IN_MEMORY_DB["TOKENS"]["REGISTER"].get(request_data.token, {}).get,
["status", "login", "code"],
)
- IN_MEMORY_DB.pop(request_data.token, None)
+ IN_MEMORY_DB["TOKENS"]["REGISTER"].pop(request_data.token, None)
if (
token_status != "confirmed"
@@ -614,7 +616,7 @@ async def auth_login_verify():
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(
+ IN_MEMORY_DB["TOKENS"]["LOGIN"][request_token].update(
{
"status": "confirmed",
"credential_id": credential.raw_id.hex(),
@@ -622,7 +624,7 @@ async def auth_login_verify():
)
current_app.add_background_task(
expire_key,
- IN_MEMORY_DB,
+ IN_MEMORY_DB["TOKENS"]["LOGIN"],
request_token,
10,
)
diff --git a/components/web/blueprints/groups.py b/components/web/blueprints/groups.py
new file mode 100644
index 0000000..a70e3ff
--- /dev/null
+++ b/components/web/blueprints/groups.py
@@ -0,0 +1,65 @@
+import components.users
+from components.models.users import UserGroups
+from components.web.utils import *
+
+
+blueprint = Blueprint("groups", __name__, url_prefix="/system/groups")
+
+
+@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("/", 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")
+@formoptions(["users"])
+async def get_groups():
+ return await render_template("system/groups.html", data={})
diff --git a/components/web/blueprints/users.py b/components/web/blueprints/users.py
index a4b3afe..b0c2438 100644
--- a/components/web/blueprints/users.py
+++ b/components/web/blueprints/users.py
@@ -1,7 +1,6 @@
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 *
@@ -17,49 +16,6 @@ def load_context():
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):
@@ -128,7 +84,9 @@ async def get_users():
},
)
else:
- return await render_template("system/users.html", data={})
+ return await render_template(
+ "system/users.html", data={"tokens": IN_MEMORY_DB["TOKENS"]}
+ )
@blueprint.route("/delete", methods=["POST"])
diff --git a/components/web/static_files/css/pico-custom.css b/components/web/static_files/css/pico-custom.css
index 72b0b42..6c9fe89 100644
--- a/components/web/static_files/css/pico-custom.css
+++ b/components/web/static_files/css/pico-custom.css
@@ -1,12 +1,13 @@
@charset "UTF-8";
/*!
- * Pico CSS ✨ v2.0.6 (https://picocss.com)
- * Copyright 2019-2024 - Licensed under MIT
+ * Pico CSS ✨ v2.1.1 (https://picocss.com)
+ * Copyright 2019-2025 - Licensed under MIT
*/
/**
* Styles
*/
-:root {
+:root,
+:host {
--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);
@@ -271,12 +272,14 @@ details summary[role=button]:not(.outline)::after {
* Color schemes
*/
[data-theme=light],
-:root:not([data-theme=dark]) {
+:root:not([data-theme=dark]),
+:host(:not([data-theme=dark])) {
+ color-scheme: light;
--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-muted-border-color: rgb(231, 234, 239.5);
--pico-primary: #5d6b89;
--pico-primary-background: #525f7a;
--pico-primary-border: var(--pico-primary-background);
@@ -314,21 +317,21 @@ details summary[role=button]:not(.outline)::after {
--pico-h4-color: #4d535e;
--pico-h5-color: #5c6370;
--pico-h6-color: #646b79;
- --pico-mark-background-color: #fde7c0;
+ --pico-mark-background-color: rgb(252.5, 230.5, 191.5);
--pico-mark-color: #0f1114;
- --pico-ins-color: #1d6a54;
- --pico-del-color: #883935;
+ --pico-ins-color: rgb(28.5, 105.5, 84);
+ --pico-del-color: rgb(136, 56.5, 53);
--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-background-color: rgb(243, 244.5, 246.75);
--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-background-color: rgb(251, 251.5, 252.25);
--pico-form-element-selected-background-color: #dfe3eb;
--pico-form-element-border-color: #cfd5e2;
--pico-form-element-color: #23262c;
@@ -337,11 +340,11 @@ details summary[role=button]:not(.outline)::after {
--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-border-color: rgb(183.5, 105.5, 106.5);
+ --pico-form-element-invalid-active-border-color: rgb(200.25, 79.25, 72.25);
--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-border-color: rgb(76, 154.5, 137.5);
+ --pico-form-element-valid-active-border-color: rgb(39, 152.75, 118.75);
--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);
@@ -359,7 +362,7 @@ details summary[role=button]:not(.outline)::after {
--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-card-sectioning-background-color: rgb(251, 251.5, 252.25);
--pico-dropdown-background-color: #fff;
--pico-dropdown-border-color: #eff1f4;
--pico-dropdown-box-shadow: var(--pico-box-shadow);
@@ -371,9 +374,8 @@ details summary[role=button]:not(.outline)::after {
--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;
+ --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, 154.5, 137.5)' 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.25, 79.25, 72.25)' 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");
}
[data-theme=light] input:is([type=submit],
[type=button],
@@ -386,13 +388,21 @@ details summary[role=button]:not(.outline)::after {
[type=reset],
[type=checkbox],
[type=radio],
+[type=file]),
+:host(: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;
+ :root:not([data-theme]),
+ :host(:not([data-theme])) {
+ color-scheme: dark;
+ --pico-background-color: rgb(19, 22.5, 30.5);
--pico-color: #c2c7d0;
--pico-text-selection-color: rgba(144, 158, 190, 0.1875);
--pico-muted-color: #7b8495;
@@ -427,7 +437,7 @@ details summary[role=button]:not(.outline)::after {
--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-box-shadow: 0.0145rem 0.029rem 0.174rem rgba(7, 8.5, 12, 0.01698), 0.0335rem 0.067rem 0.402rem rgba(7, 8.5, 12, 0.024), 0.0625rem 0.125rem 0.75rem rgba(7, 8.5, 12, 0.03), 0.1125rem 0.225rem 1.35rem rgba(7, 8.5, 12, 0.036), 0.2085rem 0.417rem 2.502rem rgba(7, 8.5, 12, 0.04302), 0.5rem 1rem 6rem rgba(7, 8.5, 12, 0.06), 0 0 0 0.0625rem rgba(7, 8.5, 12, 0.015);
--pico-h1-color: #f0f1f3;
--pico-h2-color: #e0e3e7;
--pico-h3-color: #c2c7d0;
@@ -437,31 +447,31 @@ details summary[role=button]:not(.outline)::after {
--pico-mark-background-color: #014063;
--pico-mark-color: #fff;
--pico-ins-color: #62af9a;
- --pico-del-color: #ce7e7b;
+ --pico-del-color: rgb(205.5, 126, 123);
--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-background-color: rgb(26, 30.5, 40.25);
--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-background-color: rgb(28, 33, 43.5);
--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-background-color: rgb(26, 30.5, 40.25);
--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-border-color: rgb(149.5, 74, 80);
+ --pico-form-element-invalid-active-border-color: rgb(183.25, 63.5, 59);
--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-active-border-color: rgb(22, 137, 105.5);
--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);
@@ -479,43 +489,55 @@ details summary[role=button]:not(.outline)::after {
--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-card-sectioning-background-color: rgb(26, 30.5, 40.25);
--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-modal-overlay-background-color: rgba(7.5, 8.5, 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;
+ --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(149.5, 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");
}
:root:not([data-theme]) input:is([type=submit],
[type=button],
[type=reset],
[type=checkbox],
[type=radio],
+ [type=file]),
+ :host(: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 {
+ :root:not([data-theme]) details summary[role=button].contrast:not(.outline)::after,
+ :host(: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,
+ :host(: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;
+ color-scheme: dark;
+ --pico-background-color: rgb(19, 22.5, 30.5);
--pico-color: #c2c7d0;
--pico-text-selection-color: rgba(144, 158, 190, 0.1875);
--pico-muted-color: #7b8495;
@@ -550,7 +572,7 @@ details summary[role=button]:not(.outline)::after {
--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-box-shadow: 0.0145rem 0.029rem 0.174rem rgba(7, 8.5, 12, 0.01698), 0.0335rem 0.067rem 0.402rem rgba(7, 8.5, 12, 0.024), 0.0625rem 0.125rem 0.75rem rgba(7, 8.5, 12, 0.03), 0.1125rem 0.225rem 1.35rem rgba(7, 8.5, 12, 0.036), 0.2085rem 0.417rem 2.502rem rgba(7, 8.5, 12, 0.04302), 0.5rem 1rem 6rem rgba(7, 8.5, 12, 0.06), 0 0 0 0.0625rem rgba(7, 8.5, 12, 0.015);
--pico-h1-color: #f0f1f3;
--pico-h2-color: #e0e3e7;
--pico-h3-color: #c2c7d0;
@@ -560,31 +582,31 @@ details summary[role=button]:not(.outline)::after {
--pico-mark-background-color: #014063;
--pico-mark-color: #fff;
--pico-ins-color: #62af9a;
- --pico-del-color: #ce7e7b;
+ --pico-del-color: rgb(205.5, 126, 123);
--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-background-color: rgb(26, 30.5, 40.25);
--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-background-color: rgb(28, 33, 43.5);
--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-background-color: rgb(26, 30.5, 40.25);
--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-border-color: rgb(149.5, 74, 80);
+ --pico-form-element-invalid-active-border-color: rgb(183.25, 63.5, 59);
--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-active-border-color: rgb(22, 137, 105.5);
--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);
@@ -602,21 +624,20 @@ details summary[role=button]:not(.outline)::after {
--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-card-sectioning-background-color: rgb(26, 30.5, 40.25);
--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-modal-overlay-background-color: rgba(7.5, 8.5, 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;
+ --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(149.5, 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");
}
[data-theme=dark] input:is([type=submit],
[type=button],
@@ -661,7 +682,8 @@ progress,
vertical-align: inherit;
}
-:where(:root) {
+:where(:root),
+:where(:host) {
-webkit-tap-highlight-color: transparent;
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
@@ -1229,7 +1251,8 @@ img {
fill: currentColor;
}
-svg:not(:root) {
+svg:not(:root),
+svg:not(:host) {
overflow: hidden;
}
@@ -1244,7 +1267,8 @@ samp {
font-family: var(--pico-font-family);
}
-pre code {
+pre code,
+pre samp {
font-size: inherit;
font-family: inherit;
}
@@ -1256,7 +1280,8 @@ pre {
pre,
code,
-kbd {
+kbd,
+samp {
border-radius: var(--pico-border-radius);
background: var(--pico-code-background-color);
color: var(--pico-code-color);
@@ -1265,7 +1290,8 @@ kbd {
}
code,
-kbd {
+kbd,
+samp {
display: inline-block;
padding: 0.375rem;
}
@@ -1275,7 +1301,8 @@ pre {
margin-bottom: var(--pico-spacing);
overflow-x: auto;
}
-pre > code {
+pre > code,
+pre > samp {
display: block;
padding: var(--pico-spacing);
background: none;
@@ -1302,7 +1329,7 @@ figure figcaption {
}
/**
- * Miscs
+ * Misc
*/
hr {
height: 0;
@@ -2059,7 +2086,7 @@ details.dropdown {
position: relative;
border-bottom: none;
}
-details.dropdown summary::after,
+details.dropdown > summary::after,
details.dropdown > button::after,
details.dropdown > a::after {
display: block;
@@ -2079,7 +2106,7 @@ nav details.dropdown {
margin-bottom: 0;
}
-details.dropdown summary:not([role]) {
+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);
@@ -2091,22 +2118,22 @@ details.dropdown summary:not([role]) {
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 {
+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 {
+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 {
+details.dropdown > summary:not([role]):focus-visible {
outline: none;
}
-details.dropdown summary:not([role])[aria-invalid=false] {
+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] {
+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);
@@ -2116,18 +2143,18 @@ nav details.dropdown {
display: inline;
margin: calc(var(--pico-nav-element-spacing-vertical) * -1) 0;
}
-nav details.dropdown summary::after {
+nav details.dropdown > summary::after {
transform: rotate(0deg) translateX(0rem);
}
-nav details.dropdown summary:not([role]) {
+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 {
+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 {
+details.dropdown > summary + ul {
display: flex;
z-index: 99;
position: absolute;
@@ -2147,23 +2174,23 @@ details.dropdown summary + ul {
opacity: 0;
transition: opacity var(--pico-transition), transform 0s ease-in-out 1s;
}
-details.dropdown summary + ul[dir=rtl] {
+details.dropdown > summary + ul[dir=rtl] {
right: 0;
left: auto;
}
-details.dropdown summary + ul li {
+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 {
+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 {
+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 {
+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);
@@ -2173,27 +2200,27 @@ details.dropdown summary + ul li a {
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]) {
+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 {
+details.dropdown > summary + ul li label {
width: 100%;
}
-details.dropdown summary + ul li:has(label):hover {
+details.dropdown > summary + ul li:has(label):hover {
background-color: var(--pico-dropdown-hover-background-color);
}
-details.dropdown[open] summary {
+details.dropdown[open] > summary {
margin-bottom: 0;
}
-details.dropdown[open] summary + ul {
+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 {
+details.dropdown[open] > summary::before {
display: block;
z-index: 1;
position: fixed;
@@ -2340,10 +2367,10 @@ label > details.dropdown {
/**
* Loading ([aria-busy=true])
*/
-[aria-busy=true]:not(input, select, textarea, html) {
+[aria-busy=true]:not(input, select, textarea, html, form) {
white-space: nowrap;
}
-[aria-busy=true]:not(input, select, textarea, html)::before {
+[aria-busy=true]:not(input, select, textarea, html, form)::before {
display: inline-block;
width: 1em;
height: 1em;
@@ -2353,10 +2380,10 @@ label > details.dropdown {
content: "";
vertical-align: -0.125em;
}
-[aria-busy=true]:not(input, select, textarea, html):not(:empty)::before {
+[aria-busy=true]:not(input, select, textarea, html, form):not(:empty)::before {
margin-inline-end: calc(var(--pico-spacing) * 0.5);
}
-[aria-busy=true]:not(input, select, textarea, html):empty {
+[aria-busy=true]:not(input, select, textarea, html, form):empty {
text-align: center;
}
@@ -2372,7 +2399,8 @@ a[aria-busy=true] {
/**
* Modal (
-
-
- @set hidden to "hidden" unless members is empty
+ @if members is not empty or newGroup
+ @set hidden to `hidden`
+ @end
-