From 36e606693b0b134ffda0606172c1a2bfe30120be Mon Sep 17 00:00:00 2001 From: apeters Date: Fri, 23 May 2025 08:47:10 +0000 Subject: [PATCH] Users & Groups fixes/changes Signed-off-by: apeters --- components/cluster/cli.py | 25 +- components/web/__init__.py | 5 + components/web/blueprints/__init__.py | 1 + components/web/blueprints/auth.py | 48 ++-- components/web/blueprints/groups.py | 65 +++++ components/web/blueprints/users.py | 48 +--- .../web/static_files/css/pico-custom.css | 253 ++++++++++-------- .../web/static_files/css/pico-custom.scss | 40 +-- components/web/templates/includes/menu.html | 3 + components/web/templates/system/groups.html | 49 ++++ .../system/includes/users/groups.html | 45 ++-- .../system/includes/users/pending_tokens.html | 31 +++ .../templates/system/includes/users/row.html | 2 +- components/web/templates/system/users.html | 19 +- config/defaults.py | 1 + ctrl | 2 +- 16 files changed, 397 insertions(+), 240 deletions(-) create mode 100644 components/web/blueprints/groups.py create mode 100644 components/web/templates/system/groups.html create mode 100644 components/web/templates/system/includes/users/pending_tokens.html 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 () */ -:root { +:root, +:host { --pico-scrollbar-width: 0px; } @@ -2396,43 +2424,43 @@ dialog { background-color: var(--pico-modal-overlay-background-color); color: var(--pico-color); } -dialog article { +dialog > article { width: 100%; max-height: calc(100vh - var(--pico-spacing) * 2); margin: var(--pico-spacing); overflow: auto; } @media (min-width: 576px) { - dialog article { + dialog > article { max-width: 95%; } } @media (min-width: 768px) { - dialog article { + dialog > article { max-width: 95%; } } -dialog article > header > * { +dialog > article > header > * { margin-bottom: 0; } -dialog article > header .close, dialog article > header :is(a, button)[rel=prev] { +dialog > article > header .close, dialog > article > header :is(a, button)[rel=prev] { margin: 0; margin-left: var(--pico-spacing); padding: 0; float: right; } -dialog article > footer { +dialog > article > footer { text-align: right; } -dialog article > footer button, -dialog article > footer [role=button] { +dialog > article > footer button, +dialog > article > footer [role=button] { margin-bottom: 0; } -dialog article > footer button:not(:first-of-type), -dialog article > footer [role=button]:not(:first-of-type) { +dialog > article > footer button:not(:first-of-type), +dialog > article > footer [role=button]:not(:first-of-type) { margin-left: calc(var(--pico-spacing) * 0.5); } -dialog article .close, dialog article :is(a, button)[rel=prev] { +dialog > article .close, dialog > article :is(a, button)[rel=prev] { display: block; width: 1rem; height: 1rem; @@ -2448,7 +2476,7 @@ dialog article .close, dialog article :is(a, button)[rel=prev] { opacity: 0.5; transition: opacity var(--pico-transition); } -dialog article .close:is([aria-current]:not([aria-current=false]), :hover, :active, :focus), dialog article :is(a, button)[rel=prev]:is([aria-current]:not([aria-current=false]), :hover, :active, :focus) { +dialog > article .close:is([aria-current]:not([aria-current=false]), :hover, :active, :focus), dialog > article :is(a, button)[rel=prev]:is([aria-current]:not([aria-current=false]), :hover, :active, :focus) { opacity: 1; } dialog:not([open]), dialog[open=false] { @@ -2671,7 +2699,7 @@ progress::-moz-progress-bar { [data-tooltip] { position: relative; } -[data-tooltip]:not(a, button, input) { +[data-tooltip]:not(a, button, input, [role=button]) { border-bottom: 1px dotted; text-decoration: none; cursor: help; @@ -3220,6 +3248,28 @@ fieldset.vault-unlock { padding: var(--pico-spacing) 0; } +article.user-group { + background-color: var(--pico-form-element-background-color); +} + +hgroup ol, hgroup ul { + margin-block: calc(var(--pico-spacing) / 2); +} + +@media (max-width: 1536px) { + .grid-auto-cols { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: calc(var(--pico-spacing) / 2); + } +} +@media (max-width: 1280px) { + .grid-auto-cols { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: calc(var(--pico-spacing) / 2); + } +} @media (max-width: 1024px) { .split-grid.grid { gap: calc(var(--pico-spacing) / 2); @@ -3231,17 +3281,9 @@ fieldset.vault-unlock { } .grid-auto-cols { display: grid; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(2, 1fr); gap: calc(var(--pico-spacing) / 2); } - .grid-auto-cols .grid-span-all { - grid-column: 1/-1; - } - .grid-auto-cols > article { - margin-bottom: 0; - padding-bottom: var(--pico-form-element-spacing-vertical); - --pico-border-radius: .5rem; - } th, td { --pico-spacing: 0.75rem; } @@ -3260,13 +3302,6 @@ fieldset.vault-unlock { grid-template-columns: repeat(1, 1fr); gap: calc(var(--pico-spacing) / 2); } - .grid-auto-cols .grid-span-all { - grid-column: 1/-1; - } - .grid-auto-cols > article { - margin-bottom: 0; - --pico-border-radius: .5rem; - } } .user-group { opacity: 0; diff --git a/components/web/static_files/css/pico-custom.scss b/components/web/static_files/css/pico-custom.scss index cfc53a2..7604592 100644 --- a/components/web/static_files/css/pico-custom.scss +++ b/components/web/static_files/css/pico-custom.scss @@ -101,6 +101,8 @@ $sm-breakpoint: map-get(map-get($breakpoints, sm), breakpoint); $md-breakpoint: map-get(map-get($breakpoints, md), breakpoint); $lg-breakpoint: map-get(map-get($breakpoints, lg), breakpoint); +$xl-breakpoint: map-get(map-get($breakpoints, xl), breakpoint); +$xxl-breakpoint: map-get(map-get($breakpoints, xxl), breakpoint); small { font-size: .875rem; @@ -353,9 +355,27 @@ fieldset.vault-unlock { } article.user-group { - + background-color: var(--pico-form-element-background-color); } +hgroup ol, hgroup ul { + margin-block: calc(var(--pico-spacing)/2); +} + +@media (max-width: $xxl-breakpoint) { + .grid-auto-cols { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: calc(var(--pico-spacing)/2); + } +} +@media (max-width: $xl-breakpoint) { + .grid-auto-cols { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: calc(var(--pico-spacing)/2); + } +} @media (max-width: $lg-breakpoint) { .split-grid.grid { gap: calc(var(--pico-spacing)/2); @@ -367,17 +387,9 @@ article.user-group { } .grid-auto-cols { display: grid; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(2, 1fr); gap: calc(var(--pico-spacing)/2); } - .grid-auto-cols .grid-span-all { - grid-column: 1 / -1; - } - .grid-auto-cols > article { - margin-bottom: 0; - padding-bottom: var(--pico-form-element-spacing-vertical); - --pico-border-radius: .5rem; - } th, td { --pico-spacing: 0.75rem; } @@ -396,13 +408,6 @@ article.user-group { grid-template-columns: repeat(1, 1fr); gap: calc(var(--pico-spacing)/2); } - .grid-auto-cols .grid-span-all { - grid-column: 1 / -1; - } - .grid-auto-cols > article { - margin-bottom: 0; - --pico-border-radius: .5rem; - } } .user-group { @@ -504,3 +509,4 @@ fieldset.keypair, fieldset.tresor { } } } + diff --git a/components/web/templates/includes/menu.html b/components/web/templates/includes/menu.html index f593bcc..803d6f0 100644 --- a/components/web/templates/includes/menu.html +++ b/components/web/templates/includes/menu.html @@ -51,6 +51,9 @@
  • Users
  • +
  • + Groups +
  • Logs
  • diff --git a/components/web/templates/system/groups.html b/components/web/templates/system/groups.html new file mode 100644 index 0000000..5d9825a --- /dev/null +++ b/components/web/templates/system/groups.html @@ -0,0 +1,49 @@ +{% if not request.headers.get("Hx-Request") %} + {% extends "base.html" %} +{% endif %} + +{% block breadcrumb %} + +{% endblock breadcrumb %} + +{% block body %} + +

    Groups

    + +
    +
    +
    +
    Groups object
    +

    Create an object by defining a unique name.

    +
    +
    +
    + + +
    +
    +
    +
    + {% include "system/includes/users/groups.html" %} +
    +
    + + +{% endblock body %} diff --git a/components/web/templates/system/includes/users/groups.html b/components/web/templates/system/includes/users/groups.html index 2611746..e87ff60 100644 --- a/components/web/templates/system/includes/users/groups.html +++ b/components/web/templates/system/includes/users/groups.html @@ -31,12 +31,14 @@ set group to closest .user-group to event.target set value of <[name=name]/> in group to value of <[name=new_name]/> in group if <[name=members] option:checked/> in group is empty + log event.deta add @hidden to group settle remove group trigger deleteName else add @hidden to .user-group-unsaved in group + remove .user-group-new end end on change @@ -53,31 +55,12 @@

    -
    -
    -
    - - -
    -
    -
    -