diff --git a/app.py b/app.py index 93f83bc9..4c006ada 100644 --- a/app.py +++ b/app.py @@ -38,6 +38,7 @@ from middleware.security import ( MaxContentLengthMiddleware, SecurityHeadersMiddleware, + StaticCacheHeadersMiddleware, configure_cors, ) from repositories.indexes import ensure_indexes @@ -226,6 +227,8 @@ async def docs(request: Request): # 2. Security headers — must be outer so HSTS/CSP/nosniff apply to # all responses including CORS preflights (204) and body-limit (413) app.add_middleware(SecurityHeadersMiddleware, hsts_enabled=settings.is_production) + # 2a. Long-lived cache headers for /static/* + app.add_middleware(StaticCacheHeadersMiddleware) # 3. CORS configure_cors(app, settings) # 4. Body size limit diff --git a/middleware/rate_limiter.py b/middleware/rate_limiter.py index 95ddf1a4..f1c8b51a 100644 --- a/middleware/rate_limiter.py +++ b/middleware/rate_limiter.py @@ -34,6 +34,10 @@ class Limits: API_AUTHED = "60 per minute; 5000 per day" API_ANON = "20 per minute; 1000 per day" + # Alias availability check — cheap read, UI debounces on each keystroke + API_CHECK_AUTHED = "180 per minute; 10000 per day" + API_CHECK_ANON = "60 per minute; 2000 per day" + # Auth endpoints LOGIN = "5 per minute; 50 per day" SIGNUP = "5 per minute; 50 per day" diff --git a/middleware/security.py b/middleware/security.py index 48685896..5776cc20 100644 --- a/middleware/security.py +++ b/middleware/security.py @@ -126,6 +126,22 @@ async def dispatch( return response +class StaticCacheHeadersMiddleware(BaseHTTPMiddleware): + """Set long-lived immutable Cache-Control on /static/* responses. + + Templates cache-bust via ?v=N query strings, so each asset URL is + effectively content-addressed and safe to cache for a year. + """ + + async def dispatch( + self, request: Request, call_next: RequestResponseEndpoint + ) -> Response: + response = await call_next(request) + if request.url.path.startswith("/static/") and response.status_code == 200: + response.headers["Cache-Control"] = "public, max-age=31536000, immutable" + return response + + class MaxContentLengthMiddleware(BaseHTTPMiddleware): """Reject requests whose Content-Length exceeds the configured limit.""" diff --git a/routes/api_v1/shorten.py b/routes/api_v1/shorten.py index 2f00632a..ee18c8b3 100644 --- a/routes/api_v1/shorten.py +++ b/routes/api_v1/shorten.py @@ -7,7 +7,9 @@ from __future__ import annotations -from fastapi import APIRouter, Depends, Request +from typing import Annotated + +from fastapi import APIRouter, Depends, Query, Request from dependencies import ( SHORTEN_SCOPES, @@ -18,13 +20,14 @@ ) from middleware.openapi import AUTH_RESPONSES, OPTIONAL_AUTH_SECURITY from middleware.rate_limiter import Limits, dynamic_limit, limiter -from schemas.dto.requests.url import CreateUrlRequest -from schemas.dto.responses.url import UrlResponse +from schemas.dto.requests.url import AliasCheckQuery, CreateUrlRequest +from schemas.dto.responses.url import AliasCheckResponse, UrlResponse from shared.ip_utils import get_client_ip router = APIRouter(tags=["URL Shortening"]) _shorten_limit, _shorten_key = dynamic_limit(Limits.API_AUTHED, Limits.API_ANON) +_check_limit, _check_key = dynamic_limit(Limits.API_CHECK_AUTHED, Limits.API_CHECK_ANON) @router.post( @@ -69,3 +72,31 @@ async def shorten_v1( doc = await url_service.create(body, owner_id, client_ip) return UrlResponse.from_doc(doc, settings.app_url) + + +@router.get( + "/shorten/check-alias", + responses=AUTH_RESPONSES, + openapi_extra=OPTIONAL_AUTH_SECURITY, + operation_id="checkAliasAvailability", + summary="Check Alias Availability", +) +@limiter.limit(_check_limit, key_func=_check_key) +async def check_alias( + request: Request, + url_service: UrlSvc, + query: Annotated[AliasCheckQuery, Query()], + _user: CurrentUser | None = Depends(optional_scopes_verified(SHORTEN_SCOPES)), # noqa: B008 +) -> AliasCheckResponse: + """Check whether a proposed alias would be accepted by POST /api/v1/shorten. + + Reason codes on a negative result (``length``/``format``/``taken``) let the + UI render precise inline feedback without duplicating the validation rules. + + **Authentication**: Optional — higher rate limits when authenticated. + """ + result = await url_service.check_alias(query.alias) + return AliasCheckResponse( + available=result == "available", + reason=None if result == "available" else result, + ) diff --git a/schemas/dto/requests/url.py b/schemas/dto/requests/url.py index 1ef5face..b4f6d81d 100644 --- a/schemas/dto/requests/url.py +++ b/schemas/dto/requests/url.py @@ -250,3 +250,19 @@ def _parse_filter_json(self) -> ListUrlsQuery: @property def parsed_filter(self) -> UrlFilter | None: return self._parsed_filter + + +class AliasCheckQuery(RequestBase): + """Query parameters for GET /api/v1/shorten/check-alias. + + Intentionally permissive on ``max_length`` so that out-of-range input returns + a structured ``{available: false, reason: "length"}`` response instead of a + 422 — the UI surfaces the reason inline without re-implementing rules. + """ + + alias: str = Field( + min_length=1, + max_length=64, + description="Candidate alias to check.", + examples=["mylink"], + ) diff --git a/schemas/dto/responses/url.py b/schemas/dto/responses/url.py index 52a112ee..fb68e34d 100644 --- a/schemas/dto/responses/url.py +++ b/schemas/dto/responses/url.py @@ -195,3 +195,19 @@ class UrlListResponse(ResponseBase): hasNext: bool sortBy: str sortOrder: str + + +class AliasCheckResponse(ResponseBase): + """Response body for GET /api/v1/shorten/check-alias. + + ``available`` is true only when the alias passes format/length validation + AND is not already taken. When false, ``reason`` explains why so the UI + can render a precise, non-generic message. + """ + + available: bool = Field(description="Whether the alias is free to use.") + reason: str | None = Field( + default=None, + description="When unavailable: 'length', 'format', or 'taken'.", + examples=["taken"], + ) diff --git a/services/url_service.py b/services/url_service.py index 6b56f1da..39534512 100644 --- a/services/url_service.py +++ b/services/url_service.py @@ -20,6 +20,7 @@ import time from collections.abc import Awaitable, Callable from datetime import datetime, timezone +from typing import Literal from bson import ObjectId @@ -60,6 +61,8 @@ log = get_logger(__name__) +AliasCheckResult = Literal["available", "length", "format", "taken"] + # ── Field update handlers ──────────────────────────────────────────────────── # # Each handler inspects one field on the update request and, if changed, @@ -284,6 +287,22 @@ async def check_alias_available(self, alias: str) -> bool: return False return not await self._legacy_repo.check_exists(alias) + async def check_alias(self, alias: str) -> AliasCheckResult: + """Evaluate a candidate alias against the full creation rules. + + Mirrors what POST /api/v1/shorten would enforce (length, charset, + collision) so the UI can surface precise feedback without duplicating + the rules. Returns a single literal describing the first failing check, + or ``"available"`` when the alias would be accepted today. + """ + if not (3 <= len(alias) <= 16): + return "length" + if not validate_alias(alias): + return "format" + if not await self.check_alias_available(alias): + return "taken" + return "available" + async def create( self, request: CreateUrlRequest, diff --git a/static/css/api.css b/static/css/api.css index 2b9b9ff0..35061224 100644 --- a/static/css/api.css +++ b/static/css/api.css @@ -68,7 +68,8 @@ body { padding: 10px; } -.custom-table th, .custom-table td { +.custom-table th, +.custom-table td { border: 1px solid #353535; text-align: left; } @@ -77,27 +78,33 @@ body { font-weight: bold; } -h1, h2, h3, h4 { +h1, +h2, +h3, +h4 { font-weight: bolder; } -.example-codes > div, -.emoji-example-codes > div, -.stats-example-codes > div, -.export-example-codes > div { +.example-codes>div, +.emoji-example-codes>div, +.stats-example-codes>div, +.export-example-codes>div { display: none; } -.example-codes > div.active-code, -.emoji-example-codes > div.active-code, -.stats-example-codes > div.active-code, -.export-example-codes > div.active-code { +.example-codes>div.active-code, +.emoji-example-codes>div.active-code, +.stats-example-codes>div.active-code, +.export-example-codes>div.active-code { display: block; } /* Dock styling */ -.languages-dock, .emoji-languages-dock, .stats-languages-dock, .export-languages-dock { +.languages-dock, +.emoji-languages-dock, +.stats-languages-dock, +.export-languages-dock { display: flex; justify-content: flex-start; gap: 3px; @@ -108,17 +115,26 @@ h1, h2, h3, h4 { overflow-x: scroll; } -.languages-dock::-webkit-scrollbar, .emoji-languages-dock::-webkit-scrollbar, .stats-languages-dock::-webkit-scrollbar, .export-languages-dock::-webkit-scrollbar { +.languages-dock::-webkit-scrollbar, +.emoji-languages-dock::-webkit-scrollbar, +.stats-languages-dock::-webkit-scrollbar, +.export-languages-dock::-webkit-scrollbar { display: none; } -.languages-dock::-webkit-scrollbar-track, .emoji-languages-dock::-webkit-scrollbar-track, .stats-languages-dock::-webkit-scrollbar-track, .export-languages-dock::-webkit-scrollbar-track { +.languages-dock::-webkit-scrollbar-track, +.emoji-languages-dock::-webkit-scrollbar-track, +.stats-languages-dock::-webkit-scrollbar-track, +.export-languages-dock::-webkit-scrollbar-track { background-color: transparent; } /* Button styling */ -.language-button, .emoji-language-button, .stats-language-button, .export-language-button { +.language-button, +.emoji-language-button, +.stats-language-button, +.export-language-button { background-color: #444; color: white; border: none; @@ -135,13 +151,19 @@ h1, h2, h3, h4 { } /* Button hover effect */ -.language-button:hover, .emoji-language-button:hover, .stats-language-button:hover, .export-language-button:hover { +.language-button:hover, +.emoji-language-button:hover, +.stats-language-button:hover, +.export-language-button:hover { background-color: #555; color: white; } /* Active button styling */ -.language-button.active, .emoji-language-button.active, .stats-language-button.active, .export-language-button.active { +.language-button.active, +.emoji-language-button.active, +.stats-language-button.active, +.export-language-button.active { background-color: rgb(25, 25, 25); color: white; border: none; @@ -160,12 +182,15 @@ h1, h2, h3, h4 { from { transform: scaleX(0); } + to { transform: scaleX(1); } } -.language-button::before, .emoji-language-button::before, .stats-language-button::before { +.language-button::before, +.emoji-language-button::before, +.stats-language-button::before { content: ""; position: absolute; width: 100%; @@ -178,7 +203,9 @@ h1, h2, h3, h4 { transition: all 0.3s ease-in-out 0s; } -.language-button:hover::before, .emoji-language-button:hover::before, .stats-language-button:hover::before { +.language-button:hover::before, +.emoji-language-button:hover::before, +.stats-language-button:hover::before { visibility: visible; transform: scaleX(1); } diff --git a/static/css/auth-modal.css b/static/css/auth-modal.css new file mode 100644 index 00000000..2ad4d7da --- /dev/null +++ b/static/css/auth-modal.css @@ -0,0 +1,399 @@ +/* Auth Modal Override Styles - Force dashboard consistency */ +#authModal.modal { + position: fixed; + display: none; + align-items: center; + justify-content: center; + z-index: var(--z-modal); + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(10px) saturate(180%) brightness(0.7); +} + +#authModal .modal-container { + position: relative; + max-width: 460px; + width: 100%; + max-height: 90vh; + overflow-y: auto; +} + +#authModal .modal-content { + background: rgba(15, 20, 35, 0.5); + backdrop-filter: blur(60px); + border: 1px solid rgba(255, 255, 255, 0.10); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); + animation: authModalSlideIn 0.3s ease; + margin: 0 !important; + padding: 28px !important; +} + +@keyframes authModalSlideIn { + from { + opacity: 0; + transform: scale(0.95) translateY(-20px); + } + + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +#authModal .modal-header { + display: flex; + justify-content: center; +} + +#authModal .modal-title-section { + flex: 1; +} + +#authModal .modal-title { + margin: 0 0 8px 0; + font-size: 25px; + font-weight: 600; + color: #ffffff; + line-height: 1.3; +} + +#authModal .modal-subtitle { + margin: 0; + font-size: 14px; + color: rgba(255, 255, 255, 0.70); + font-weight: 400; + text-align: center; +} + +#authModal .modal-footer { + display: flex; + flex-direction: column; + align-items: center; +} + +#authForm { + margin: 0; + padding: 0; +} + +/* Form field overrides */ +#authModal .form-grid { + display: flex; + flex-direction: column; + gap: 20px; +} + +#authModal .field label { + font-size: 13px; + font-weight: 500; + color: rgba(255, 255, 255, 0.70); + text-transform: uppercase; + letter-spacing: 0.5px; + margin: 0; + display: block; +} + +#authModal .field input { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.10); + border-radius: 8px; + font-size: 14px; + font-family: inherit; + margin: 8px 0 0 0; +} + +#authModal .field input:focus { + outline: none; + border-color: #7c3aed; + background: rgba(255, 255, 255, 0.08); + box-shadow: none; +} + +#authModal .field input::placeholder { + color: rgba(255, 255, 255, 0.5) !important; + opacity: 1 !important; +} + +/* Button overrides */ +#authModal .btn { + display: inline-flex; + justify-content: center; + padding: 12px 16px; + border: none; + border-radius: 8px; + font-size: 14px; + text-decoration: none; + transition: all 0.2s; + font-family: inherit; + margin: 30px 0 0 0; + height: auto !important; +} + +#authModal .btn-primary { + background: #7c3aed; + color: white; +} + +#authModal .btn-primary:hover { + background: #6d28d9; +} + +#authModal .btn-primary:active { + background: #5b21b6; +} + +/* OAuth button */ +.oauth-section { + display: flex; + justify-content: center; + align-items: center; + margin: 30px 0 20px 0; + gap: 10px; +} + +#authModal .oauth-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + padding: 12px 16px; + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 8px; + background: rgba(255, 255, 255, 0.05); + color: rgba(255, 255, 255, 0.9); + font-size: 14px; + transition: all 0.2s ease; + margin: 0; +} + +#authModal .oauth-btn:hover { + background: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.25); +} + +/* Divider */ +#authModal .auth-divider { + display: flex; + align-items: center; + margin: 20px 0; + text-align: center; +} + +#authModal .auth-divider::before, +#authModal .auth-divider::after { + content: ''; + flex: 1; + height: 1px; + background: rgba(255, 255, 255, 0.15); +} + +#authModal .auth-divider span { + padding: 0 12px; + font-size: 13px; + color: rgba(255, 255, 255, 0.6); +} + +/* Toggle section */ +#authModal .auth-toggle { + font-size: 14px; + color: rgba(255, 255, 255, 0.7); + margin: 10px 0 0 0; + padding: 0; +} + +#authModal .auth-toggle button { + background: none; + border: none; + color: #7c3aed; + cursor: pointer; + font-size: inherit; + transition: color 0.2s ease; + font-family: inherit; + padding: 0; + margin: 0 0 0 5px; + width: auto; + height: auto; + display: inline-block; +} + +#authModal .auth-toggle button:hover { + color: #6d28d9; +} + +/* Legal notice */ +#authModal .legal-notice { + font-size: 10px; + color: rgba(255, 255, 255, 0.5); + text-align: center; + margin: 30px 0 0 0; + padding: 0; + line-height: 1.5; +} + +#authModal .legal-notice a { + color: #7c3aed; + text-decoration: none; + transition: color 0.2s ease; + font-size: 10px; +} + +#authModal .legal-notice a:hover { + color: #6d28d9; + text-decoration: underline; +} + +/* Error message */ +#authModal .auth-error { + display: none; + color: #ef4444; + font-size: 14px; + text-align: center; + margin: 20px 20px 0 20px; + padding: 8px 12px; + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); + border-radius: 8px; +} + +/* Password Strength Indicator */ +#authModal .password-strength-container { + margin: 8px 0 0 0; + padding: 0; +} + +#authModal .password-strength-bar-container { + width: 100%; + height: 4px; + background: rgba(255, 255, 255, 0.1); + border-radius: 2px; + overflow: hidden; + margin-bottom: 4px; +} + +#authModal .password-strength-bar { + height: 100%; + width: 0%; + background: #ef4444; + border-radius: 2px; + transition: all 0.3s ease; +} + +#authModal .password-strength-label { + font-size: 12px; + color: #ef4444; + text-align: right; + font-weight: 500; +} + +/* Password Requirements */ +#authModal .password-requirements { + margin: 8px 0 0 0; + padding: 0; + list-style: none; + font-size: 12px; +} + +#authModal .password-requirements li { + display: flex; + align-items: center; + margin: 2px 0; + color: rgba(255, 255, 255, 0.7); + transition: color 0.2s ease; +} + +#authModal .password-requirements .requirement-icon { + margin-right: 6px; + font-weight: bold; + width: 12px; + text-align: center; +} + +#authModal .password-requirements .requirement-missing { + color: #ef4444; +} + +#authModal .password-requirements .requirement-missing .requirement-icon { + color: #ef4444; +} + +#authModal .password-requirements .requirement-met { + color: #22c55e; +} + +#authModal .password-requirements .requirement-met .requirement-icon { + color: #22c55e; +} + +/* Mobile responsiveness */ +@media screen and (max-width: 600px) { + #authModal .modal-container { + max-width: calc(100vw - 20px) !important; + max-height: calc(100vh - 24px); + } + + #authModal .modal-content { + padding: 22px 18px !important; + } + + #authModal .modal-header, + #authModal .modal-body, + #authModal .modal-footer { + padding-left: 0; + padding-right: 0; + } + + #authModal .modal-title { + font-size: 22px; + } + + #authModal .modal-subtitle { + font-size: 13px; + } + + .oauth-section { + margin: 20px 0 14px 0; + gap: 8px; + } + + #authModal .oauth-btn { + padding: 10px 12px; + font-size: 13px; + gap: 6px; + } + + #authModal .oauth-btn svg { + width: 16px; + height: 16px; + } + + #authModal .auth-divider { + margin: 14px 0; + } + + #authModal .form-grid { + gap: 14px; + } + + #authModal .field input { + padding: 10px 12px; + } + + #authModal .btn { + margin: 20px 0 0 0; + padding: 10px 14px; + } + + #authModal .legal-notice { + margin: 18px 0 0 0; + } +} + +@media screen and (max-width: 400px) { + #authModal .oauth-btn span { + display: none; + } + + #authModal .oauth-btn { + padding: 12px 16px; + gap: 0; + } +} \ No newline at end of file diff --git a/static/css/auth.css b/static/css/auth.css index 53160748..a3af7e30 100644 --- a/static/css/auth.css +++ b/static/css/auth.css @@ -13,7 +13,7 @@ border-radius: 14px; background: rgba(15, 18, 37, 0.95); border: 1px solid #2a2e52; - box-shadow: 0 20px 60px rgba(0,0,0,0.45); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45); } #authModal h2 { @@ -87,6 +87,7 @@ justify-content: center; gap: 8px; } + #authModal .toggle button { background: none; border: none; @@ -101,10 +102,15 @@ text-decoration: underline; font: inherit; } -#authError { color: #ff7b7b; text-align: center; } -@media screen and (max-width: 600px){ - #authModal .modal-content { margin: 20px; padding: 20px; } +#authError { + color: #ff7b7b; + text-align: center; } - +@media screen and (max-width: 600px) { + #authModal .modal-content { + margin: 20px; + padding: 20px; + } +} \ No newline at end of file diff --git a/static/css/base.css b/static/css/base.css index 9089cb12..b1d70d74 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -1,5 +1,22 @@ @import url('https://fonts.googleapis.com/css2?family=Allerta+Stencil&display=swap'); +:root { + /* Z-Index Scale */ + --z-dropdown: 100; + --z-sticky: 200; + --z-overlay: 1000; + --z-modal: 2000; + --z-notification: 3000; + --z-max: 9999; + + /* Status Colors */ + --color-danger: #ef4444; + --color-danger-hover: #dc2626; + --color-success: #22c55e; + --color-warning: #f59e0b; + --color-info: #6366f1; +} + body::-webkit-scrollbar { display: none; } diff --git a/static/css/confetti.css b/static/css/confetti.css deleted file mode 100644 index 418ef2eb..00000000 --- a/static/css/confetti.css +++ /dev/null @@ -1,16 +0,0 @@ -body { - min-height: 100vh; - /* overflow: hidden; */ -} - -.confetti { - width: 8px; - height: 8px; - position: absolute; - z-index: 11; - background: var(--color); - top: 0; - left: 0; - will-change: transform; - pointer-events: none; -} diff --git a/static/css/contact.css b/static/css/contact.css index 3ddca121..755abda5 100644 --- a/static/css/contact.css +++ b/static/css/contact.css @@ -3,19 +3,14 @@ body { margin: 0; padding: 0; box-sizing: border-box; - background-image: linear-gradient(3deg, #fc6538, #678382); - background-color: rgb(136, 43, 8); color: white; -} - -body { width: 100%; height: 100%; background-size: cover; background-position: center center; background-repeat: repeat; background-image: url("data:image/svg+xml;utf8,%3Csvg viewBox=%220 0 2000 1000%22 xmlns=%22http:%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cmask id=%22b%22 x=%220%22 y=%220%22 width=%222000%22 height=%221000%22%3E%3Cpath fill=%22url(%23a)%22 d=%22M0 0h2000v1000H0z%22%2F%3E%3C%2Fmask%3E%3Cpath fill=%22%23040337%22 d=%22M0 0h2000v1000H0z%22%2F%3E%3Cg style=%22transform-origin:center center%22 stroke=%22%230e1082%22 stroke-width=%222%22 mask=%22url(%23b)%22%3E%3Cpath fill=%22none%22 d=%22M0 0h100v100H0z%22%2F%3E%3Cpath fill=%22%230e1082d5%22 d=%22M100 0h100v100H100z%22%2F%3E%3Cpath fill=%22none%22 d=%22M200 0h100v100H200z%22%2F%3E%3Cpath fill=%22%230e108215%22 d=%22M300 0h100v100H300z%22%2F%3E%3Cpath fill=%22none%22 d=%22M400 0h100v100H400z%22%2F%3E%3Cpath fill=%22%230e1082ca%22 d=%22M500 0h100v100H500z%22%2F%3E%3Cpath fill=%22%230e10820b%22 d=%22M600 0h100v100H600z%22%2F%3E%3Cpath fill=%22none%22 d=%22M700 0h100v100H700zM800 0h100v100H800z%22%2F%3E%3Cpath fill=%22%230e1082c7%22 d=%22M900 0h100v100H900z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1000 0h100v100h-100zM1100 0h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e108290%22 d=%22M1200 0h100v100h-100z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1300 0h100v100h-100zM1400 0h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e1082b8%22 d=%22M1500 0h100v100h-100z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1600 0h100v100h-100zM1700 0h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e108258%22 d=%22M1800 0h100v100h-100z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1900 0h100v100h-100zM0 100h100v100H0zM100 100h100v100H100zM200 100h100v100H200z%22%2F%3E%3Cpath fill=%22%230e1082ff%22 d=%22M300 100h100v100H300z%22%2F%3E%3Cpath fill=%22none%22 d=%22M400 100h100v100H400zM500 100h100v100H500zM600 100h100v100H600zM700 100h100v100H700zM800 100h100v100H800z%22%2F%3E%3Cpath fill=%22%230e1082f4%22 d=%22M900 100h100v100H900z%22%2F%3E%3Cpath fill=%22%230e108208%22 d=%22M1000 100h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e1082a9%22 d=%22M1100 100h100v100h-100z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1200 100h100v100h-100zM1300 100h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e1082aa%22 d=%22M1400 100h100v100h-100z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1500 100h100v100h-100zM1600 100h100v100h-100zM1700 100h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e1082d3%22 d=%22M1800 100h100v100h-100z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1900 100h100v100h-100zM0 200h100v100H0z%22%2F%3E%3Cpath fill=%22%230e108291%22 d=%22M100 200h100v100H100z%22%2F%3E%3Cpath fill=%22none%22 d=%22M200 200h100v100H200zM300 200h100v100H300z%22%2F%3E%3Cpath fill=%22%230e108265%22 d=%22M400 200h100v100H400z%22%2F%3E%3Cpath fill=%22none%22 d=%22M500 200h100v100H500zM600 200h100v100H600z%22%2F%3E%3Cpath fill=%22%230e1082c4%22 d=%22M700 200h100v100H700z%22%2F%3E%3Cpath fill=%22none%22 d=%22M800 200h100v100H800zM900 200h100v100H900z%22%2F%3E%3Cpath fill=%22%230e1082d8%22 d=%22M1000 200h100v100h-100z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1100 200h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e10824e%22 d=%22M1200 200h100v100h-100z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1300 200h100v100h-100zM1400 200h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e1082cd%22 d=%22M1500 200h100v100h-100z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1600 200h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e1082e1%22 d=%22M1700 200h100v100h-100z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1800 200h100v100h-100zM1900 200h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e108234%22 d=%22M0 300h100v100H0z%22%2F%3E%3Cpath fill=%22none%22 d=%22M100 300h100v100H100z%22%2F%3E%3Cpath fill=%22%230e1082d6%22 d=%22M200 300h100v100H200z%22%2F%3E%3Cpath fill=%22none%22 d=%22M300 300h100v100H300zM400 300h100v100H400z%22%2F%3E%3Cpath fill=%22%230e10820f%22 d=%22M500 300h100v100H500z%22%2F%3E%3Cpath fill=%22none%22 d=%22M600 300h100v100H600zM700 300h100v100H700zM800 300h100v100H800zM900 300h100v100H900zM1000 300h100v100h-100zM1100 300h100v100h-100zM1200 300h100v100h-100zM1300 300h100v100h-100zM1400 300h100v100h-100zM1500 300h100v100h-100zM1600 300h100v100h-100zM1700 300h100v100h-100zM1800 300h100v100h-100zM1900 300h100v100h-100zM0 400h100v100H0zM100 400h100v100H100zM200 400h100v100H200zM300 400h100v100H300z%22%2F%3E%3Cpath fill=%22%230e108229%22 d=%22M400 400h100v100H400z%22%2F%3E%3Cpath fill=%22none%22 d=%22M500 400h100v100H500z%22%2F%3E%3Cpath fill=%22%230e1082b4%22 d=%22M600 400h100v100H600z%22%2F%3E%3Cpath fill=%22none%22 d=%22M700 400h100v100H700zM800 400h100v100H800zM900 400h100v100H900zM1000 400h100v100h-100zM1100 400h100v100h-100zM1200 400h100v100h-100zM1300 400h100v100h-100zM1400 400h100v100h-100zM1500 400h100v100h-100zM1600 400h100v100h-100zM1700 400h100v100h-100zM1800 400h100v100h-100zM1900 400h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e1082d4%22 d=%22M0 500h100v100H0z%22%2F%3E%3Cpath fill=%22%230e108244%22 d=%22M100 500h100v100H100z%22%2F%3E%3Cpath fill=%22%230e108269%22 d=%22M200 500h100v100H200z%22%2F%3E%3Cpath fill=%22%230e10825c%22 d=%22M300 500h100v100H300z%22%2F%3E%3Cpath fill=%22none%22 d=%22M400 500h100v100H400zM500 500h100v100H500z%22%2F%3E%3Cpath fill=%22%230e10823e%22 d=%22M600 500h100v100H600z%22%2F%3E%3Cpath fill=%22none%22 d=%22M700 500h100v100H700z%22%2F%3E%3Cpath fill=%22%230e1082cc%22 d=%22M800 500h100v100H800z%22%2F%3E%3Cpath fill=%22none%22 d=%22M900 500h100v100H900zM1000 500h100v100h-100zM1100 500h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e1082bd%22 d=%22M1200 500h100v100h-100z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1300 500h100v100h-100zM1400 500h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e10821d%22 d=%22M1500 500h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e108294%22 d=%22M1600 500h100v100h-100z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1700 500h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e10823b%22 d=%22M1800 500h100v100h-100z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1900 500h100v100h-100zM0 600h100v100H0zM100 600h100v100H100z%22%2F%3E%3Cpath fill=%22%230e108245%22 d=%22M200 600h100v100H200z%22%2F%3E%3Cpath fill=%22%230e1082c4%22 d=%22M300 600h100v100H300z%22%2F%3E%3Cpath fill=%22none%22 d=%22M400 600h100v100H400zM500 600h100v100H500zM600 600h100v100H600z%22%2F%3E%3Cpath fill=%22%230e108238%22 d=%22M700 600h100v100H700z%22%2F%3E%3Cpath fill=%22%230e108253%22 d=%22M800 600h100v100H800z%22%2F%3E%3Cpath fill=%22%230e1082fd%22 d=%22M900 600h100v100H900z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1000 600h100v100h-100zM1100 600h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e108269%22 d=%22M1200 600h100v100h-100z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1300 600h100v100h-100zM1400 600h100v100h-100zM1500 600h100v100h-100zM1600 600h100v100h-100zM1700 600h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e108252%22 d=%22M1800 600h100v100h-100z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1900 600h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e1082d2%22 d=%22M0 700h100v100H0z%22%2F%3E%3Cpath fill=%22none%22 d=%22M100 700h100v100H100z%22%2F%3E%3Cpath fill=%22%230e1082c2%22 d=%22M200 700h100v100H200z%22%2F%3E%3Cpath fill=%22none%22 d=%22M300 700h100v100H300zM400 700h100v100H400zM500 700h100v100H500zM600 700h100v100H600zM700 700h100v100H700zM800 700h100v100H800z%22%2F%3E%3Cpath fill=%22%230e10824b%22 d=%22M900 700h100v100H900z%22%2F%3E%3Cpath fill=%22%230e108230%22 d=%22M1000 700h100v100h-100z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1100 700h100v100h-100zM1200 700h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e10829b%22 d=%22M1300 700h100v100h-100z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1400 700h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e10826e%22 d=%22M1500 700h100v100h-100z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1600 700h100v100h-100zM1700 700h100v100h-100zM1800 700h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e108249%22 d=%22M1900 700h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e1082e9%22 d=%22M0 800h100v100H0z%22%2F%3E%3Cpath fill=%22none%22 d=%22M100 800h100v100H100z%22%2F%3E%3Cpath fill=%22%230e1082b3%22 d=%22M200 800h100v100H200z%22%2F%3E%3Cpath fill=%22%230e1082f0%22 d=%22M300 800h100v100H300z%22%2F%3E%3Cpath fill=%22none%22 d=%22M400 800h100v100H400zM500 800h100v100H500z%22%2F%3E%3Cpath fill=%22%230e10824a%22 d=%22M600 800h100v100H600z%22%2F%3E%3Cpath fill=%22none%22 d=%22M700 800h100v100H700zM800 800h100v100H800zM900 800h100v100H900z%22%2F%3E%3Cpath fill=%22%230e108220%22 d=%22M1000 800h100v100h-100z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1100 800h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e108260%22 d=%22M1200 800h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e1082aa%22 d=%22M1300 800h100v100h-100z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1400 800h100v100h-100zM1500 800h100v100h-100zM1600 800h100v100h-100zM1700 800h100v100h-100zM1800 800h100v100h-100zM1900 800h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e10826d%22 d=%22M0 900h100v100H0z%22%2F%3E%3Cpath fill=%22none%22 d=%22M100 900h100v100H100zM200 900h100v100H200zM300 900h100v100H300zM400 900h100v100H400zM500 900h100v100H500zM600 900h100v100H600zM700 900h100v100H700zM800 900h100v100H800z%22%2F%3E%3Cpath fill=%22%230e1082eb%22 d=%22M900 900h100v100H900z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1000 900h100v100h-100zM1100 900h100v100h-100zM1200 900h100v100h-100zM1300 900h100v100h-100zM1400 900h100v100h-100zM1500 900h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e108266%22 d=%22M1600 900h100v100h-100z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1700 900h100v100h-100z%22%2F%3E%3Cpath fill=%22%230e108225%22 d=%22M1800 900h100v100h-100z%22%2F%3E%3Cpath fill=%22none%22 d=%22M1900 900h100v100h-100z%22%2F%3E%3C%2Fg%3E%3Cpath fill=%22%23f3f3f3%22 filter=%22url(%23c)%22 opacity=%22.8%22 d=%22M0 0h2000v1000H0z%22%2F%3E%3Cdefs%3E%3CradialGradient id=%22a%22%3E%3Cstop offset=%220%22 stop-color=%22%23fff%22%2F%3E%3Cstop offset=%221%22 stop-color=%22%23fff%22 stop-opacity=%220%22%2F%3E%3C%2FradialGradient%3E%3Cfilter id=%22c%22 x=%22-800%22 y=%22-400%22 width=%222800%22 height=%221400%22 filterUnits=%22userSpaceOnUse%22 primitiveUnits=%22userSpaceOnUse%22 color-interpolation-filters=%22linearRGB%22%3E%3CfeTurbulence type=%22fractalNoise%22 baseFrequency=%22.11%22 numOctaves=%224%22 seed=%2215%22 stitchTiles=%22no-stitch%22 x=%220%22 y=%220%22 width=%222000%22 height=%221000%22 result=%22turbulence%22%2F%3E%3CfeSpecularLighting surfaceScale=%2210%22 specularConstant=%22.13%22 specularExponent=%2220%22 lighting-color=%22%23fff%22 x=%220%22 y=%220%22 width=%222000%22 height=%221000%22 in=%22turbulence%22 result=%22specularLighting%22%3E%3CfeDistantLight azimuth=%223%22 elevation=%22100%22%2F%3E%3C%2FfeSpecularLighting%3E%3C%2Ffilter%3E%3C%2Fdefs%3E%3C%2Fsvg%3E"); - } +} h1 { color: #fff; @@ -70,7 +65,7 @@ textarea:focus::placeholder { color: #ccc; } -button.h-captcha{ +button.h-captcha { border: none; width: 180px; padding: 10px 13px; @@ -157,7 +152,7 @@ button.h-captcha img { width: 80%; } - .opts { + #opts { font-size: 1rem; } diff --git a/static/css/contacts-modal.css b/static/css/contacts-modal.css index 8d841f5e..358a7281 100644 --- a/static/css/contacts-modal.css +++ b/static/css/contacts-modal.css @@ -6,7 +6,7 @@ width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); - z-index: 999999999999999999; + z-index: var(--z-modal); display: none; align-items: center; justify-content: center; @@ -17,7 +17,7 @@ margin-top: 70px; padding: 20px; border-radius: 10px; - box-shadow: 0 8px 32px 0 rgba( 0, 0, 0, 0.37 ); + box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); width: 100%; max-width: 500px; font-family: 'Allerta Stencil', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; @@ -74,7 +74,7 @@ border-radius: 10px; color: white; font-size: 20px; - border: 2px solid rgba(255,255,255, 0.08); + border: 2px solid rgba(255, 255, 255, 0.08); background: rgba(255, 255, 255, 0.125); } @@ -98,6 +98,7 @@ opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); @@ -109,6 +110,7 @@ opacity: 1; transform: translateY(0); } + to { opacity: 0; transform: translateY(20px); diff --git a/static/css/customNotification.css b/static/css/customNotification.css deleted file mode 100644 index 43dbd6f8..00000000 --- a/static/css/customNotification.css +++ /dev/null @@ -1,109 +0,0 @@ -.custom-top-notification { - display: none; - position: fixed; - bottom: 0; - right: 0; - padding: 5px 15px; - color: white; - z-index: 9999999999999999999999999999999; - /* background: rgba(255, 255, 255, 0.105); */ - border-radius: 6px; - max-width: 330px; - min-width: 280px; - margin: 20px; - backdrop-filter: blur(30px); - border: 1px solid rgba(0, 0, 0, 0.1); - border-bottom: 0; - animation: slide-in 0.5s cubic-bezier(0.42, 0, 0.58, 1); - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; -} - -.custom-top-notification.warning { - background-color: rgb(67, 53, 25); -} - -.custom-top-notification.success { - background-color: rgb(56, 60, 26); -} - -.custom-top-notification.error { - background-color: rgb(61, 28, 27); -} - -.custom-top-notification .closebtn { - float: right; - background: none; - border: none; - color: white; - font-size: 20px; - cursor: pointer; -} - -.custom-top-notification.warning .closebtn:hover { - color: rgb(250, 225, 0); -} - -.custom-top-notification.success .closebtn:hover { - color: rgb(106, 203, 93); -} - -.custom-top-notification.error .closebtn:hover { - color: rgb(255, 153, 163); -} - -.custom-top-notification p { - margin: 5px 0; - color: white; - letter-spacing: 0.5px; - text-align: center; - padding-right: 25px; - padding-left: 8px; - padding-top: 5px; - font-weight: normal; - padding-bottom: 15px; - font-size: 16px; -} - -.custom-top-notification .close-progress-bar { - position: absolute; - bottom: 0; - left: 0; - height: 5px; - width: -webkit-fill-available; - border-bottom-left-radius: 1000px; - border-bottom-right-radius: 1000px; - margin: 0 0; - /* animation: progress-bar-animation 5s linear; */ -} - -.custom-top-notification.warning .close-progress-bar { - background: rgb(250, 225, 0); -} - -.custom-top-notification.success .close-progress-bar { - background: rgb(106, 203, 93); -} - -.custom-top-notification.error .close-progress-bar { - background: rgb(255, 153, 163); -} - -@keyframes progress-bar-animation { - from { - width: 100%; - } - - to { - width: 0; - } -} - -@keyframes slide-in { - from { - transform: translateX(150%); - } - - to { - transform: translateX(0); - } -} diff --git a/static/css/dashboard.css b/static/css/dashboard.css deleted file mode 100644 index 06324a12..00000000 --- a/static/css/dashboard.css +++ /dev/null @@ -1,538 +0,0 @@ -:root { - --bg: #090d1a; - --glass: rgba(15, 20, 40, 0.52); - --glass-2: rgba(255, 255, 255, 0.06); - --border: rgba(255, 255, 255, 0.10); - --border-2: rgba(255, 255, 255, 0.18); - --text: #ffffff; - --muted: rgba(255, 255, 255, 0.70); - --accent: #7c3aed; - --accent-2: #2563eb; - --success: #22c55e; - --warning: #f59e0b; - --danger: #ef4444; - --radius-lg: 16px; - --radius-md: 12px; - --radius-sm: 10px; - --shadow: 0 8px 24px rgba(0, 0, 0, 0.35); -} - -body { - background: radial-gradient(1000px 600px at -10% -10%, rgba(124, 58, 237, 0.18), transparent 60%), - radial-gradient(900px 600px at 110% 0%, rgba(37, 99, 235, 0.16), transparent 60%), - linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0)); - background-color: var(--bg); -} - -.dashboard { - display: grid; - grid-template-columns: 260px 1fr; - gap: 20px; - max-width: 100%; - margin: 28px auto; - padding: 0 24px; -} - -.sidebar { - position: sticky; - top: 18px; - height: calc(100vh - 56px); - display: flex; - flex-direction: column; - gap: 14px; - border: 1px solid var(--border); - border-radius: var(--radius-lg); - background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.02)); - backdrop-filter: blur(14px) saturate(1.1); - box-shadow: var(--shadow); - padding: 16px; -} - -.brand { - display: flex; - align-items: center; - gap: 12px; - padding: 10px 8px 14px 8px; - border-bottom: 1px solid var(--border); -} - -.logo-circle { - width: 36px; - height: 36px; - border-radius: 50%; - background: linear-gradient(135deg, var(--accent), var(--accent-2)); - display: grid; - place-items: center; - color: #fff; - font-weight: 700; - letter-spacing: 0.4px; -} - -.brand-text .title { - color: var(--text); - font-weight: 700; - letter-spacing: 0.2px; -} - -.brand-text .subtitle { - color: var(--muted); - font-size: 12px; -} - -.nav { - display: flex; - flex-direction: column; - gap: 6px; - padding: 8px 0; -} - -.nav-item { - display: flex; - align-items: center; - gap: 10px; - padding: 10px 12px; - border-radius: var(--radius-sm); - color: var(--text); - text-decoration: none; - border: 1px solid transparent; - transition: transform .15s ease, border-color .2s ease, background .2s ease; -} - -.nav-item .dot { - width: 6px; - height: 6px; - border-radius: 999px; - background: var(--border-2); -} - -.nav-item:hover { - background: var(--glass-2); - border-color: var(--border); -} - -.nav-item.active { - background: linear-gradient(135deg, rgba(124, 58, 237, 0.18), rgba(37, 99, 235, 0.18)); - border-color: rgba(124, 58, 237, 0.35); -} - -.nav-item.active .dot { - background: #a78bfa; -} - -.nav-item.disabled { - opacity: .6; - cursor: not-allowed; -} - -.sidebar-footer { - padding-top: 10px; - border-top: 1px solid var(--border); -} - -.sidebar-bottom-links { - margin-top: auto; - display: flex; - flex-direction: column; - gap: 6px; - padding: 8px 0; -} - -.content { - display: flex; - flex-direction: column; - gap: 16px; - min-width: 0; -} - -.content-header h1 { - margin: 0; - color: var(--text); - font-size: 22px; -} - -.content-header p { - margin: 2px 0 0; - color: var(--muted); -} - -.surface { - border: 1px solid var(--border); - border-radius: var(--radius-lg); - background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.02)); - backdrop-filter: blur(12px) saturate(1.05); - box-shadow: var(--shadow); -} - -.toolbar { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - padding: 12px; - position: relative; - z-index: 10; - overflow: visible; -} - -.filters { - display: grid; - grid-template-columns: 1fr; - gap: 10px; - align-items: end; - width: 100%; -} - -.filters.compact { - grid-template-columns: 1fr; -} - -.field { - display: flex; - flex-direction: column; - gap: 8px; -} - -/* Segmented control */ -.seg { - position: relative; - display: grid; - grid-auto-flow: column; - grid-auto-columns: 1fr; - gap: 8px; - padding: 6px; - border: 1px solid var(--border); - border-radius: 999px; - background: rgba(255, 255, 255, 0.04); -} - -.seg.seg--3 { - grid-template-columns: repeat(3, 1fr); -} - -.seg.seg--2 { - grid-template-columns: repeat(2, 1fr); -} - -.seg button { - position: relative; - z-index: 1; - border: 0; - background: transparent; - color: var(--text); - padding: 8px 10px; - border-radius: 999px; - cursor: pointer; - font-weight: 600; - letter-spacing: .3px; -} - -.seg .seg-indicator { - position: absolute; - z-index: 0; - top: 6px; - bottom: 6px; - left: 6px; - width: var(--seg-w, calc((100% - 16px) / 3)); - border-radius: 999px; - background: rgba(255, 255, 255, 0.08); - border: 1px solid var(--border-2); - transition: transform .2s ease; -} - -.seg.seg--2 .seg-indicator { - width: calc((100% - 14px) / 2); -} - -.seg[data-active="0"] .seg-indicator { - transform: translateX(0%); -} - -.seg[data-active="1"] .seg-indicator { - transform: translateX(100%); -} - -.seg[data-active="2"] .seg-indicator { - transform: translateX(200%); -} - -.field label { - color: var(--muted); - font-size: 12px; - letter-spacing: .2px; -} - -.field input, -.field select { - width: 100%; - padding: 10px 12px; - color: var(--text); - background: rgba(255, 255, 255, 0.05); - border: 1px solid var(--border-2); - border-radius: 12px; -} - -.field.search input { - padding-left: 12px; -} - -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - padding: 10px 14px; - border-radius: 12px; - border: 1px solid var(--border-2); - background: rgba(255, 255, 255, 0.06); - color: var(--text); - cursor: pointer; - transition: transform .12s ease, background .2s ease, border-color .2s ease; -} - -/* Options button caret alignment */ -#btn-options { - display: inline-flex; - align-items: center; - gap: 8px; -} - -#btn-options .caret { - line-height: 1; - display: inline-block; - transform: translateY(1px); -} - -.btn:hover { - background: rgba(255, 255, 255, 0.12); - border-color: rgba(255, 255, 255, 0.28); -} - -.btn:active { - transform: translateY(1px); -} - -.btn-ghost { - background: transparent; - border-color: var(--border); -} - -.btn-primary { - border: none; - background: linear-gradient(135deg, var(--accent-2), var(--accent)); -} - -.btn-primary:hover { - filter: brightness(1.05); -} - -/* Options dropdown */ -.options { - position: relative; -} - -.options-dropdown { - position: absolute; - top: calc(100% + 10px); - right: 0; - left: auto; - min-width: 720px; - max-width: 86vw; - padding: 20px; - background: rgba(15, 20, 35, 0.98); - border: 1px solid rgba(255, 255, 255, 0.15); - border-radius: 12px; - backdrop-filter: blur(16px); - box-shadow: rgba(0, 0, 0, 0.3) 0 20px 40px 0px; - z-index: 1000; -} - -.options-grid { - display: grid; - grid-template-columns: repeat(3, minmax(220px, 1fr)); - gap: 16px 18px; - align-items: end; -} - -.options-actions { - display: flex; - justify-content: flex-end; - gap: 8px; - margin-top: 10px; -} - -.list-container { - padding: 8px; - overflow: visible; -} - -.loading, -.empty { - padding: 26px; - color: var(--muted); - text-align: center; -} - -.links { - display: grid; - gap: 10px; -} - -.link-item { - display: grid; - grid-template-columns: 1.4fr .9fr .9fr; - gap: 12px; - padding: 14px; - border-radius: 14px; - border: 1px solid var(--border); - background: rgba(255, 255, 255, 0.03); - transition: transform .15s ease, background .2s ease, border-color .2s ease; -} - -.link-item:hover { - background: rgba(255, 255, 255, 0.06); - border-color: rgba(255, 255, 255, 0.18); - transform: translateY(-1px); -} - -.link-main { - display: grid; - gap: 6px; -} - -.link-short { - color: #bfdbfe; - text-decoration: none; - width: fit-content; -} - -.link-long { - color: var(--muted); - word-break: break-all; - font-size: 13px; -} - -.link-meta { - display: flex; - flex-wrap: wrap; - gap: 6px; - align-content: start; -} - -.chip { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 6px 10px; - border-radius: 999px; - border: 1px solid var(--border-2); - background: rgba(255, 255, 255, 0.06); - color: var(--text); - font-size: 12px; - font-weight: 500; -} - -.chip.status.ACTIVE { - background: rgba(34, 197, 94, 0.15); - border-color: rgba(34, 197, 94, 0.32); - color: #86efac; -} - -.link-stats { - display: grid; - grid-auto-flow: column; - gap: 12px; - align-content: start; -} - -.stat { - display: flex; - flex-direction: column; - gap: 2px; -} - -.label { - color: var(--muted); - font-size: 11px; - letter-spacing: .3px; - text-transform: uppercase; -} - -.value { - color: var(--text); - font-size: 13px; -} - -.pagination-bar { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - padding: 10px 20px 10px 10px; -} - -.pager { - display: inline-flex; - align-items: center; - gap: 8px; -} - -.pager .btn { - padding: 8px 12px; -} - -.page-info { - color: var(--muted); -} - -.fade-in { - animation: fade-in .24s ease both; -} - -@keyframes fade-in { - from { - opacity: 0; - transform: translateY(2px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -@media (max-width: 1024px) { - .options-dropdown { - min-width: 560px; - } - - .options-grid { - grid-template-columns: repeat(2, minmax(160px, 1fr)); - } - - .link-item { - grid-template-columns: 1fr; - } -} - -@media (max-width: 720px) { - .dashboard { - grid-template-columns: 1fr; - } - - .sidebar { - position: static; - height: auto; - } - - .options-dropdown { - position: fixed; - right: 12px; - left: 12px; - top: 80px; - min-width: auto; - } - - .options-grid { - grid-template-columns: 1fr; - } -} \ No newline at end of file diff --git a/static/css/dashboard/apps.css b/static/css/dashboard/apps.css index bc56f714..5c634ec9 100644 --- a/static/css/dashboard/apps.css +++ b/static/css/dashboard/apps.css @@ -104,7 +104,7 @@ } .app-card:hover .kebab-btn, -.kebab-menu:has(.kebab-dropdown.open) .kebab-btn, +.kebab-menu.is-open .kebab-btn, .kebab-btn:focus { opacity: 1; } @@ -114,59 +114,18 @@ background: rgba(255, 255, 255, 0.08); } -.kebab-dropdown { - position: absolute; - top: 100%; - right: 0; - margin-top: 4px; - background: rgba(12, 14, 24, 0.98); - backdrop-filter: blur(20px); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: var(--radius-md, 8px); - padding: 4px; +.kebab-menu-list { min-width: 150px; - z-index: 100; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(0, 0, 0, 0.2); - opacity: 0; - visibility: hidden; - transform: translateY(6px); - transition: all 0.2s; -} - -.kebab-dropdown.open { - opacity: 1; - visibility: visible; - transform: translateY(0); } -.kebab-item { - display: flex; - align-items: center; - gap: 10px; - width: 100%; - padding: 8px 10px; - border: none; - background: transparent; - color: var(--text-secondary, #999); - font-size: 14px; - cursor: pointer; - border-radius: var(--radius-sm, 6px); - transition: all 0.15s; -} - -.kebab-item i { +.kebab-menu-list .dropdown-item i { width: 18px; height: 18px; flex-shrink: 0; font-size: 18px; } -.kebab-item:hover { - background: var(--hover-bg, rgba(255, 255, 255, 0.06)); - color: var(--text-primary, #e5e5e5); -} - -.kebab-item--danger:hover { +.kebab-menu-list .dropdown-item.kebab-item--danger:hover { background: rgba(239, 68, 68, 0.1); color: #ef4444; } diff --git a/static/css/dashboard/billing.css b/static/css/dashboard/billing.css index 3d9cef3f..6e59ba03 100644 --- a/static/css/dashboard/billing.css +++ b/static/css/dashboard/billing.css @@ -63,7 +63,7 @@ gap: 1rem; } -.detail-item > i { +.detail-item>i { font-size: 1.5rem; color: var(--accent-primary); opacity: 0.8; @@ -120,7 +120,7 @@ transform: translateY(-2px); } -.features-grid .feature-item > i { +.features-grid .feature-item>i { font-size: 1.5rem; color: var(--accent-primary); flex-shrink: 0; @@ -178,4 +178,4 @@ .features-grid .feature-item:hover { background: rgba(99, 102, 241, 0.15); } -} +} \ No newline at end of file diff --git a/static/css/dashboard/dashboard-base.css b/static/css/dashboard/dashboard-base.css index 5c3c3840..bd8ac49b 100644 --- a/static/css/dashboard/dashboard-base.css +++ b/static/css/dashboard/dashboard-base.css @@ -1,3 +1,45 @@ +@import url('https://fonts.googleapis.com/css2?family=Allerta+Stencil&display=swap'); + +:root { + /* Z-Index Scale */ + --z-dropdown: 100; + --z-sticky: 200; + --z-overlay: 1000; + --z-modal: 2000; + --z-notification: 3000; + --z-max: 9999; + + /* Status Colors */ + --color-danger: #ef4444; + --color-danger-hover: #dc2626; + --color-success: #22c55e; + --color-warning: #f59e0b; + --color-info: #6366f1; +} + +body::-webkit-scrollbar { + display: none; +} + +body::-webkit-scrollbar-track { + background-color: transparent; +} + +* { + box-sizing: border-box; + -webkit-tap-highlight-color: transparent; +} + +body { + font-family: 'Allerta Stencil', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + margin: 0; + padding: 0; + background-attachment: fixed; + background-repeat: no-repeat; + background-color: #333; + -ms-overflow-style: none; +} + /* Dashboard Layout Variables */ :root { --sidebar-width: 260px; @@ -55,7 +97,7 @@ body.dashboard-layout { display: flex; flex-direction: column; transition: width var(--transition-speed) var(--transition-easing); - z-index: 100; + z-index: var(--z-dropdown); } .dashboard-sidebar.collapsed { @@ -89,12 +131,28 @@ body.dashboard-layout { } .brand-logo img { - /* width: 32px; */ height: 26px; margin-top: 5px; flex-shrink: 0; } +/* Show the full logo-with-text when expanded, swap to the square favicon + when collapsed. Both imgs are rendered so no flash on toggle. */ +.brand-logo-icon { + display: none; + height: 28px !important; + width: 28px; + margin-top: 0 !important; +} + +.collapsed .brand-logo-full { + display: none; +} + +.collapsed .brand-logo-icon { + display: block; +} + .brand-text { font-size: 20px; font-weight: 700; @@ -104,8 +162,29 @@ body.dashboard-layout { transition: opacity var(--transition-speed) var(--transition-easing), width var(--transition-speed) var(--transition-easing); } +/* Collapsed: stack logo + toggle in the same slot and crossfade on hover. + Logo is visible at rest; hovering the header reveals the expand button. + `pointer-events: none` on the logo lets clicks on the logo-area pass + through to the toggle button underneath, so clicking "the logo" expands + the sidebar without needing to hover first. */ +.collapsed .sidebar-header { + position: relative; + justify-content: center; +} + .collapsed .sidebar-brand { - display: none; + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + opacity: 1; + pointer-events: none; + transition: opacity 0.15s ease; +} + +.collapsed .sidebar-header:hover .sidebar-brand { + opacity: 0; } .sidebar-toggle { @@ -136,10 +215,18 @@ body.dashboard-layout { padding: 20px 15px; } -/* Ensure toggle button is always visible */ +/* Toggle is always rendered + clickable; opacity fades in on header hover. */ .collapsed .sidebar-toggle { flex-shrink: 0; margin: 0 auto; + opacity: 0; + transition: opacity 0.15s ease; +} + +.collapsed .sidebar-header:hover .sidebar-toggle, +.collapsed .sidebar-toggle:focus, +.collapsed .sidebar-toggle:focus-visible { + opacity: 1; } /* Sidebar Navigation */ @@ -246,8 +333,9 @@ body.dashboard-layout { margin-top: auto; } -.profile-dropdown { +.profile-dropdown.dropdown { position: relative; + display: block; } .profile-button { @@ -348,31 +436,24 @@ body.dashboard-layout { width: 0; } -.profile-menu { - position: absolute; +.profile-dropdown > .profile-menu { + /* Opens upward — override primitive's default top placement. */ + top: auto; bottom: 100%; left: 0; right: 0; margin-bottom: 8px; background: rgba(20, 25, 40, 0.98); - backdrop-filter: blur(20px); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); padding: 8px; - opacity: 0; - visibility: hidden; transform: translateY(10px); - transition: all 0.3s; box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.4); } -.profile-menu.active { - opacity: 1; - visibility: visible; +.profile-dropdown.is-open > .profile-menu { transform: translateY(0); } -.collapsed .profile-menu { +.collapsed .profile-dropdown > .profile-menu { left: 100%; right: auto; bottom: 0; @@ -421,7 +502,7 @@ body.dashboard-layout { margin: 8px 0; } -.profile-button.active .profile-chevron { +.profile-button.is-active .profile-chevron { transform: rotate(180deg); } @@ -486,7 +567,7 @@ body.dashboard-layout { background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.02)); backdrop-filter: blur(20px) saturate(1.2); border-bottom: 1px solid var(--border-color); - z-index: 200; + z-index: var(--z-sticky); padding: 0 16px; align-items: center; justify-content: space-between; @@ -544,7 +625,7 @@ body.dashboard-layout { opacity: 0; visibility: hidden; transition: all 0.3s ease; - z-index: 150; + z-index: var(--z-sticky); backdrop-filter: blur(4px); } @@ -574,7 +655,7 @@ body.dashboard-layout { height: 100vh; transform: translateX(-100%); transition: transform var(--transition-speed) var(--transition-easing); - z-index: 160; + z-index: var(--z-sticky); /* Remove collapsed width behavior on mobile */ width: 280px !important; } @@ -585,9 +666,11 @@ body.dashboard-layout { transform: translateX(-100%); } - /* Show sidebar when mobile-open */ + /* Show sidebar when mobile-open — stack above the overlay so its + backdrop-filter doesn't smear the sidebar content. */ .dashboard-sidebar.mobile-open { transform: translateX(0); + z-index: calc(var(--z-sticky) + 1); } /* Always show nav text on mobile */ @@ -605,6 +688,15 @@ body.dashboard-layout { display: flex !important; } + /* Force the full logo even when the desktop-collapsed state is cached. */ + .dashboard-sidebar.collapsed .brand-logo-full { + display: block; + } + + .dashboard-sidebar.collapsed .brand-logo-icon { + display: none; + } + /* Main content adjustments */ .dashboard-main { margin-left: 0 !important; @@ -636,7 +728,7 @@ body.dashboard-layout { margin-bottom: 8px; width: auto; /* Ensure it appears above the button on mobile */ - z-index: 1000; + z-index: var(--z-overlay); } /* Override collapsed positioning on mobile */ @@ -676,4 +768,94 @@ body.dashboard-layout { to { transform: rotate(360deg); } -} \ No newline at end of file +} + +.dropdown { + position: relative; + display: inline-flex; + align-items: center; +} + +.dropdown-trigger { + cursor: pointer; + transition: background-color 0.15s ease, color 0.15s ease; +} + +.dropdown-trigger .ti-chevron-down { + transition: transform 0.2s ease; +} + +.dropdown-trigger.is-active .ti-chevron-down { + transform: rotate(180deg); +} + +.dropdown-menu { + position: absolute; + top: calc(100% + 8px); + left: 0; + min-width: 160px; + background: rgba(15, 20, 35, 0.95); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 8px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); + backdrop-filter: blur(16px); + z-index: var(--z-overlay); + padding: 4px; + overflow: hidden; + visibility: hidden; + opacity: 0; + transform: translateY(-8px); + transition: opacity 0.15s ease-out, transform 0.15s ease-out, visibility 0s linear 0.15s; +} + +.dropdown.is-open > .dropdown-menu { + visibility: visible; + opacity: 1; + transform: translateY(0); + transition: opacity 0.15s ease-out, transform 0.15s ease-out, visibility 0s linear 0s; +} + +.dropdown-menu--align-right { + left: auto; + right: 0; +} + +.dropdown-item { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 8px 12px; + background: transparent; + border: none; + border-radius: 6px; + color: rgba(255, 255, 255, 0.8); + font-size: 14px; + font-weight: 500; + text-align: left; + cursor: pointer; + transition: background-color 0.15s ease, color 0.15s ease; +} + +.dropdown-item:hover, +.dropdown-item:focus-visible { + outline: none; + background: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.95); +} + +.dropdown-item.is-active { + background: rgba(124, 58, 237, 0.18); + color: #fff; +} + +.dropdown-item[disabled] { + opacity: 0.4; + cursor: not-allowed; +} + +.dropdown-divider { + height: 1px; + margin: 4px 0; + background: rgba(255, 255, 255, 0.08); +} diff --git a/static/css/dashboard/dateRangePicker.css b/static/css/dashboard/dateRangePicker.css index 46b59d71..63d8ead3 100644 --- a/static/css/dashboard/dateRangePicker.css +++ b/static/css/dashboard/dateRangePicker.css @@ -62,7 +62,7 @@ border-radius: 12px; box-shadow: rgba(0, 0, 0, 0.3) 0 20px 40px 0px; backdrop-filter: blur(16px); - z-index: 1000; + z-index: var(--z-overlay); min-width: 700px; max-height: 500px; overflow: hidden; @@ -151,7 +151,7 @@ .relative-option.selected { background: rgba(124, 58, 237, 0.3); color: white; - border-left: 3px solid #7c3aed; + border-left: 3px solid var(--accent-primary); } /* Custom Section */ @@ -203,12 +203,12 @@ .input-group input:focus { outline: none; - border-color: #7c3aed; + border-color: var(--accent-primary); background: rgba(255, 255, 255, 0.15); } .input-group input.invalid:focus { - border-color: #ef4444; + border-color: var(--color-danger); background: rgba(239, 68, 68, 0.1); } @@ -219,7 +219,7 @@ .apply-btn { padding: 10px 20px; - background: #7c3aed; + background: var(--accent-primary); color: white; border: none; border-radius: 6px; @@ -232,7 +232,7 @@ } .apply-btn:hover { - background: #6d28d9; + background: var(--accent-primary); } .apply-btn:disabled { @@ -343,7 +343,7 @@ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), visibility 0s linear 0.3s; - z-index: 10000 !important; + z-index: var(--z-modal) !important; overflow-y: auto; -webkit-overflow-scrolling: touch; @@ -370,20 +370,37 @@ visibility 0s linear 0s; } - /* Handle bar for bottom sheet */ - .date-range-dropdown::before { - content: ''; + /* Functional handle: swipe down or tap to dismiss. */ + .date-range-dropdown .sheet-handle { + display: flex; + justify-content: center; + align-items: center; + height: 20px; + width: 100%; + touch-action: none; + cursor: grab; + user-select: none; position: sticky; - top: 12px; - left: 50%; - transform: translateX(-50%); + top: 0; + background: rgba(15, 20, 35, 0.98); + z-index: 10; + } + + .date-range-dropdown .sheet-handle::before { + content: ''; width: 36px; height: 4px; - background: rgba(255, 255, 255, 0.3); border-radius: 2px; - display: block; - margin: 0 auto 8px; - z-index: 10; + background: rgba(255, 255, 255, 0.3); + transition: background 0.2s ease; + } + + .date-range-dropdown .sheet-handle:active { + cursor: grabbing; + } + + .date-range-dropdown .sheet-handle:active::before { + background: rgba(255, 255, 255, 0.5); } /* Backdrop overlay */ @@ -458,13 +475,13 @@ .relative-option:focus, .history-item:focus, .apply-btn:focus { - outline: 2px solid #7c3aed; + outline: 2px solid var(--accent-primary); outline-offset: 2px; } /* Error states */ .input-error { - color: #ef4444; + color: var(--color-danger); font-size: 12px; margin-top: 5px; display: block; diff --git a/static/css/dashboard/keys.css b/static/css/dashboard/keys.css index 9fd66c91..ee087748 100644 --- a/static/css/dashboard/keys.css +++ b/static/css/dashboard/keys.css @@ -99,8 +99,11 @@ } .btn-primary:hover { - background: #6d31d8; - border-color: #6d31d8; + background: var(--accent-primary); + border-color: var(--accent-primary); + filter: brightness(1.12); + transform: translateY(-1px); + box-shadow: 0 6px 18px rgba(124, 58, 237, 0.35); } .btn-ghost { @@ -114,8 +117,8 @@ .btn-danger { background: transparent; - border-color: #ef4444; - color: #ef4444; + border-color: var(--color-danger); + color: var(--color-danger); } .btn-danger:hover:not(:disabled) { @@ -274,19 +277,19 @@ .status-active { background: rgba(34, 197, 94, 0.1); - color: #22c55e; + color: var(--color-success); border: 1px solid rgba(34, 197, 94, 0.2); } .status-revoked { background: rgba(239, 68, 68, 0.1); - color: #ef4444; + color: var(--color-danger); border: 1px solid rgba(239, 68, 68, 0.2); } .status-expired { background: rgba(245, 158, 11, 0.1); - color: #f59e0b; + color: var(--color-warning); border: 1px solid rgba(245, 158, 11, 0.2); } @@ -300,7 +303,7 @@ display: flex; align-items: center; justify-content: center; - z-index: 10000; + z-index: var(--z-modal); padding: 20px; } @@ -726,7 +729,7 @@ .key-success-modal .success-icon { background: rgba(34, 197, 94, 0.1); - color: #22c55e; + color: var(--color-success); border: 2px solid rgba(34, 197, 94, 0.2); } @@ -841,7 +844,7 @@ display: flex; align-items: center; gap: 10px; - color: #f59e0b; + color: var(--color-warning); font-size: 13px; } @@ -912,6 +915,9 @@ color: var(--text-secondary); line-height: 1.6; margin: 0; + max-width: 440px; + margin-left: auto; + margin-right: auto; } /* Animations */ @@ -957,6 +963,8 @@ @media (max-width: 768px) { .dashboard-content { padding: 0; + display: flex; + flex-direction: column; } .page-header { @@ -994,6 +1002,9 @@ padding: 0; border-radius: 0; margin: 0; + flex: 1; + display: flex; + flex-direction: column; } .keys-toolbar { @@ -1016,6 +1027,7 @@ /* Constrain to parent width */ max-width: 100%; /* Never exceed parent */ + flex: 1; } /* Style scrollbar */ @@ -1138,4 +1150,157 @@ .permission-description { font-size: 11px; } +} + +/* Delete Key Confirmation Modal (duplicated from links url-modal delete styles) */ +#delete-key-modal { + display: none; + position: fixed; + inset: 0; + z-index: var(--z-max); + opacity: 0; + visibility: hidden; + transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1), visibility 0.4s cubic-bezier(0.4, 0, 0.2, 1); +} + +#delete-key-modal.active { + display: flex; + opacity: 1; + visibility: visible; +} + +#delete-key-modal .modal-container { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + padding: 20px; + position: relative; + z-index: 2; + background: rgba(0, 0, 0, 0.7); + -webkit-backdrop-filter: blur(10px) saturate(180%) brightness(0.7); + backdrop-filter: blur(10px) saturate(180%) brightness(0.7); +} + +#delete-key-modal .modal-content { + backdrop-filter: blur(60px); + background: rgba(15, 20, 35, 0.5); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 16px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05); + width: 100%; + max-width: 500px; + max-height: 90vh; + overflow: hidden; + transform: scale(0.85) translateY(20px); + opacity: 0; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); +} + +#delete-key-modal.active .modal-content { + transform: scale(1) translateY(0); + opacity: 1; +} + +#delete-key-modal .modal-header { + display: flex; + align-items: center; + padding: 20px 24px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +#delete-key-modal .modal-title-section-delete { + display: flex; + align-items: center; + gap: 12px; +} + +#delete-key-modal .modal-icon { + width: 48px; + height: 48px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + flex-shrink: 0; + background: linear-gradient(135deg, #ef4444, #dc2626); + color: white; +} + +#delete-key-modal .modal-title { + font-size: 20px; + font-weight: 600; + color: var(--text-primary); + margin: 0; + line-height: 1.3; +} + +#delete-key-modal .modal-body { + padding: 20px 24px; +} + +#delete-key-modal .modal-body p { + color: var(--text-secondary); + margin: 0; + line-height: 1.5; + font-size: 14px; +} + +#delete-key-modal .modal-footer { + display: flex; + align-items: center; + justify-content: flex-end; + padding: 16px 24px; + border-top: 1px solid rgba(255, 255, 255, 0.08); + gap: 10px; +} + +#delete-key-modal .btn-cancel { + background: rgba(255, 255, 255, 0.1); + color: var(--text-secondary); + border-color: rgba(255, 255, 255, 0.2); +} + +#delete-key-modal .btn-cancel:hover { + background: rgba(255, 255, 255, 0.15); + color: var(--text-primary); + border-color: rgba(255, 255, 255, 0.3); +} + +#delete-key-modal .btn-delete { + background: var(--color-danger); + color: white; + border-color: var(--color-danger); +} + +#delete-key-modal .btn-delete:hover:not(:disabled) { + background: var(--color-danger-hover); + border-color: var(--color-danger-hover); +} + +@media (max-width: 768px) { + #delete-key-modal .modal-container { + padding: 16px; + } + + #delete-key-modal .modal-content { + max-width: none; + max-height: 95vh; + } + + #delete-key-modal .modal-body { + padding: 24px 20px; + } + + #delete-key-modal .modal-footer { + padding: 20px; + flex-direction: column-reverse; + gap: 10px; + } + + #delete-key-modal .modal-footer .btn { + width: 100%; + } } \ No newline at end of file diff --git a/static/css/dashboard/links.css b/static/css/dashboard/links.css index cb13ccfc..1120afe5 100644 --- a/static/css/dashboard/links.css +++ b/static/css/dashboard/links.css @@ -35,9 +35,17 @@ } .page-header-actions .btn:hover { - background: #6d31d8; - border-color: #6d31d8; + background: var(--accent-primary); + border-color: var(--accent-primary); text-decoration: none; + filter: brightness(1.12); + transform: translateY(-1px); + box-shadow: 0 6px 18px rgba(124, 58, 237, 0.35); +} + +/* Success modal must sit above all other modals on this page */ +#link-success-modal { + z-index: calc(var(--z-max) + 1); } /* Success Modal Close Button */ @@ -308,7 +316,7 @@ .success-icon { background: rgba(34, 197, 94, 0.1); - color: #22c55e; + color: var(--color-success); } .success-icon i { @@ -318,12 +326,12 @@ /* Form Error States */ .field input.error, .field textarea.error { - border-color: #ef4444; + border-color: var(--color-danger); background: rgba(239, 68, 68, 0.05); } .field-error { - color: #ef4444; + color: var(--color-danger); font-size: 12px; margin-top: 4px; display: block; @@ -362,7 +370,7 @@ gap: 20px; flex-wrap: wrap; position: relative; - z-index: 100; + z-index: var(--z-dropdown); padding: 20px 24px; border-bottom: 1px solid var(--border-color); background: rgba(255, 255, 255, 0.02); @@ -442,8 +450,11 @@ } .btn-primary:hover { - background: #6d31d8; - border-color: #6d31d8; + background: var(--accent-primary); + border-color: var(--accent-primary); + filter: brightness(1.12); + transform: translateY(-1px); + box-shadow: 0 6px 18px rgba(124, 58, 237, 0.35); } .btn-ghost { @@ -455,15 +466,10 @@ background: rgba(255, 255, 255, 0.08); } -.caret { - font-size: 10px; - opacity: 0.7; -} - /* Options Dropdown */ .options { position: relative; - z-index: 101; + z-index: var(--z-dropdown); } .options-dropdown { @@ -479,14 +485,14 @@ border-radius: 12px; backdrop-filter: blur(60px); box-shadow: rgba(0, 0, 0, 0.3) 0 20px 40px 0px; - z-index: 102; + z-index: var(--z-dropdown); visibility: hidden; opacity: 0; transform: translateY(-8px); transition: opacity 0.15s ease-out, transform 0.15s ease-out, visibility 0s linear 0.15s; } -.options-dropdown.show { +.options.dropdown.is-open > .options-dropdown { visibility: visible; opacity: 1; transform: translateY(0); @@ -526,13 +532,14 @@ .datetime-wrapper input, .field select, .field input[type="datetime-local"] { - padding: 8px 12px; - padding-right: 36px; + padding: 0 36px 0 12px; background: rgba(255, 255, 255, 0.05); border: 1px solid var(--border-color); border-radius: var(--radius-sm); color: var(--text-primary); font-size: 14px; + height: 36px; + box-sizing: border-box; transition: all 0.2s; width: 100%; -webkit-appearance: none; @@ -587,16 +594,19 @@ input[type="datetime-local"]::-webkit-inner-spin-button { /* Segmented Control */ .seg { display: flex; + align-items: stretch; background: rgba(255, 255, 255, 0.05); border: 1px solid var(--border-color); border-radius: var(--radius-sm); overflow: hidden; position: relative; + height: 36px; + box-sizing: border-box; } .seg button { flex: 1; - padding: 8px; + padding: 0 8px; background: transparent; border: none; color: var(--text-secondary); @@ -746,7 +756,7 @@ input[type="datetime-local"]::-webkit-inner-spin-button { } .col-short-url .link-short:hover { - color: #6d31d8; + color: var(--accent-primary); } .col-long-url { @@ -803,7 +813,7 @@ input[type="datetime-local"]::-webkit-inner-spin-button { .badge-status.badge-active { background: rgba(34, 197, 94, 0.1); - color: #22c55e; + color: var(--color-success); border: 1px solid rgba(34, 197, 94, 0.2); } @@ -835,13 +845,13 @@ input[type="datetime-local"]::-webkit-inner-spin-button { .badge-password { background: rgba(245, 158, 11, 0.1); - color: #f59e0b; + color: var(--color-warning); border: 1px solid rgba(245, 158, 11, 0.2); } .badge-max-clicks { background: rgba(37, 99, 235, 0.1); - color: #2563eb; + color: var(--accent-secondary); border: 1px solid rgba(37, 99, 235, 0.2); } @@ -902,6 +912,19 @@ input[type="datetime-local"]::-webkit-inner-spin-button { color: var(--text-secondary); line-height: 1.6; margin: 0; + max-width: 440px; + margin-left: auto; + margin-right: auto; +} + +.empty-state .empty-cta.btn-primary { + margin-top: 28px; + background: var(--accent-primary); + border: 1px solid var(--accent-primary); + color: white; + padding: 10px 16px; + font-size: 14px; + font-weight: 500; } /* Pagination */ @@ -990,6 +1013,8 @@ input[type="datetime-local"]::-webkit-inner-spin-button { @media (max-width: 768px) { .dashboard-content { padding: 0 !important; + display: flex; + flex-direction: column; } .page-header { @@ -1041,15 +1066,14 @@ input[type="datetime-local"]::-webkit-inner-spin-button { transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), visibility 0s linear 0.3s; - z-index: 10000 !important; + z-index: var(--z-modal) !important; /* Enable scrolling for long content */ overflow-y: auto; -webkit-overflow-scrolling: touch; } - .options-dropdown[style*="display: block"], - .options-dropdown[style*="display:block"] { + .options.dropdown.is-open > .options-dropdown { visibility: visible !important; opacity: 1 !important; transform: translateY(0) !important; @@ -1073,28 +1097,59 @@ input[type="datetime-local"]::-webkit-inner-spin-button { pointer-events: none; } - .options-dropdown[style*="display: block"]::before, - .options-dropdown[style*="display:block"]::before { + .options.dropdown.is-open > .options-dropdown::before { opacity: 1; pointer-events: auto; } - /* Add handle bar at top of bottom sheet */ - .options-dropdown::after { + /* Handle bar: visible + functional drag/tap-to-close zone. */ + .options-dropdown .sheet-handle { + display: flex; + justify-content: center; + align-items: center; + height: 20px; + width: 100%; + touch-action: none; + cursor: grab; + user-select: none; + position: sticky; + top: 0; + background: rgba(15, 20, 35, 0.98); + z-index: 3; + } + + .options-dropdown .sheet-handle::before { content: ''; - position: absolute; - top: 12px; - left: 50%; - transform: translateX(-50%); width: 36px; height: 4px; - background: rgba(255, 255, 255, 0.3); border-radius: 2px; + background: rgba(255, 255, 255, 0.3); + transition: background 0.2s ease; + } + + .options-dropdown .sheet-handle:active { + cursor: grabbing; + } + + .options-dropdown .sheet-handle:active::before { + background: rgba(255, 255, 255, 0.5); } - /* Adjust padding for mobile bottom sheet */ + /* Adjust padding for mobile bottom sheet (handle bar owns the top space). */ .options-dropdown .options-grid { - padding: 32px 20px 20px; + padding: 4px 20px 20px; + } + + /* Pin Apply/Reset to the bottom of the bottom sheet so the user never + has to scroll to submit. */ + .options-dropdown .options-actions { + position: sticky; + bottom: 0; + padding: 16px 20px calc(16px + env(safe-area-inset-bottom)); + margin: 0; + background: rgba(15, 20, 35, 0.98); + backdrop-filter: blur(20px); + z-index: 2; } /* Fix the container width to viewport */ @@ -1110,6 +1165,9 @@ input[type="datetime-local"]::-webkit-inner-spin-button { margin: 0; backdrop-filter: none; /* Remove backdrop-filter to allow fixed positioning */ + flex: 1; + display: flex; + flex-direction: column; } .toolbar { @@ -1136,6 +1194,7 @@ input[type="datetime-local"]::-webkit-inner-spin-button { /* Constrain to parent width */ max-width: 100%; /* Never exceed parent */ + flex: 1; } /* Style scrollbar */ diff --git a/static/css/dashboard/settings.css b/static/css/dashboard/settings.css new file mode 100644 index 00000000..6edf21ad --- /dev/null +++ b/static/css/dashboard/settings.css @@ -0,0 +1,679 @@ +.settings-grid { + display: grid; + gap: 24px; +} + +.settings-section { + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 24px; +} + +.section-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid var(--border-color); +} + +.section-icon { + width: 40px; + height: 40px; + border-radius: var(--radius-sm); + background: linear-gradient(135deg, rgba(124, 58, 237, 0.1), rgba(37, 99, 235, 0.1)); + display: flex; + align-items: center; + justify-content: center; + color: var(--accent-primary); +} + +.section-icon i { + font-size: 22px; +} + +.section-title { + font-size: 18px; + font-weight: 600; + color: var(--text-primary); +} + +.section-description { + font-size: 14px; + color: var(--text-secondary); + margin-top: 4px; +} + +.settings-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.settings-item:last-child { + border-bottom: none; +} + +.setting-label { + font-size: 14px; + color: var(--text-primary); +} + +.setting-value { + font-size: 14px; + color: var(--text-secondary); +} + +.placeholder-badge { + display: inline-block; + padding: 4px 12px; + background: rgba(124, 58, 237, 0.1); + color: var(--accent-primary); + border-radius: var(--radius-sm); + font-size: 12px; + font-weight: 500; +} + +.badge { + padding: 4px 8px; + border-radius: var(--radius-sm); + font-size: 12px; + font-weight: 500; +} + +.badge-success { + background: rgba(34, 197, 94, 0.1); + color: #22c55e; +} + +.badge-warning { + background: rgba(245, 158, 11, 0.1); + color: #f59e0b; +} + +.btn { + padding: 6px 12px; + border: none; + border-radius: var(--radius-sm); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 6px; +} + +.btn-sm { + padding: 4px 8px; + font-size: 12px; +} + +.btn-primary { + background: var(--accent-primary); + color: white; +} + +.btn-outline { + background: transparent; + border: 1px solid var(--border-color); + color: var(--text-primary); +} + +.btn-danger { + background: rgba(239, 68, 68, 0.1); + color: #ef4444; + border: 1px solid rgba(239, 68, 68, 0.2); +} + +.oauth-provider { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.oauth-provider:last-child { + border-bottom: none; +} + +.provider-info { + display: flex; + align-items: center; + gap: 12px; +} + +.provider-icon { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + font-size: 22px; + color: white; +} + +.provider-details { + display: flex; + flex-direction: column; + gap: 2px; +} + +.provider-name { + font-size: 14px; + color: var(--text-primary); + font-weight: 500; +} + +.provider-email { + font-size: 12px; + color: var(--text-secondary); +} + +.oauth-buttons { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +/* Profile Picture Selection Styles */ +.picture-grid { + display: flex; + gap: 16px; + margin-top: 16px; + flex-wrap: wrap; +} + +.picture-option { + position: relative; + cursor: pointer; + transition: transform 0.2s ease; +} + +.picture-option:hover:not(.disabled) { + transform: scale(1.05); +} + +.picture-option.disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.picture-option.disabled:hover { + transform: none; +} + +.picture-preview { + width: 64px; + height: 64px; + border-radius: 50%; + object-fit: cover; + border: 3px solid transparent; +} + +.picture-option.selected .picture-preview { + border-color: var(--accent-primary); +} + +.picture-placeholder { + width: 64px; + height: 64px; + border-radius: 50%; + background: #6b7280; + display: flex; + align-items: center; + justify-content: center; + border: 3px solid transparent; +} + +.picture-placeholder i { + font-size: 24px; + color: white; +} + +.current-indicator { + position: absolute; + top: -2px; + right: -2px; + width: 20px; + height: 20px; + background: var(--accent-primary); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 12px; + border: 2px solid var(--surface-bg); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.coming-soon-badge { + position: absolute; + top: -8px; + right: -8px; + background: var(--accent-secondary); + color: white; + font-size: 10px; + padding: 2px 6px; + border-radius: 8px; + font-weight: 500; + font-family: monospace; +} + +/* Set Password Modal Styles - Match dashboard modal patterns */ +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 9999; + opacity: 0; + visibility: hidden; + transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1), visibility 0.4s cubic-bezier(0.4, 0, 0.2, 1); +} + +.modal.active { + display: flex; + opacity: 1; + visibility: visible; +} + +.modal-backdrop { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + -webkit-backdrop-filter: blur(10px) saturate(180%) brightness(0.7); + backdrop-filter: blur(10px) saturate(180%) brightness(0.7); +} + +.modal-container { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + padding: 20px; + position: relative; + z-index: 2; + background: rgba(0, 0, 0, 0.7); + -webkit-backdrop-filter: blur(10px) saturate(180%) brightness(0.7); + backdrop-filter: blur(10px) saturate(180%) brightness(0.7); +} + +.modal-container.modal-sm { + margin: 0 auto; +} + +.modal-content { + backdrop-filter: blur(60px); + background: rgba(15, 20, 35, 0.5); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 16px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05); + width: 100%; + max-width: 550px; + max-height: 90vh; + overflow: hidden; + transform: scale(0.85) translateY(20px); + opacity: 0; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; +} + +.modal.active .modal-content { + transform: scale(1) translateY(0); + opacity: 1; +} + +.modal-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: 20px 21px 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.02); +} + +.modal-title-section { + display: flex; + align-items: center; + gap: 16px; + flex: 1; +} + +.modal-icon { + width: 48px; + height: 48px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + flex-shrink: 0; + background: linear-gradient(135deg, rgba(124, 58, 237, 0.1), rgba(37, 99, 235, 0.1)); + color: var(--accent-primary); +} + +.modal-title { + font-size: 24px; + font-weight: 600; + color: var(--text-primary); + margin: 0 0 4px 0; + line-height: 1.2; +} + +.modal-subtitle { + font-size: 14px; + color: var(--text-secondary); + margin: 0; + line-height: 1.4; +} + +.modal-close { + background: none; + border: none; + color: var(--text-secondary); + font-size: 24px; + cursor: pointer; + padding: 8px; + border-radius: 8px; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; +} + +.modal-close:hover { + background: rgba(255, 255, 255, 0.1); + color: var(--text-primary); +} + +.modal-body { + padding: 32px; + max-height: 60vh; + overflow-y: auto; +} + +.modal-body::-webkit-scrollbar { + width: 6px; +} + +.modal-body::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + border-radius: 3px; +} + +.modal-body::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 3px; +} + +.form-grid { + display: grid; + gap: 24px; +} + +.field { + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; + min-width: 0; +} + +.field label { + font-size: 14px; + font-weight: 500; + color: var(--text-primary); +} + +.field input { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 12px 16px; + color: var(--text-primary); + font-size: 14px; + transition: all 0.2s ease; + width: 100%; + box-sizing: border-box; + min-width: 0; +} + +.field input:focus { + outline: none; + border-color: var(--accent-primary); + background: rgba(255, 255, 255, 0.08); +} + +.field input::placeholder { + color: var(--text-secondary); +} + +.field-hint { + font-size: 12px; + color: var(--text-secondary); + line-height: 1.4; +} + +.modal-footer { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 12px; + padding: 24px 32px; + border-top: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.02); +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 10px 16px; + border-radius: 6px; + font-size: 13px; + font-weight: 500; + text-decoration: none; + border: 1px solid transparent; + cursor: pointer; + transition: all 0.15s ease; + white-space: nowrap; + min-height: 36px; +} + +.btn i { + font-size: 14px; +} + +.btn-primary { + background: #6366f1; + color: white; + border-color: #6366f1; +} + +.btn-primary:hover { + background: #5855eb; + border-color: #5855eb; +} + +.btn-primary:active { + background: #4f46e5; + border-color: #4f46e5; +} + +.btn-secondary { + background: rgba(255, 255, 255, 0.1); + color: var(--text-secondary); + border-color: rgba(255, 255, 255, 0.2); +} + +.btn-secondary:hover { + background: rgba(255, 255, 255, 0.15); + color: var(--text-primary); + border-color: rgba(255, 255, 255, 0.3); +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} + +.btn-settings { + background: var(--accent-primary); + color: white; + padding: 4px 8px; + border: none; + border-radius: var(--radius-sm); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 6px; +} + +.spinning { + animation: spin 1s linear infinite; +} + +/* Field Error Styles */ +.field input.error { + border-color: #ef4444 !important; + background: rgba(239, 68, 68, 0.05); +} + +.field-error { + color: #ef4444; + font-size: 12px; + margin-top: 4px; + display: block; +} + +/* Password Strength Indicator */ +.password-strength-container { + margin: 8px 0 0 0; + padding: 0; +} + +.password-strength-bar-container { + width: 100%; + height: 4px; + background: rgba(255, 255, 255, 0.1); + border-radius: 2px; + overflow: hidden; + margin-bottom: 4px; +} + +.password-strength-bar { + height: 100%; + width: 0%; + background: #ef4444; + border-radius: 2px; + transition: all 0.3s ease; +} + +.password-strength-label { + font-size: 12px; + color: #ef4444; + text-align: right; + font-weight: 500; +} + +/* Password Requirements */ +.password-requirements { + margin: 8px 0 0 0; + padding: 0; + list-style: none; + font-size: 12px; +} + +.password-requirements li { + display: flex; + align-items: center; + margin: 2px 0; + color: rgba(255, 255, 255, 0.7); + transition: color 0.2s ease; +} + +.password-requirements .requirement-icon { + margin-right: 6px; + font-weight: bold; + width: 12px; + text-align: center; +} + +.password-requirements .requirement-missing { + color: #ef4444; +} + +.password-requirements .requirement-missing .requirement-icon { + color: #ef4444; +} + +.password-requirements .requirement-met { + color: #22c55e; +} + +.password-requirements .requirement-met .requirement-icon { + color: #22c55e; +} + +/* Mobile Responsive */ +@media (max-width: 768px) { + .modal-container { + padding: 16px; + } + + .modal-content { + max-width: none; + max-height: 95vh; + } + + .modal-header, + .modal-body, + .modal-footer { + padding: 20px; + } + + .modal-title-section { + gap: 12px; + } + + .modal-icon { + width: 40px; + height: 40px; + font-size: 20px; + } + + .modal-title { + font-size: 20px; + } + + .modal-footer { + flex-direction: column; + gap: 12px; + } + + .modal-footer .btn { + width: 100%; + justify-content: center; + } +} \ No newline at end of file diff --git a/static/css/dashboard/statistics.css b/static/css/dashboard/statistics.css index d92c529e..72443d97 100644 --- a/static/css/dashboard/statistics.css +++ b/static/css/dashboard/statistics.css @@ -48,29 +48,6 @@ align-items: center; } -.time-selector { - padding: 10px 15px; - border-radius: 12px; - color: white; - background: rgba(255, 255, 255, 0.15); - border: 1px solid rgba(255, 255, 255, 0.18); - box-shadow: rgba(0, 0, 0, 0.16) 0 10px 36px 0px, rgba(0, 0, 0, 0.06) 0 0 0 1px; -} - -.time-selector:hover { - background: rgba(255, 255, 255, 0.25); - transition: background-color 0.2s ease-in-out; - cursor: pointer; -} - -.time-selector:focus { - outline: none; -} - -.time-selector option { - color: black; -} - /* Refresh Control */ .refresh-control { display: flex; @@ -113,7 +90,7 @@ justify-content: center; padding: 40px 20px; border-radius: 12px; - z-index: 10; + z-index: 10; } .chart-empty-state.hidden { @@ -137,9 +114,12 @@ } @keyframes float { - 0%, 100% { + + 0%, + 100% { transform: translateY(0px); } + 50% { transform: translateY(-10px); } @@ -199,75 +179,31 @@ display: flex; align-items: center; justify-content: center; - position: relative; min-height: 36px; } -.export-btn-wrapper { - position: relative; - display: inline-block; -} - .export-btn:hover { color: rgba(255, 255, 255, 0.95); background-color: rgba(255, 255, 255, 0.15); border-color: rgba(255, 255, 255, 0.2); } -.export-btn.active { +.export-btn.is-active { background: rgba(124, 58, 237, 0.15); border-color: rgba(124, 58, 237, 0.4); color: rgba(255, 255, 255, 0.95); } -/* Export Dropdown Menu */ -.export-dropdown-menu { - position: absolute; - background: rgba(15, 20, 35, 0.98); - backdrop-filter: blur(20px) saturate(1.2); - border: 1px solid rgba(255, 255, 255, 0.18); - border-radius: 12px; - padding: 8px; - opacity: 0; - visibility: hidden; - transform: translateY(-10px); - transition: all 0.2s ease; - box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4); - z-index: 1000; +.export-menu { min-width: 200px; - top: calc(100% + 8px); - left: -20px; -} - -.export-dropdown-menu.active { - opacity: 1; - visibility: visible; - transform: translateY(0); } -.export-menu-item { - width: 100%; - display: flex; - align-items: center; - gap: 12px; +.export-menu .dropdown-item { padding: 10px 14px; - border-radius: 8px; - color: rgba(255, 255, 255, 0.85); - background: transparent; - border: none; - text-align: left; - cursor: pointer; - transition: all 0.2s ease; - font-size: 14px; - font-weight: 500; -} - -.export-menu-item:hover { - background: rgba(255, 255, 255, 0.1); - color: white; + gap: 12px; } -.export-menu-item i { +.export-menu .dropdown-item i { font-size: 18px; flex-shrink: 0; } @@ -309,67 +245,8 @@ background-color: rgba(255, 255, 255, 0.1); } -.auto-refresh-btn.active { - /* background-color: rgba(124, 58, 237, 0.15); */ - color: rgba(255, 255, 255, 0.95); -} - -.auto-refresh-btn.active .ti-chevron-down { - transform: rotate(180deg); -} - -.auto-refresh-btn .ti-chevron-down { - transition: transform 0.2s ease; -} - -.auto-refresh-dropdown .dropdown-menu { - position: absolute; - top: calc(100% + 10px); - right: 0; - background: rgba(15, 20, 35, 0.95); - border: 1px solid rgba(255, 255, 255, 0.15); - border-radius: 8px; +.auto-refresh-dropdown .auto-refresh-menu { min-width: 80px; - z-index: 1000; - visibility: hidden; - box-shadow: rgba(0, 0, 0, 0.3) 0 20px 40px 0px; - backdrop-filter: blur(16px); - overflow: hidden; - opacity: 0; - transform: translateY(-8px); - transition: opacity 0.15s ease-out, transform 0.15s ease-out, visibility 0s linear 0.15s; -} - -.auto-refresh-dropdown .dropdown-menu.show { - visibility: visible; - opacity: 1; - transform: translateY(0); - transition: opacity 0.15s ease-out, transform 0.15s ease-out, visibility 0s linear 0s; -} - -.auto-refresh-dropdown .dropdown-item { - padding: 8px 12px; - color: rgba(255, 255, 255, 0.8); - cursor: pointer; - display: flex; - align-items: center; - gap: 6px; - transition: all 0.2s ease; - font-size: 14px; - font-weight: 500; -} - -.auto-refresh-dropdown .dropdown-item:hover { - background: rgba(255, 255, 255, 0.12); - color: rgba(255, 255, 255, 0.95); -} - -.auto-refresh-dropdown .dropdown-item:first-child { - border-radius: 8px 8px 0 0; -} - -.auto-refresh-dropdown .dropdown-item:last-child { - border-radius: 0 0 8px 8px; } /* Filters Dropdown Container */ @@ -398,7 +275,7 @@ color: rgba(255, 255, 255, 0.95); } -.filters-btn.active { +.filters-btn.is-active { background: rgba(124, 58, 237, 0.15); border-color: rgba(124, 58, 237, 0.4); color: rgba(255, 255, 255, 0.95); @@ -426,7 +303,7 @@ color: rgba(255, 255, 255, 0.6); } -.filters-btn.active .filters-chevron { +.filters-btn.is-active .filters-chevron { transform: rotate(180deg); color: rgba(255, 255, 255, 0.8); } @@ -443,7 +320,7 @@ border-radius: 12px; backdrop-filter: blur(16px); box-shadow: rgba(0, 0, 0, 0.3) 0 20px 40px 0px; - z-index: 9999; + z-index: var(--z-max); visibility: hidden; overflow: hidden; opacity: 0; @@ -451,7 +328,7 @@ transition: opacity 0.15s ease-out, transform 0.15s ease-out, visibility 0s linear 0.15s; } -.filters-dropdown.show { +.filters-dropdown-container.dropdown.is-open > .filters-dropdown { visibility: visible; opacity: 1; transform: translateY(0); @@ -560,12 +437,6 @@ color: rgba(255, 255, 255, 0.5); } -.filter-actions-separator { - height: 1px; - background: rgba(255, 255, 255, 0.1); - margin: 8px 16px; -} - /* Filter Values View */ .filter-values-view { padding: 8px; @@ -627,7 +498,7 @@ .values-search-input:focus { outline: none; - border-color: #7c3aed; + border-color: var(--accent-primary); background: rgba(255, 255, 255, 0.12); } @@ -657,36 +528,12 @@ text-align: center; } -.filters-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); - gap: 16px; - margin-bottom: 16px; -} - .filter-group { display: flex; flex-direction: column; gap: 8px; } -.filter-label { - display: flex; - align-items: center; - gap: 8px; - font-size: 12px; - font-weight: 600; - color: rgba(255, 255, 255, 0.8); - text-transform: uppercase; - letter-spacing: 0.5px; - margin-bottom: 4px; -} - -.filter-label i { - font-size: 14px; - color: rgba(255, 255, 255, 0.6); -} - /* Multi-Select Components */ .multi-select-wrapper { position: relative; @@ -694,12 +541,12 @@ } .multi-select-wrapper:has(.multi-select-dropdown.show) { - z-index: 10000; + z-index: var(--z-modal); } /* Fallback for browsers that don't support :has() */ .multi-select-wrapper.active { - z-index: 10000; + z-index: var(--z-modal); } .multi-select-trigger { @@ -756,7 +603,7 @@ border-radius: 12px; box-shadow: rgba(0, 0, 0, 0.3) 0 20px 40px; backdrop-filter: blur(20px); - z-index: 9999; + z-index: var(--z-max); max-height: 320px; visibility: hidden; overflow: hidden; @@ -785,28 +632,6 @@ } } -.dropdown-header { - padding: 16px; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - background: rgba(255, 255, 255, 0.02); - border-radius: 12px 12px 0 0; -} - -.dropdown-search { - position: relative; - display: flex; - align-items: center; - margin-bottom: 12px; -} - -.dropdown-search i { - position: absolute; - left: 12px; - color: rgba(255, 255, 255, 0.5); - font-size: 14px; - pointer-events: none; -} - .search-input { width: 100%; padding: 10px 12px 10px 36px; @@ -820,7 +645,7 @@ .search-input:focus { outline: none; - border-color: #7c3aed; + border-color: var(--accent-primary); background: rgba(255, 255, 255, 0.15); box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2); } @@ -829,11 +654,6 @@ color: rgba(255, 255, 255, 0.5); } -.dropdown-actions { - display: flex; - gap: 8px; -} - .select-all-btn, .clear-all-btn { padding: 8px 14px; @@ -917,8 +737,8 @@ } .option-item input[type="checkbox"]:checked+.checkmark { - background: #7c3aed; - border-color: #7c3aed; + background: var(--accent-primary); + border-color: var(--accent-primary); box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2); transform: scale(1.1); } @@ -974,23 +794,6 @@ position: relative; } -.country-flag { - width: 20px; - height: 15px; - font-size: 16px; - flex-shrink: 0; -} - -/* Filter Actions */ -.filters-actions { - display: flex; - justify-content: center; - padding-top: 16px; - border-top: 1px solid rgba(255, 255, 255, 0.1); -} - - - .clear-all-filters-btn { display: flex; align-items: center; @@ -1180,18 +983,6 @@ transition: opacity 0.3s ease, transform 0.3s ease; } -/* Fade transition classes */ -.chart-view-enter { - opacity: 0; - transform: translateY(10px); -} - -.chart-view-enter-active { - opacity: 1; - transform: translateY(0); - transition: opacity 0.3s ease, transform 0.3s ease; -} - .chart-view-exit { opacity: 1; transform: translateY(0); @@ -1228,12 +1019,6 @@ } /* Cascade Select Buttons */ -.cascade-select { - position: relative; - display: inline-block; - transition: opacity 0.3s ease; -} - .cascade-select[style*="pointer-events: none"] { cursor: not-allowed; } @@ -1241,8 +1026,8 @@ .cascade-btn { padding: 8px 12px; border-radius: 8px; - border: 1px solid rgba(255, 255, 255, 0.18); - background: rgba(255, 255, 255, 0.15); + border: 1px solid var(--accent-primary); + background: rgba(124, 58, 237, 0.3); color: white; cursor: pointer; font-size: 14px; @@ -1253,12 +1038,7 @@ } .cascade-btn:hover:not(:disabled) { - background: rgba(255, 255, 255, 0.25); -} - -.cascade-btn.active:not(:disabled) { - background: rgba(124, 58, 237, 0.3); - border-color: #7c3aed; + background: rgba(124, 58, 237, 0.45); } .cascade-btn:disabled { @@ -1268,65 +1048,20 @@ border-color: rgba(255, 255, 255, 0.1) !important; } -.cascade-dropdown { - position: absolute; - top: calc(100% + 5px); - right: 0; - background: rgba(30, 35, 50, 0.98); - border: 1px solid rgba(255, 255, 255, 0.18); - border-radius: 8px; - box-shadow: rgba(0, 0, 0, 0.3) 0 10px 30px; - backdrop-filter: blur(10px); - z-index: 1000; +.cascade-menu { min-width: 180px; - visibility: hidden; - overflow: hidden; - opacity: 0; - transform: translateY(-8px); - transition: opacity 0.15s ease-out, transform 0.15s ease-out, visibility 0s linear 0.15s; } -.cascade-dropdown.show { - visibility: visible; - opacity: 1; - transform: translateY(0); - transition: opacity 0.15s ease-out, transform 0.15s ease-out, visibility 0s linear 0s; -} - -.cascade-option { - width: 100%; - padding: 12px 16px; - border: none; - background: transparent; - color: rgba(255, 255, 255, 0.8); - cursor: pointer; - font-size: 14px; - text-align: left; - transition: all 0.2s ease; - display: flex; - align-items: center; +.cascade-menu .dropdown-item { + padding: 10px 14px; gap: 10px; } -.cascade-option:hover { - background: rgba(255, 255, 255, 0.1); - color: white; -} - -.cascade-option.active { - background: rgba(124, 58, 237, 0.3); - color: white; -} - -.cascade-option i { +.cascade-menu .dropdown-item i { font-size: 16px; width: 20px; } -.cascade-option span { - flex: 1; -} - /* Headings */ .chart-section h2, .clicks-counter-section h2 { @@ -1362,7 +1097,7 @@ display: flex; justify-content: center; align-items: center; - z-index: 1000; + z-index: var(--z-overlay); } .modal-content { @@ -1395,12 +1130,6 @@ padding: 5px; } -.date-inputs { - display: flex; - gap: 20px; - margin-bottom: 20px; -} - .input-group { flex: 1; } @@ -1412,27 +1141,6 @@ font-size: 0.9rem; } -.date-input { - width: 100%; - padding: 10px; - border-radius: 8px; - border: 1px solid rgba(255, 255, 255, 0.18); - background: rgba(255, 255, 255, 0.1); - color: white; - font-size: 0.9rem; -} - -.date-input:focus { - outline: none; - border-color: rgba(255, 255, 255, 0.4); -} - -.modal-actions { - display: flex; - gap: 15px; - justify-content: flex-end; -} - .btn-secondary, .btn-primary { padding: 10px 20px; @@ -1448,7 +1156,7 @@ } .btn-primary { - background: #7c3aed; + background: var(--accent-primary); color: white; } @@ -1457,10 +1165,6 @@ opacity: 0.8; } -.anychart-credits { - display: none !important; -} - /* Responsive Design */ @media screen and (max-width: 1200px) { .main-stats-container { @@ -1505,10 +1209,6 @@ gap: 10px; } - .filters-grid { - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 14px; - } } @media screen and (max-width: 768px) { @@ -1571,7 +1271,7 @@ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), visibility 0s linear 0.3s; - z-index: 9999; + z-index: var(--z-max); overflow-y: auto; -webkit-overflow-scrolling: touch; @@ -1579,7 +1279,7 @@ /* Remove desktop animation */ } - .filters-dropdown.show { + .filters-dropdown-container.dropdown.is-open > .filters-dropdown { visibility: visible; opacity: 1; transform: translateY(0); @@ -1605,33 +1305,52 @@ pointer-events: none; } - .filters-dropdown.show::before { + .filters-dropdown-container.dropdown.is-open > .filters-dropdown::before { opacity: 1; pointer-events: auto; } - /* Handle bar */ - .filters-dropdown::after { + /* Handle bar: visible + functional drag/tap-to-close zone. */ + .filters-dropdown .sheet-handle { + display: flex; + justify-content: center; + align-items: center; + height: 20px; + width: 100%; + touch-action: none; + cursor: grab; + user-select: none; + position: sticky; + top: 0; + background: rgba(15, 20, 35, 0.98); + z-index: 3; + } + + .filters-dropdown .sheet-handle::before { content: ''; - position: absolute; - top: 12px; - left: 50%; - transform: translateX(-50%); width: 36px; height: 4px; - background: rgba(255, 255, 255, 0.3); border-radius: 2px; - z-index: 1; + background: rgba(255, 255, 255, 0.3); + transition: background 0.2s ease; + } + + .filters-dropdown .sheet-handle:active { + cursor: grabbing; + } + + .filters-dropdown .sheet-handle:active::before { + background: rgba(255, 255, 255, 0.5); } .filter-types-list, .filter-values-view { - padding: 32px 20px 20px; + padding: 4px 20px 20px; min-height: 300px; } /* Mobile Bottom Sheet for Auto-Refresh Dropdown */ - .auto-refresh-dropdown .dropdown-menu { + .auto-refresh-dropdown .auto-refresh-menu { position: fixed; top: auto; right: 0; @@ -1639,21 +1358,16 @@ bottom: 0; width: 100%; max-width: 100%; + min-width: unset; border-radius: 24px 24px 0 0; box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.4); - display: block; - visibility: hidden; - opacity: 0; transform: translateY(100%); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), visibility 0s linear 0.3s; - min-width: unset; } - .auto-refresh-dropdown .dropdown-menu.show { - visibility: visible; - opacity: 1; + .auto-refresh-dropdown.is-open .auto-refresh-menu { transform: translateY(0); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), @@ -1661,7 +1375,7 @@ } /* Backdrop for auto-refresh */ - .auto-refresh-dropdown .dropdown-menu::before { + .auto-refresh-dropdown .auto-refresh-menu::before { content: ''; position: fixed; top: 0; @@ -1675,31 +1389,47 @@ pointer-events: none; } - .auto-refresh-dropdown .dropdown-menu.show::before { + .auto-refresh-dropdown.is-open .auto-refresh-menu::before { opacity: 1; pointer-events: auto; } - /* Handle bar for auto-refresh */ - .auto-refresh-dropdown .dropdown-menu::after { + /* Functional handle: swipe down or tap to dismiss. */ + .auto-refresh-dropdown .auto-refresh-menu .sheet-handle { + display: flex; + justify-content: center; + align-items: center; + height: 20px; + width: 100%; + touch-action: none; + cursor: grab; + user-select: none; + position: sticky; + top: 0; + background: rgba(15, 20, 35, 0.98); + z-index: 3; + } + + .auto-refresh-dropdown .auto-refresh-menu .sheet-handle::before { content: ''; - position: absolute; - top: 12px; - left: 50%; - transform: translateX(-50%); width: 36px; height: 4px; - background: rgba(255, 255, 255, 0.3); border-radius: 2px; + background: rgba(255, 255, 255, 0.3); + transition: background 0.2s ease; } - .auto-refresh-dropdown .dropdown-item { - padding: 16px 20px; - font-size: 16px; + .auto-refresh-dropdown .auto-refresh-menu .sheet-handle:active { + cursor: grabbing; + } + + .auto-refresh-dropdown .auto-refresh-menu .sheet-handle:active::before { + background: rgba(255, 255, 255, 0.5); } - .auto-refresh-dropdown .dropdown-item:first-child { - margin-top: 24px; + .auto-refresh-dropdown .auto-refresh-menu .dropdown-item { + padding: 16px 20px; + font-size: 16px; } /* Multi-select dropdowns as bottom sheets */ @@ -1767,9 +1497,6 @@ z-index: 1; } - .dropdown-header { - padding: 32px 20px 16px; - } } @media screen and (max-width: 600px) { @@ -1803,22 +1530,11 @@ width: 100%; } - .date-inputs { - flex-direction: column; - gap: 15px; - } - .modal-content { min-width: 300px; margin: 20px; } - - .filters-grid { - grid-template-columns: 1fr; - gap: 12px; - } - .multi-select-dropdown { max-height: 240px; } @@ -1835,12 +1551,15 @@ flex-shrink: 0; } -/* Table View Button Styles */ -.table-view-btn { - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 8px; +/* Chart / Table segmented toggle — cascade-btn styling, joined edges */ +.view-toggle { + display: inline-flex; +} + +.view-toggle-btn { padding: 8px 12px; + border: 1px solid rgba(255, 255, 255, 0.18); + background: rgba(255, 255, 255, 0.15); color: rgba(255, 255, 255, 0.7); cursor: pointer; transition: all 0.2s ease; @@ -1848,23 +1567,35 @@ align-items: center; justify-content: center; font-size: 14px; + position: relative; } -.table-view-btn:hover { - background: rgba(255, 255, 255, 0.15); - color: rgba(255, 255, 255, 0.9); - border-color: rgba(255, 255, 255, 0.3); +.view-toggle-btn:first-child { + border-radius: 8px 0 0 8px; } -.table-view-btn.active { - background: rgba(139, 92, 246, 0.2); - border-color: rgba(139, 92, 246, 0.4); - color: rgba(139, 92, 246, 1); +.view-toggle-btn:last-child { + border-radius: 0 8px 8px 0; + margin-left: -1px; } -.table-view-btn.active:hover { - background: rgba(139, 92, 246, 0.3); - border-color: rgba(139, 92, 246, 0.5); +.view-toggle-btn:hover:not(.active) { + background: rgba(255, 255, 255, 0.25); + color: white; + z-index: 1; +} + +.view-toggle-btn.active { + background: rgba(124, 58, 237, 0.3); + border-color: var(--accent-primary); + color: white; + z-index: 2; +} + +.view-toggle-btn:focus-visible { + outline: 2px solid var(--accent-primary); + outline-offset: 2px; + z-index: 3; } /* new table full width styles (experimental) */ @@ -1916,17 +1647,6 @@ transition: opacity 0.3s ease, transform 0.3s ease; } -.table-view-exit { - opacity: 1; - transform: translateY(0); -} - -.table-view-exit-active { - opacity: 0; - transform: translateY(-10px); - transition: opacity 0.3s ease, transform 0.3s ease; -} - .stats-table .table-header { display: grid; grid-template-columns: 1fr 140px 140px; @@ -2096,4 +1816,8 @@ .stats-chart:hover { opacity: 0.95; +} + +.anychart-credits { + display: none !important; } \ No newline at end of file diff --git a/static/css/dashboard/url-modal.css b/static/css/dashboard/url-modal.css index 5465745c..3630fd49 100644 --- a/static/css/dashboard/url-modal.css +++ b/static/css/dashboard/url-modal.css @@ -8,7 +8,7 @@ left: 0; width: 100%; height: 100%; - z-index: 9999; + z-index: var(--z-max); opacity: 0; visibility: hidden; transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1), visibility 0.4s cubic-bezier(0.4, 0, 0.2, 1); @@ -74,7 +74,8 @@ background: rgba(255, 255, 255, 0.02); } -.modal-title-section-delete, .modal-title-section-success { +.modal-title-section-delete, +.modal-title-section-success { display: flex; align-items: center; gap: 16px; @@ -222,6 +223,18 @@ font-size: 16px; } +.tab.has-error::after { + content: ''; + position: absolute; + top: 6px; + right: 8px; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--color-danger); + box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.4); +} + /* Tab Content */ .tab-content { display: none; @@ -276,8 +289,8 @@ color: var(--text-primary); } -.field input, -.field select { +.modal .field input, +.modal .field select { background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; @@ -290,18 +303,18 @@ min-width: 0; } -.field input:focus, -.field select:focus { +.modal .field input:focus, +.modal .field select:focus { outline: none; border-color: var(--accent-primary); background: rgba(255, 255, 255, 0.08); } -.field input::placeholder { +.modal .field input::placeholder { color: var(--text-secondary); } -.field input:disabled { +.modal .field input:disabled { opacity: 0.6; cursor: not-allowed; background: rgba(255, 255, 255, 0.02); @@ -310,7 +323,7 @@ /* Input Group */ .input-group { display: flex; - align-items: center; + align-items: stretch; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; @@ -321,9 +334,62 @@ min-width: 0; } +.alias-status { + display: none; + align-items: center; + padding: 0 10px; + font-size: 16px; + color: var(--text-secondary); + flex-shrink: 0; +} + +.alias-status.show { + display: flex; +} + +.alias-status.is-checking i { + animation: spin 1s linear infinite; +} + +.alias-status.is-available { + color: var(--color-success, #22c55e); +} + +.alias-status.is-unavailable { + color: var(--color-danger); +} + +.alias-dice { + background: transparent; + border: none; + border-left: 1px solid rgba(255, 255, 255, 0.08); + padding: 0 14px; + color: var(--text-secondary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: color 0.15s ease, background 0.15s ease; + flex-shrink: 0; +} + +.alias-dice:hover { + color: var(--accent-primary); + background: rgba(255, 255, 255, 0.04); +} + +.alias-dice i { + font-size: 18px; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + .input-group:focus-within { - border-color: var(--accent-color); - box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); + border-color: var(--accent-primary); + background: rgba(255, 255, 255, 0.08); } .input-prefix { @@ -336,15 +402,20 @@ flex-shrink: 0; } -.input-group input { +.modal .input-group input { background: none; border: none; + border-radius: 0; flex: 1; min-width: 0; width: 100%; box-sizing: border-box; } +.modal .input-group input:focus { + background: transparent; +} + /* Select Wrapper */ .select-wrapper { position: relative; @@ -373,6 +444,35 @@ line-height: 1.4; } +/* Field Error */ +.field-error { + font-size: 12px; + color: var(--color-danger); + line-height: 1.4; + display: block; +} + +.field.has-error > .field-hint { + display: none; +} + +.field.has-error input, +.field.has-error select, +.field.has-error .input-group { + border-color: var(--color-danger); + background: rgba(239, 68, 68, 0.05); +} + +.field.has-error .input-group input { + background: transparent; +} + +.field.has-error input:focus, +.field.has-error select:focus, +.field.has-error .input-group:focus-within { + border-color: var(--color-danger); +} + /* Password Field Group */ .password-field-group { display: flex; @@ -387,28 +487,6 @@ gap: 12px; } -.btn-link { - background: none; - border: none; - color: #ef4444; - font-size: 12px; - cursor: pointer; - display: flex; - align-items: center; - gap: 4px; - padding: 4px 0; - transition: all 0.2s ease; -} - -.btn-link:hover { - color: #dc2626; - text-decoration: underline; -} - -.btn-link i { - font-size: 12px; -} - .password-status { font-size: 12px; color: var(--text-secondary); @@ -449,8 +527,8 @@ } .checkbox-field input[type="checkbox"]:checked+.checkbox-label .checkbox-indicator { - background: var(--accent-color); - border-color: var(--accent-color); + background: var(--accent-primary); + border-color: var(--accent-primary); } .checkbox-field input[type="checkbox"]:checked+.checkbox-label .checkbox-indicator::after { @@ -539,9 +617,9 @@ } .btn-primary { - background: #6366f1; + background: var(--color-info); color: white; - border-color: #6366f1; + border-color: var(--color-info); } .btn-primary:hover { @@ -555,9 +633,9 @@ } .btn-warning { - background: #f59e0b; + background: var(--color-warning); color: white; - border-color: #f59e0b; + border-color: var(--color-warning); } .btn-warning:hover { @@ -571,14 +649,14 @@ } .btn-danger { - background: #ef4444; + background: var(--color-danger); color: white; - border-color: #ef4444; + border-color: var(--color-danger); } .btn-danger:hover { - background: #dc2626; - border-color: #dc2626; + background: var(--color-danger-hover); + border-color: var(--color-danger-hover); } .btn-danger:active { @@ -623,7 +701,6 @@ /* Clickable Rows */ .clickable-row { transition: all 0.2s ease; - border-radius: 8px; } .clickable-row:hover { @@ -657,7 +734,7 @@ } .url-preview strong { - color: #ef4444; + color: var(--color-danger); font-weight: 600; } @@ -748,6 +825,12 @@ } .tab { - justify-content: flex-start; + padding: 10px 8px; + font-size: 13px; + gap: 6px; + } + + .tab i { + font-size: 14px; } } \ No newline at end of file diff --git a/static/css/docs.css b/static/css/docs.css index e2502469..302c306b 100644 --- a/static/css/docs.css +++ b/static/css/docs.css @@ -74,7 +74,8 @@ code { border-radius: 5px; } -ul, ol{ +ul, +ol { color: rgba(243, 254, 228, 0.8); font-size: 1.1rem; } @@ -166,12 +167,14 @@ a:hover { padding: 10px 20px; } -.next-link, .next-link:hover { +.next-link, +.next-link:hover { all: unset; cursor: pointer; } -.next-link:hover, .next-link:hover svg { +.next-link:hover, +.next-link:hover svg { color: #2DF98D; } @@ -248,4 +251,4 @@ footer p { margin-top: 20px; } -} +} \ No newline at end of file diff --git a/static/css/error.css b/static/css/error.css index b7eb71f5..88ec1ec0 100644 --- a/static/css/error.css +++ b/static/css/error.css @@ -38,4 +38,4 @@ body { color: #fff; letter-spacing: 5px; margin: 20px 0; -} +} \ No newline at end of file diff --git a/static/css/header.css b/static/css/header.css index 9d54dd0f..6aed6b90 100644 --- a/static/css/header.css +++ b/static/css/header.css @@ -7,7 +7,7 @@ border-radius: 0; border-bottom: 1px solid rgba(255, 255, 255, 0.08); position: sticky; - z-index: 9999; + z-index: var(--z-max, 9999); top: 0; backdrop-filter: blur(20px); display: flex; @@ -310,7 +310,7 @@ padding: 8px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35); backdrop-filter: blur(12px); - z-index: 1000; + z-index: var(--z-overlay, 1000); } .profile-dropdown a, @@ -334,4 +334,69 @@ background: rgba(255, 255, 255, 0.08); text-shadow: none !important; width: -webkit-fill-available; +} + +.self-promo { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + padding: 7px 10px; + background-color: rgba(0, 0, 0, 0.5); +} + +.self-promo-inner { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + width: -webkit-fill-available; +} + +.self-promo-inner a { + all: unset; + color: white; + font-weight: normal; + font-size: 17px; + letter-spacing: 0.8px; + cursor: pointer; + text-align: center; + font-family: "Allerta Stencil", sans-serif; +} + +.self-promo-inner a:hover { + text-decoration: underline; +} + +.self-promo button { + background-color: rgba(255, 255, 255, 0.125); + backdrop-filter: blur(10px); + border: none; + color: #fff; + font-size: 1.2rem; + cursor: pointer; + outline: none; + border-radius: 50%; + height: 27px; + width: 27px; + padding: 3px; + margin: 0; +} + +.self-promo button:hover { + background-color: rgba(255, 255, 255, 0.25); +} + +.self-promo button img { + vertical-align: initial; +} + +.self-promo.hidden { + display: none; +} + +@media screen and (max-width: 500px) { + .self-promo-inner a { + font-size: 12px; + } } \ No newline at end of file diff --git a/static/css/landing-edit-modal.css b/static/css/landing-edit-modal.css index 7f965e6c..cf9c6996 100644 --- a/static/css/landing-edit-modal.css +++ b/static/css/landing-edit-modal.css @@ -32,7 +32,7 @@ left: 0 !important; width: 100% !important; height: 100% !important; - z-index: 9999 !important; + z-index: var(--z-max) !important; opacity: 0 !important; visibility: hidden !important; transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1), visibility 0.4s cubic-bezier(0.4, 0, 0.2, 1) !important; diff --git a/static/css/mobile-header.css b/static/css/mobile-header.css index fdfd3c1a..c56afed9 100644 --- a/static/css/mobile-header.css +++ b/static/css/mobile-header.css @@ -4,7 +4,7 @@ background: rgba(255, 255, 255, 0.04); border-radius: 0; position: sticky; - z-index: 9999; + z-index: var(--z-max, 9999); top: 0; left: 0; right: 0; @@ -143,13 +143,13 @@ ul.mobile-menu { border-top: 1px solid rgba(255, 255, 255, 0.08); } -ul.mobile-menu > li { +ul.mobile-menu>li { display: block; padding: 0; transition: background 0.2s ease; } -ul.mobile-menu > li > a { +ul.mobile-menu>li>a { display: block; text-decoration: none; color: rgba(255, 255, 255, 0.75); @@ -159,17 +159,17 @@ ul.mobile-menu > li > a { transition: all 0.2s ease; } -ul.mobile-menu > li > a.active { +ul.mobile-menu>li>a.active { color: #fff; background: rgba(255, 255, 255, 0.05); } -ul.mobile-menu > li > a:hover { +ul.mobile-menu>li>a:hover { text-decoration: none; color: #fff; } -ul.mobile-menu > li:hover { +ul.mobile-menu>li:hover { background: rgba(255, 255, 255, 0.08); cursor: pointer; } @@ -192,36 +192,35 @@ ul.mobile-menu > li:hover { width: 14px !important; } -.menu-signin { - color: #fff !important; - font-weight: 500; +/* Mobile profile menu */ +.mobile-profile { + display: flex; + align-items: center; } -/* Mobile profile menu */ -.mobile-profile { - display: flex; - align-items: center; -} -.mobile-profile .profile-btn { - border: 0; - background: transparent; - padding: 0; - line-height: 0; +.mobile-profile .profile-btn { + border: 0; + background: transparent; + padding: 0; + line-height: 0; border-radius: 50%; cursor: pointer; } -.mobile-profile .profile-avatar-container { - width: 36px; - height: 36px; - position: relative; + +.mobile-profile .profile-avatar-container { + width: 36px; + height: 36px; + position: relative; } -.mobile-profile .profile-btn img { - width: 36px; - height: 36px; - border-radius: 50%; - border: 1px solid rgba(255,255,255,0.2); - display: block; + +.mobile-profile .profile-btn img { + width: 36px; + height: 36px; + border-radius: 50%; + border: 1px solid rgba(255, 255, 255, 0.2); + display: block; } + .mobile-profile .profile-initials-circle { width: 36px; height: 36px; @@ -233,38 +232,41 @@ ul.mobile-menu > li:hover { font-weight: 600; font-size: 14px; color: white; - border: 1px solid rgba(255,255,255,0.2); -} -.mobile-profile .profile-dropdown { - position: absolute; - right: 0; - top: calc(100% + 8px); - min-width: 180px; - background: rgba(13,17,35,0.96); - border: 1px solid rgba(255,255,255,0.12); - border-radius: 10px; - padding: 8px; - box-shadow: 0 8px 24px rgba(0,0,0,0.35); - backdrop-filter: blur(12px); - z-index: 1000; + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.mobile-profile .profile-dropdown { + position: absolute; + right: 0; + top: calc(100% + 8px); + min-width: 180px; + background: rgba(13, 17, 35, 0.96); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 10px; + padding: 8px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35); + backdrop-filter: blur(12px); + z-index: var(--z-overlay, 1000); } + .mobile-profile .profile-dropdown a, -.mobile-profile .profile-dropdown button { - display: block; - width: 100%; - text-align: left; - color: #fff; - text-decoration: none; - padding: 8px 10px; - background: transparent; - border: 0; - border-radius: 8px; - font-size: 14px; - cursor: pointer; +.mobile-profile .profile-dropdown button { + display: block; + width: 100%; + text-align: left; + color: #fff; + text-decoration: none; + padding: 8px 10px; + background: transparent; + border: 0; + border-radius: 8px; + font-size: 14px; + cursor: pointer; } + .mobile-profile .profile-dropdown a:hover, -.mobile-profile .profile-dropdown button:hover { - background: rgba(255,255,255,0.08); +.mobile-profile .profile-dropdown button:hover { + background: rgba(255, 255, 255, 0.08); } diff --git a/static/css/notifications.css b/static/css/notifications.css new file mode 100644 index 00000000..f15800f4 --- /dev/null +++ b/static/css/notifications.css @@ -0,0 +1,102 @@ +.notification { + position: fixed; + bottom: 24px; + right: 24px; + padding: 14px 16px; + border-radius: var(--radius-md, 12px); + background: rgba(15, 20, 40, 0.82); + backdrop-filter: blur(16px) saturate(1.4); + -webkit-backdrop-filter: blur(16px) saturate(1.4); + color: var(--text-primary, #fff); + font-weight: 400; + font-size: 13.5px; + line-height: 1.4; + z-index: calc(var(--z-max, 9999) + 2); + max-width: 360px; + min-width: 260px; + border: 1px solid var(--border-color, rgba(255, 255, 255, 0.10)); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35), 0 2px 6px rgba(0, 0, 0, 0.2); + display: flex; + align-items: center; + gap: 10px; + overflow: hidden; + transform: translateY(calc(100% + 32px)); + transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.2s ease; + opacity: 0; +} + +.notification.show { + transform: translateY(0); + opacity: 1; +} + +.notification-success i { + color: var(--color-success, #22c55e); +} + +.notification-error i { + color: var(--color-danger, #ef4444); +} + +.notification-warning i { + color: var(--color-warning, #f59e0b); +} + +.notification-info i { + color: var(--color-info, #6366f1); +} + +.notification i { + font-size: 16px; + flex-shrink: 0; +} + +.notification span { + flex: 1; + color: var(--text-primary, #fff); +} + +.notification .notification-progress { + position: absolute; + bottom: 0; + left: 0; + height: 2px; + border-radius: 0 0 var(--radius-md, 12px) var(--radius-md, 12px); + animation: notification-progress-bar linear forwards; +} + +.notification-success .notification-progress { + background: var(--color-success, #22c55e); +} + +.notification-error .notification-progress { + background: var(--color-danger, #ef4444); +} + +.notification-warning .notification-progress { + background: var(--color-warning, #f59e0b); +} + +.notification-info .notification-progress { + background: var(--color-info, #6366f1); +} + +@keyframes notification-progress-bar { + from { + width: 100%; + } + + to { + width: 0; + } +} + +@media screen and (max-width: 480px) { + .notification { + bottom: 16px; + right: 12px; + left: 12px; + max-width: none; + min-width: 0; + } +} \ No newline at end of file diff --git a/static/css/password.css b/static/css/password.css index ecf5e62f..8ad4481a 100644 --- a/static/css/password.css +++ b/static/css/password.css @@ -1,19 +1,13 @@ body { font-family: monospace, sans-serif; - background-color: #222; color: #fff; margin-top: 230px; - height: 100vh; background-attachment: fixed; overflow: hidden; -webkit-tap-highlight-color: transparent; -ms-user-select: none; -moz-user-select: none; user-select: none; -} - - -body { width: 100%; height: 100%; background-size: cover; diff --git a/static/css/preview.css b/static/css/preview.css index 322cb5ea..4734d30d 100644 --- a/static/css/preview.css +++ b/static/css/preview.css @@ -64,32 +64,6 @@ body { } } -.preview-icon { - width: 64px; - height: 64px; - margin: 0 auto 24px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - font-size: 32px; -} - -.preview-icon-normal { - background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); - color: white; -} - -.lock-icon { - background: rgba(245, 158, 11, 0.15); - color: #f59e0b; -} - -.error-icon { - background: rgba(239, 68, 68, 0.15); - color: #ef4444; -} - h1 { font-size: 28px; font-weight: 700; @@ -333,12 +307,6 @@ h1 { flex-direction: column; } - .preview-icon { - width: 56px; - height: 56px; - font-size: 28px; - } - .full-url { font-size: 16px; } diff --git a/static/css/prism-duotone-dark.css b/static/css/prism-duotone-dark.css index ecead646..e6c499ac 100644 --- a/static/css/prism-duotone-dark.css +++ b/static/css/prism-duotone-dark.css @@ -33,19 +33,23 @@ pre[class*="language-"] { border-radius: 5px; } -pre > code[class*="language-"] { +pre>code[class*="language-"] { font-size: 1em; padding: 0; } -pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, -code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { +pre[class*="language-"]::-moz-selection, +pre[class*="language-"] ::-moz-selection, +code[class*="language-"]::-moz-selection, +code[class*="language-"] ::-moz-selection { text-shadow: none; background: #6a51e6; } -pre[class*="language-"]::selection, pre[class*="language-"] ::selection, -code[class*="language-"]::selection, code[class*="language-"] ::selection { +pre[class*="language-"]::selection, +pre[class*="language-"] ::selection, +code[class*="language-"]::selection, +code[class*="language-"] ::selection { text-shadow: none; background: #6a51e6; } @@ -58,7 +62,7 @@ pre[class*="language-"] { } /* Inline code */ -:not(pre) > code[class*="language-"] { +:not(pre)>code[class*="language-"] { padding: .1em; border-radius: .3em; } @@ -151,7 +155,7 @@ code.language-scss, cursor: help; } -pre > code.highlight { +pre>code.highlight { outline: .4em solid #8a75f5; outline-offset: .4em; } @@ -163,7 +167,7 @@ pre > code.highlight { border-right-color: #2c2937; } -.line-numbers .line-numbers-rows > span:before { +.line-numbers .line-numbers-rows>span:before { color: #3c3949; } @@ -174,4 +178,4 @@ pre > code.highlight { background: rgba(224, 145, 66, 0.2); background: -webkit-linear-gradient(left, rgba(224, 145, 66, 0.2) 70%, rgba(224, 145, 66, 0)); background: linear-gradient(to right, rgba(224, 145, 66, 0.2) 70%, rgba(224, 145, 66, 0)); -} +} \ No newline at end of file diff --git a/static/css/report.css b/static/css/report.css index 3e5ab85c..50e616d2 100644 --- a/static/css/report.css +++ b/static/css/report.css @@ -3,12 +3,7 @@ body { margin: 0; padding: 0; box-sizing: border-box; - background-image: linear-gradient(3deg, #fc6538, #678382); - background-color: rgb(136, 43, 8); color: white; -} - -body { width: 100%; height: 100%; background-size: cover; @@ -124,7 +119,7 @@ textarea:focus::placeholder { border-bottom-left-radius: 18px; } -button.h-captcha{ +button.h-captcha { border: none; width: 180px; padding: 10px 13px; diff --git a/static/css/result.css b/static/css/result.css index 4970fc8b..44095ac9 100644 --- a/static/css/result.css +++ b/static/css/result.css @@ -4,10 +4,6 @@ -webkit-tap-highlight-color: transparent; } -body { - background-image: linear-gradient(to top, #63037a, #062021); -} - body { width: 100%; height: 100%; @@ -120,7 +116,7 @@ body { display: flex; flex-direction: column; align-items: center; - gap:20px; + gap: 20px; } #main-container { @@ -144,7 +140,7 @@ body { align-items: center; justify-content: center; font-size: 25px; - background: rgba(0,0,0, 0.5); + background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(10px); border-radius: 15px; color: white; @@ -315,10 +311,6 @@ body { margin-top: 30px; } -.for-middle { - text-align: center; -} - @media screen and (max-width: 800px) { .copy-button { padding: 8px 20px 7px 20px; @@ -350,4 +342,19 @@ body { padding: 12px 25px; font-size: 13px; } +}body { + min-height: 100vh; + /* overflow: hidden; */ } + +.confetti { + width: 8px; + height: 8px; + position: absolute; + z-index: 11; + background: var(--color); + top: 0; + left: 0; + will-change: transform; + pointer-events: none; +} \ No newline at end of file diff --git a/static/css/self-promo.css b/static/css/self-promo.css deleted file mode 100644 index 4a71978b..00000000 --- a/static/css/self-promo.css +++ /dev/null @@ -1,65 +0,0 @@ -.self-promo { - width: 100%; - display: flex; - justify-content: space-between; - align-items: center; - padding: 7px 10px; - background-color: rgba(0, 0, 0, 0.5); -} - -.self-promo-inner { - display: flex; - justify-content: center; - align-items: center; - width: 100%; - width: -webkit-fill-available; -} - -.self-promo-inner a { - text-align: center; - all: unset; - color: white; - font-weight: normal; - font-size: 17px; - letter-spacing: 0.8px; - cursor: pointer; - text-align: center; - font-family: "Allerta Stencil", sans-serif; -} - -.self-promo-inner a:hover { - text-decoration: underline; -} - -.self-promo button { - background-color: rgba(255, 255, 255, 0.125); - backdrop-filter: blur(10px); - border: none; - color: #fff; - font-size: 1.2rem; - cursor: pointer; - outline: none; - border-radius: 50%; - height: 27px; - width: 27px; - padding: 3px; - margin: 0; -} - -.self-promo button:hover { - background-color: rgba(255, 255, 255, 0.25); -} - -.self-promo button img { - vertical-align: initial; -} - -.self-promo.hidden { - display: none; -} - -@media screen and (max-width: 500px) { - .self-promo-inner a { - font-size: 12px; - } -} \ No newline at end of file diff --git a/static/css/stats-view.css b/static/css/stats-view.css index da723202..8ecd5c67 100644 --- a/static/css/stats-view.css +++ b/static/css/stats-view.css @@ -180,7 +180,8 @@ select:hover { .export-data-wrapper .button-pair { display: flex; gap: 20px; - flex-basis: calc(50% - 10px); /* Subtract half of the gap size */ + flex-basis: calc(50% - 10px); + /* Subtract half of the gap size */ justify-content: space-between; } @@ -490,4 +491,4 @@ h3 { .main-data-container { margin-top: 40px; } -} +} \ No newline at end of file diff --git a/static/css/v2-announcement.css b/static/css/v2-announcement.css index dac5577a..74c3413e 100644 --- a/static/css/v2-announcement.css +++ b/static/css/v2-announcement.css @@ -21,9 +21,9 @@ position: fixed; bottom: 24px; right: 24px; - z-index: 9999; + z-index: var(--z-max); /* subtle glassy pill that matches modal tones */ - background: linear-gradient(180deg, rgba(124,58,237,0.14), rgba(37,99,235,0.10)); + background: linear-gradient(180deg, rgba(124, 58, 237, 0.14), rgba(37, 99, 235, 0.10)); color: var(--v2-text); padding: 16px 30px; border-radius: 999px; @@ -54,19 +54,21 @@ /* animated border band using mask to show only the outer rim */ content: ''; position: absolute; - inset: -3px; /* slightly outside the pill */ + inset: -3px; + /* slightly outside the pill */ border-radius: inherit; - padding: 3px; /* thickness of the visible border */ - background: linear-gradient(90deg, rgba(255,255,255,0.06), rgba(124,58,237,0.9), rgba(99,102,241,0.9), rgba(255,255,255,0.06)); + padding: 3px; + /* thickness of the visible border */ + background: linear-gradient(90deg, rgba(255, 255, 255, 0.06), rgba(124, 58, 237, 0.9), rgba(99, 102, 241, 0.9), rgba(255, 255, 255, 0.06)); background-size: 200% 100%; background-position: 0% 50%; pointer-events: none; z-index: 1; /* show only the band by using mask with content-box */ - -webkit-mask: linear-gradient(#fff,#fff) content-box, linear-gradient(#fff,#fff); + -webkit-mask: linear-gradient(#fff, #fff) content-box, linear-gradient(#fff, #fff); -webkit-mask-composite: xor; mask-composite: exclude; - mask: linear-gradient(#fff,#fff) content-box, linear-gradient(#fff,#fff); + mask: linear-gradient(#fff, #fff) content-box, linear-gradient(#fff, #fff); animation: borderShift 2.8s linear infinite; filter: blur(6px); opacity: 0.95; @@ -80,30 +82,56 @@ top: -30%; width: 60%; height: 160%; - background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.18) 50%, transparent 100%); + background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.18) 50%, transparent 100%); transform: skewX(-22deg) translateX(-150%); pointer-events: none; opacity: 0; z-index: 4; - animation: sheen 3.8s cubic-bezier(.2,.9,.2,1) infinite; + animation: sheen 3.8s cubic-bezier(.2, .9, .2, 1) infinite; animation-delay: 0.6s; } @keyframes borderShift { - 0% { background-position: 0% 50%; } - 50% { background-position: 100% 50%; } - 100% { background-position: 200% 50%; } + 0% { + background-position: 0% 50%; + } + + 50% { + background-position: 100% 50%; + } + + 100% { + background-position: 200% 50%; + } } @keyframes sheen { - 0% { transform: skewX(-22deg) translateX(-150%); opacity: 0; } - 10% { opacity: 0.9; } - 45% { transform: skewX(-22deg) translateX(30%); opacity: 0.9; } - 70% { opacity: 0.35; } - 100% { transform: skewX(-22deg) translateX(200%); opacity: 0; } + 0% { + transform: skewX(-22deg) translateX(-150%); + opacity: 0; + } + + 10% { + opacity: 0.9; + } + + 45% { + transform: skewX(-22deg) translateX(30%); + opacity: 0.9; + } + + 70% { + opacity: 0.35; + } + + 100% { + transform: skewX(-22deg) translateX(200%); + opacity: 0; + } } @media (prefers-reduced-motion: reduce) { + .v2-badge::before, .v2-badge::after { animation: none !important; @@ -113,14 +141,32 @@ } @keyframes shine { - 0% { transform: skewX(-22deg) translateX(-150%); opacity: 0; } - 8% { opacity: 0.9; } - 40% { transform: skewX(-22deg) translateX(40%); opacity: 0.9; } - 60% { opacity: 0.4; } - 100% { transform: skewX(-22deg) translateX(200%); opacity: 0; } + 0% { + transform: skewX(-22deg) translateX(-150%); + opacity: 0; + } + + 8% { + opacity: 0.9; + } + + 40% { + transform: skewX(-22deg) translateX(40%); + opacity: 0.9; + } + + 60% { + opacity: 0.4; + } + + 100% { + transform: skewX(-22deg) translateX(200%); + opacity: 0; + } } @media (prefers-reduced-motion: reduce) { + .v2-badge::before, .v2-badge::after { animation: none !important; @@ -146,7 +192,7 @@ display: none; position: fixed; inset: 0; - z-index: 10000; + z-index: var(--z-modal); background: var(--v2-overlay); backdrop-filter: blur(16px) saturate(140%); padding: clamp(16px, 4vw, 40px); @@ -164,6 +210,7 @@ from { opacity: 0; } + to { opacity: 1; } @@ -204,7 +251,7 @@ } } -.v2-modal > * { +.v2-modal>* { position: relative; z-index: 1; } @@ -218,6 +265,7 @@ opacity: 0; transform: translateY(40px) scale(0.95); } + to { opacity: 1; transform: translateY(0) scale(1); @@ -284,6 +332,7 @@ opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); @@ -291,8 +340,7 @@ } /* Emoji & icons */ -.v2-emoji-container, -.v2-feature-icon-large { +.v2-emoji-container { width: 108px; height: 108px; border-radius: 28px; @@ -359,12 +407,12 @@ background-position: center; border-radius: 12px; box-shadow: 0 18px 40px rgba(6, 4, 20, 0.6); - border: 1px solid rgba(255,255,255,0.06); + border: 1px solid rgba(255, 255, 255, 0.06); opacity: 0; transform: translateY(8px) scale(0.98); transition: opacity 180ms ease, transform 180ms ease; pointer-events: none; - z-index: 10002; + z-index: var(--z-modal); } .v2-preview-tooltip.visible { @@ -374,7 +422,9 @@ /* Hide previews on small screens */ @media (max-width: 720px) { - .v2-preview-tooltip { display: none; } + .v2-preview-tooltip { + display: none; + } } /* Global floating tooltip (positioned via JS next to each bullet) */ @@ -386,39 +436,38 @@ background-size: cover; background-position: center; border-radius: 12px; - box-shadow: 0 28px 70px rgba(6,4,20,0.75); - border: 1px solid rgba(255,255,255,0.06); + box-shadow: 0 28px 70px rgba(6, 4, 20, 0.75); + border: 1px solid rgba(255, 255, 255, 0.06); opacity: 0; transform: translateY(8px) scale(0.98); - transition: opacity 220ms cubic-bezier(.2,.9,.2,1), transform 220ms cubic-bezier(.2,.9,.2,1); + transition: opacity 220ms cubic-bezier(.2, .9, .2, 1), transform 220ms cubic-bezier(.2, .9, .2, 1); pointer-events: none; - z-index: 12000; + z-index: var(--z-modal); } + .v2-preview-tooltip-global.visible { opacity: 1; transform: translateY(0) scale(1); } @media (max-width: 900px) { - .v2-preview-tooltip-global { display: none; } -} - -.v2-feature-icon-large { - margin-left: auto; - margin-right: auto; + .v2-preview-tooltip-global { + display: none; + } } @keyframes emojiFloat { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-8px); } } -.v2-feature-icon-large i, .v2-feature-highlight-item i { color: #b197fc; font-size: 26px; @@ -573,9 +622,9 @@ .v2-progress-dot.active { width: 36px; - background: rgba(255,255,255,0.04); - border: 1px solid rgba(255,255,255,0.06); - box-shadow: 0 6px 14px rgba(6,4,20,0.45), 0 2px 6px rgba(124,58,237,0.06) inset; + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.06); + box-shadow: 0 6px 14px rgba(6, 4, 20, 0.45), 0 2px 6px rgba(124, 58, 237, 0.06) inset; } .v2-progress-dot:hover { @@ -591,8 +640,9 @@ /* Tablet: hide absolute-positioned arrows, show inline mobile nav */ @media (max-width: 900px) { - .v2-modal > .v2-prev-btn, - .v2-modal > .v2-next-btn { + + .v2-modal>.v2-prev-btn, + .v2-modal>.v2-next-btn { /* hide the absolutely-positioned side arrows */ display: none; } @@ -754,4 +804,4 @@ .v2-mobile-nav .v2-progress-dot.active { width: 28px; } -} +} \ No newline at end of file diff --git a/static/js/alias-checker.js b/static/js/alias-checker.js new file mode 100644 index 00000000..aec316db --- /dev/null +++ b/static/js/alias-checker.js @@ -0,0 +1,172 @@ +/** + * Alias availability checker + random generator. + * + * window.AliasChecker.attach({ + * inputId, diceBtn, indicator, getCurrentAlias?, onValidityChange?, + * }) + * window.AliasChecker.randomAlias() + * + * Debounces server hits; resolves client-side length/format first. + * Uses the shared setFieldError/clearFieldError primitive for error text. + */ +(function () { + const ALPHABET = + 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + const LENGTH = 7; + const DEBOUNCE_MS = 300; + + function randomAlias() { + const rng = window.crypto || window.msCrypto; + if (rng && rng.getRandomValues) { + const buf = new Uint32Array(LENGTH); + rng.getRandomValues(buf); + let out = ''; + for (let i = 0; i < LENGTH; i++) out += ALPHABET[buf[i] % ALPHABET.length]; + return out; + } + let out = ''; + for (let i = 0; i < LENGTH; i++) { + out += ALPHABET[Math.floor(Math.random() * ALPHABET.length)]; + } + return out; + } + + function clientValidate(alias) { + if (alias.length < 3) return 'Must be at least 3 characters'; + if (alias.length > 16) return 'Must be at most 16 characters'; + if (!/^[a-zA-Z0-9_-]+$/.test(alias)) { + return 'Only letters, numbers, underscores, and hyphens are allowed'; + } + return null; + } + + function reasonToMessage(reason) { + switch (reason) { + case 'length': + return 'Must be 3-16 characters'; + case 'format': + return 'Only letters, numbers, underscores, and hyphens are allowed'; + case 'taken': + return 'This alias is already taken'; + default: + return 'Alias is not available'; + } + } + + function attach(options) { + const input = document.getElementById(options.inputId); + if (!input) return; + const { diceBtn, indicator, getCurrentAlias, onValidityChange } = options; + + let debounceTimer = null; + let inFlight = null; + + function setIndicator(state) { + indicator.classList.remove('show', 'is-checking', 'is-available', 'is-unavailable'); + if (state === 'idle') { + indicator.innerHTML = ''; + return; + } + indicator.classList.add('show'); + if (state === 'loading') { + indicator.classList.add('is-checking'); + indicator.innerHTML = ''; + } else if (state === 'available') { + indicator.classList.add('is-available'); + indicator.innerHTML = ''; + } else { + indicator.classList.add('is-unavailable'); + indicator.innerHTML = ''; + } + } + + function emit(valid) { + if (typeof onValidityChange === 'function') onValidityChange(valid); + } + + function applyResult(alias, valid, message) { + if (input.value.trim() !== alias) return; + if (valid) { + setIndicator('available'); + window.clearFieldError(options.inputId); + emit(true); + } else { + setIndicator('unavailable'); + window.setFieldError(options.inputId, message); + emit(false); + } + } + + function runServerCheck(alias) { + if (inFlight) inFlight.abort(); + const controller = new AbortController(); + inFlight = controller; + fetch( + `/api/v1/shorten/check-alias?alias=${encodeURIComponent(alias)}`, + { signal: controller.signal, credentials: 'same-origin' }, + ) + .then((r) => r.json()) + .then((data) => { + if (data.available) { + applyResult(alias, true); + } else { + applyResult(alias, false, reasonToMessage(data.reason)); + } + }) + .catch((err) => { + if (err.name === 'AbortError') return; + setIndicator('idle'); + }); + } + + input.addEventListener('input', () => { + if (debounceTimer) clearTimeout(debounceTimer); + if (inFlight) inFlight.abort(); + + const alias = input.value.trim(); + if (!alias) { + setIndicator('idle'); + window.clearFieldError(options.inputId); + emit(null); + return; + } + + if (typeof getCurrentAlias === 'function' && alias === getCurrentAlias()) { + setIndicator('idle'); + window.clearFieldError(options.inputId); + emit(true); + return; + } + + const clientError = clientValidate(alias); + if (clientError) { + setIndicator('unavailable'); + window.setFieldError(options.inputId, clientError); + emit(false); + return; + } + + setIndicator('loading'); + debounceTimer = setTimeout(() => runServerCheck(alias), DEBOUNCE_MS); + }); + + if (diceBtn) { + diceBtn.addEventListener('click', (e) => { + e.preventDefault(); + input.value = randomAlias(); + input.dispatchEvent(new Event('input', { bubbles: true })); + input.focus(); + }); + } + + function reset() { + if (debounceTimer) clearTimeout(debounceTimer); + if (inFlight) inFlight.abort(); + setIndicator('idle'); + } + + return { reset }; + } + + window.AliasChecker = { attach, randomAlias }; +})(); diff --git a/static/js/confetti.js b/static/js/confetti.js index 123786d3..001b30bf 100644 --- a/static/js/confetti.js +++ b/static/js/confetti.js @@ -1,4 +1,4 @@ -var amount = 2000, +var amount = 200, between = (min, max) => min + Math.random() * (max - min), colors = [ "#d400ff", @@ -17,14 +17,14 @@ let interval = setInterval(() => { if (current < amount) { animate(createConfetti()); } -}, 50); +}, 30); -setTimeout(() => clearInterval(interval), 5000); +setTimeout(() => clearInterval(interval), 1200); function createConfetti() { let div = document.createElement('div'); // Randomize the size and shape of the confetti - let size = between(10, 25); // Increase the minimum and maximum values + let size = between(6, 12); let shape = Math.random() > 0.5 ? 'circle' : 'square'; gsap.set(div, { attr: { @@ -44,21 +44,12 @@ function createConfetti() { } function animate(element) { - // Adjust the gravity, drag, and terminal velocity values - let gravity = 0.3; - let drag = 0.05; - let terminalVelocity = 10; - // Randomize the initial velocity and rotation of the confetti - let velocityX = between(-25, 25); - let velocityY = between(0, -50); - let rotationX = between(0, 360); - let rotationY = between(0, 360); let rotationZ = between(0, 360); gsap.to(element, { y: window.innerHeight + 40, ease: 'power1.out', - delay: between(0, .25), - duration: between(2, 5), + delay: between(0, .15), + duration: between(1.5, 3), onComplete() { if (element instanceof Element || element instanceof HTMLDocument) { current--; @@ -68,28 +59,7 @@ function animate(element) { }); gsap.to(element, { rotationZ: rotationZ + between(90, 180), - repeat: -1, - yoyo: true, - duration: between(3, 6) - }); - gsap.to(element, { - rotationX: rotationX + between(0, 360), - rotationY: rotationY + between(0, 360), - repeat: -1, - yoyo: true, - duration: between(3, 6) - }); - // Apply forces to velocity and position - gsap.ticker.add(() => { - velocityX -= velocityX * drag; - velocityY = Math.min(velocityY + gravity, terminalVelocity); - velocityX += Math.random() > 0.5 ? Math.random() : -Math.random(); - velocityY += Math.random() > 0.5 ? Math.random() : -Math.random(); - }); - // Increase the scale factor of each particle - gsap.to(element, { - scale: 1.5, // Change this value to make the confetti bigger or smaller - ease: "elastic.out(1, 0.3)", - duration: 1 + duration: between(2, 4), + ease: 'none' }); } diff --git a/static/js/customNotification.js b/static/js/customNotification.js deleted file mode 100644 index b971cb44..00000000 --- a/static/js/customNotification.js +++ /dev/null @@ -1,39 +0,0 @@ -function createNotification(id, type) { - var notification = document.createElement("div"); - notification.classList.add("custom-top-notification"); - notification.id = id; - notification.innerHTML = '×

This is a custom top notification

'; - - if(type==="warning") { - notification.classList.add("warning"); - } - else if(type==="success") { - notification.classList.add("success"); - } - else if(type==="error") { - notification.classList.add("error"); - } - - document.body.appendChild(notification); -} - -function customTopNotification(Id, text, time, type="warning") { - var element = document.getElementById("customNotification"+Id); - if (element) { - element.remove(); - } - - createNotification("customNotification"+Id, type); - - var newNotification = document.getElementById("customNotification"+Id); - newNotification.getElementsByTagName("p")[0].innerHTML = text; - newNotification.style.display = "block"; - - var progressbar = newNotification.getElementsByClassName("close-progress-bar")[0]; - var animation = "progress-bar-animation " + time + "s linear"; - progressbar.style.animation = animation; - - setTimeout(function () { - newNotification.remove(); - }, time * 1000 - 200); -} \ No newline at end of file diff --git a/static/js/dashboard.js b/static/js/dashboard.js deleted file mode 100644 index 896c0d38..00000000 --- a/static/js/dashboard.js +++ /dev/null @@ -1,327 +0,0 @@ -(function () { - // Get host URL from window config or fallback to root element - const rawHost = window.dashboardConfig?.hostUrl || document.querySelector('[data-host]')?.getAttribute('data-host') || ''; - const host = rawHost.replace(/\/+$/, ''); - const displayHost = host ? (host + '/') : '/'; - - const els = { - search: document.getElementById('f-search'), - status: document.getElementById('f-status'), - password: document.getElementById('f-password'), - maxClicks: document.getElementById('f-maxclicks'), - createdAfter: document.getElementById('f-created-after'), - createdBefore: document.getElementById('f-created-before'), - sortBy: document.getElementById('f-sortby'), - order: document.getElementById('f-order'), - pageSize: document.getElementById('f-pagesize'), - apply: document.getElementById('btn-apply'), - reset: document.getElementById('btn-reset'), - optionsBtn: document.getElementById('btn-options'), - optionsDropdown: document.getElementById('options-dropdown'), - loading: document.getElementById('list-loading'), - empty: document.getElementById('list-empty'), - list: document.getElementById('links-list'), - pagination: document.getElementById('pagination'), - tpl: document.getElementById('tpl-link-item'), - }; - - let state = { - page: 1, - hasNext: false, - total: 0, - pageSize: 20, - sortBy: 'last_click', - sortOrder: 'descending', - filters: {}, - }; - - function toEpochSeconds(value) { - if (!value) return undefined; - try { return Math.floor(new Date(value).getTime() / 1000); } catch { return undefined; } - } - - function buildQuery() { - const filter = {}; - if (els.search.value.trim()) filter.search = els.search.value.trim(); - if (els.status.value) filter.status = els.status.value; - if (els.password.value) filter.passwordSet = els.password.value; - if (els.maxClicks.value) filter.maxClicksSet = els.maxClicks.value; - if (els.createdAfter.value) filter.createdAfter = toEpochSeconds(els.createdAfter.value); - if (els.createdBefore.value) filter.createdBefore = toEpochSeconds(els.createdBefore.value); - - const params = new URLSearchParams(); - params.set('page', String(state.page)); - params.set('pageSize', String(els.pageSize.value || state.pageSize)); - params.set('sortBy', els.sortBy.value || state.sortBy); - params.set('sortOrder', els.order.value || state.sortOrder); - if (Object.keys(filter).length) { params.set('filter', JSON.stringify(filter)); } - return params.toString(); - } - - function setLoading(isLoading) { - els.loading.style.display = isLoading ? 'block' : 'none'; - } - - function clearList() { - els.list.innerHTML = ''; - } - - function formatDate(iso) { - if (!iso) return '—'; - return window.SmartDatetime ? window.SmartDatetime.formatCreated(iso) : - (() => { try { return new Date(iso).toLocaleString(); } catch { return '—'; } })(); - } - - function formatTs(ts) { - if (!ts && ts !== 0) return '—'; - return window.SmartDatetime ? window.SmartDatetime.formatLastClick(ts) : - (() => { try { return new Date(ts * 1000).toLocaleString(); } catch { return '—'; } })(); - } - - function trimProtocol(url) { - if (!url) return ''; - return String(url).replace(/^https?:\/\//i, ''); - } - - function createItem(it) { - const node = els.tpl.content.firstElementChild.cloneNode(true); - const shortA = node.querySelector('.link-short'); - const long = node.querySelector('.link-long'); - const activeBadge = node.querySelector('.badge-active'); - const inactiveBadge = node.querySelector('.badge-inactive'); - const blockedBadge = node.querySelector('.badge-blocked'); - const expiredBadge = node.querySelector('.badge-expired'); - const pwBadge = node.querySelector('.badge-password'); - const mcBadge = node.querySelector('.badge-max-clicks'); - const privBadge = node.querySelector('.badge-private'); - const blockBotsBadge = node.querySelector('.badge-block-bots'); - const created = node.querySelector('.created-date'); - const last = node.querySelector('.last-click-date'); - const total = node.querySelector('.total-clicks-count'); - - // Short URL - shortA.textContent = trimProtocol(displayHost) + (it.alias ? it.alias : ''); - shortA.href = '/' + (it.alias || ''); - - // Long URL - long.textContent = it.long_url || ''; - long.title = it.long_url || ''; - - // Dates and clicks - created.textContent = formatDate(it.created_at); - last.textContent = formatTs(it.last_click); - total.textContent = (it.total_clicks ?? '0'); - - // Status badges - show appropriate badge based on status - if (it.status === 'ACTIVE') { - activeBadge.style.display = 'inline-flex'; - } else if (it.status === 'INACTIVE') { - inactiveBadge.style.display = 'inline-flex'; - } else if (it.status === 'BLOCKED') { - blockedBadge.style.display = 'inline-flex'; - node.classList.add('row-blocked'); - } else if (it.status === 'EXPIRED') { - expiredBadge.style.display = 'inline-flex'; - } - - // Password badge - if (it.password_set) { - pwBadge.style.display = 'inline-flex'; - } - - // Max clicks badge - if (typeof it.max_clicks === 'number') { - mcBadge.style.display = 'inline-flex'; - mcBadge.setAttribute('data-tooltip', `Max clicks: ${it.max_clicks}`); - } - - // Private stats badge - if (it.private_stats) { - privBadge.style.display = 'inline-flex'; - } - - // Block bots badge - if (it.block_bots) { - blockBotsBadge.style.display = 'inline-flex'; - } - - // Store full URL data on the row for the modal - node.setAttribute('data-url-data', JSON.stringify(it)); - node.style.cursor = 'pointer'; - node.classList.add('clickable-row'); - - return node; - } - - async function fetchData() { - setLoading(true); - els.empty.style.display = 'none'; - try { - const qs = buildQuery(); - const doFetch = (typeof window.authFetch === 'function') ? window.authFetch : fetch; - const res = await doFetch(`/api/v1/urls?${qs}`, { credentials: 'include' }); - if (!res.ok) { throw new Error('Request failed'); } - const data = await res.json(); - state.page = data.page; - state.pageSize = data.pageSize; - state.total = data.total; - state.hasNext = data.hasNext; - state.sortBy = data.sortBy; - state.sortOrder = data.sortOrder; - - clearList(); - if (!data.items || data.items.length === 0) { - els.empty.style.display = 'block'; - document.getElementById('links-table').style.display = 'none'; - els.pagination.style.display = 'none'; - return; - } - document.getElementById('links-table').style.display = 'block'; - const frag = document.createDocumentFragment(); - for (const it of data.items) { frag.appendChild(createItem(it)); } - els.list.appendChild(frag); - renderPagination(); - - // Initialize tooltips for the newly created items - initializeTooltips(); - } catch (err) { - clearList(); - els.empty.style.display = 'block'; - document.getElementById('links-table').style.display = 'none'; - } finally { - setLoading(false); - } - } - - // Initialize Tippy.js tooltips for attribute badges - function initializeTooltips() { - // Destroy existing tooltips first - if (window.attributeTooltips) { - window.attributeTooltips.forEach(instance => instance.destroy()); - } - window.attributeTooltips = []; - - // Find all tooltip triggers and initialize Tippy.js - const tooltipTriggers = document.querySelectorAll('.tooltip-trigger[data-tooltip]'); - - tooltipTriggers.forEach(element => { - // Remove the title attribute to prevent native tooltips - element.removeAttribute('title'); - - const instance = tippy(element, { - content: element.getAttribute('data-tooltip'), - placement: 'top', - theme: 'dark', - animation: 'fade', - duration: [200, 150], - delay: [200, 0], - arrow: true, - hideOnClick: false, - trigger: 'mouseenter focus', - zIndex: 9999 - }); - window.attributeTooltips.push(instance); - }); - } - - function renderPagination() { - const totalPages = Math.max(1, Math.ceil(state.total / state.pageSize)); - if (totalPages <= 1) { els.pagination.style.display = 'none'; return; } - els.pagination.style.display = 'flex'; - const start = (state.page - 1) * state.pageSize + 1; - const end = Math.min(state.total, state.page * state.pageSize); - els.pagination.innerHTML = ''; - const info = document.createElement('div'); - info.className = 'pagination-info'; - info.textContent = `Showing ${start} to ${end} of ${state.total}`; - - const container = document.createElement('div'); - container.className = 'pagination-controls'; - const prev = document.createElement('button'); prev.className = 'btn'; prev.textContent = 'Prev'; prev.disabled = state.page <= 1; - const next = document.createElement('button'); next.className = 'btn'; next.textContent = 'Next'; next.disabled = !state.hasNext; - prev.addEventListener('click', () => { if (state.page > 1) { state.page -= 1; fetchData(); } }); - next.addEventListener('click', () => { if (state.hasNext) { state.page += 1; fetchData(); } }); - container.appendChild(prev); - container.appendChild(next); - - els.pagination.appendChild(info); - els.pagination.appendChild(container); - } - - function applyFilters() { - state.page = 1; - fetchData(); - // Close the options dropdown with animation - if (els.optionsDropdown) { - els.optionsDropdown.classList.remove('show'); - } - } - - function resetFilters() { - for (const key of ['search', 'status', 'password', 'maxClicks', 'createdAfter', 'createdBefore']) { - if (els[key]) els[key].value = ''; - } - if (els.sortBy) els.sortBy.value = 'last_click'; - if (els.order) els.order.value = 'descending'; - // reset segmented visual state - document.querySelectorAll('.seg').forEach(seg => { - const targetId = seg.getAttribute('data-target'); - const hidden = document.getElementById(targetId); - const defaultValue = (targetId === 'f-order') ? 'descending' : ''; - if (hidden) hidden.value = defaultValue; - seg.setAttribute('data-active', (targetId === 'f-order') ? '0' : '2'); - }); - if (els.pageSize) els.pageSize.value = '20'; - applyFilters(); - } - - // wire events - els.apply.addEventListener('click', applyFilters); - els.reset.addEventListener('click', resetFilters); - els.search.addEventListener('keydown', (e) => { if (e.key === 'Enter') { applyFilters(); } }); - - // options dropdown toggle - function toggleOptions() { - if (!els.optionsDropdown) return; - const isOpen = els.optionsDropdown.classList.contains('show'); - if (isOpen) { - els.optionsDropdown.classList.remove('show'); - } else { - els.optionsDropdown.classList.add('show'); - } - } - if (els.optionsBtn) { els.optionsBtn.addEventListener('click', toggleOptions); } - window.addEventListener('click', (e) => { - if (!els.optionsDropdown) return; - if (e.target === els.optionsBtn || els.optionsBtn.contains(e.target)) { return; } - if (!els.optionsDropdown.contains(e.target)) { els.optionsDropdown.classList.remove('show'); } - }); - - // segmented controls behavior - function initSegments() { - const segs = document.querySelectorAll('.seg'); - segs.forEach(seg => { - const targetId = seg.getAttribute('data-target'); - const hidden = document.getElementById(targetId); - const buttons = Array.from(seg.querySelectorAll('button[data-value]')); - const indexByValue = new Map(buttons.map((b, i) => [b.getAttribute('data-value'), i])); - function apply(value) { - if (hidden) { hidden.value = value; } - const idx = indexByValue.has(value) ? indexByValue.get(value) : 0; - seg.setAttribute('data-active', String(idx)); - } - apply(hidden ? hidden.value : ''); - buttons.forEach(btn => btn.addEventListener('click', () => apply(btn.getAttribute('data-value') || ''))); - }); - } - - initSegments(); - - // Expose fetchData globally for other components to refresh the list - window.fetchData = fetchData; - - // initial load - fetchData(); -})(); - - diff --git a/static/js/dashboard/dashboard-base.js b/static/js/dashboard/dashboard-base.js index eec4eb72..fa15ae2d 100644 --- a/static/js/dashboard/dashboard-base.js +++ b/static/js/dashboard/dashboard-base.js @@ -1,25 +1,41 @@ -// Dashboard Base JavaScript +async function authFetch(url, options = {}) { + const { headers: callerHeaders, ...restOptions } = options; + const response = await fetch(url, { + ...restOptions, + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + ...callerHeaders, + }, + }); + if (response.status === 401) { + window.location.href = '/'; + } + return response; +} + +async function logout() { + const res = await authFetch('/auth/logout', { method: 'POST' }); + if (res.ok) { + window.location.href = '/'; + } +} + document.addEventListener('DOMContentLoaded', function () { - // Sidebar toggle functionality const sidebar = document.getElementById('sidebar'); const sidebarToggle = document.getElementById('sidebarToggle'); - const profileButton = document.getElementById('profileButton'); - const profileMenu = document.getElementById('profileMenu'); const navItems = document.querySelectorAll('.nav-item'); - // Load sidebar state from localStorage const sidebarState = localStorage.getItem('sidebarCollapsed'); if (sidebarState === 'true') { sidebar.classList.add('collapsed'); } - // Toggle sidebar sidebarToggle?.addEventListener('click', function () { sidebar.classList.toggle('collapsed'); const isCollapsed = sidebar.classList.contains('collapsed'); localStorage.setItem('sidebarCollapsed', isCollapsed); - // Update toggle icon const icon = sidebarToggle.querySelector('i'); if (icon) { if (isCollapsed) { @@ -30,32 +46,12 @@ document.addEventListener('DOMContentLoaded', function () { } // Close profile menu when collapsing - if (isCollapsed && profileMenu) { - profileMenu.classList.remove('active'); - profileButton.classList.remove('active'); - } - }); - - // Profile dropdown functionality - profileButton?.addEventListener('click', function (e) { - e.stopPropagation(); - profileMenu.classList.toggle('active'); - profileButton.classList.toggle('active'); - }); - - // Close profile menu when clicking outside - document.addEventListener('click', function (e) { - if (profileMenu && !profileMenu.contains(e.target) && !profileButton.contains(e.target)) { - profileMenu.classList.remove('active'); - profileButton.classList.remove('active'); + if (isCollapsed && window.Dropdown) { + const profileDd = document.querySelector('.profile-dropdown.dropdown'); + if (profileDd) window.Dropdown.close(profileDd); } }); - // Prevent menu from closing when clicking inside it - profileMenu?.addEventListener('click', function (e) { - e.stopPropagation(); - }); - // Mobile menu functionality const mobileMenuToggle = document.getElementById('mobileMenuToggle'); let sidebarOverlay = document.querySelector('.sidebar-overlay'); @@ -157,13 +153,10 @@ document.addEventListener('DOMContentLoaded', function () { sidebarToggle?.click(); } - // Escape to close mobile sidebar or profile menu + // Escape to close mobile sidebar (profile menu Esc handled by Dropdown primitive) if (e.key === 'Escape') { if (window.innerWidth <= 768 && sidebar.classList.contains('mobile-open')) { closeMobileSidebar(); - } else if (profileMenu?.classList.contains('active')) { - profileMenu.classList.remove('active'); - profileButton.classList.remove('active'); } } }); diff --git a/static/js/dashboard/dateRangePicker.js b/static/js/dashboard/dateRangePicker.js index 5ffd7d58..bbd848e9 100644 --- a/static/js/dashboard/dateRangePicker.js +++ b/static/js/dashboard/dateRangePicker.js @@ -46,6 +46,7 @@ class DateRangePicker { + + +