From c0a726ab8add2dfb3a4ac5602da51c5064c8315b Mon Sep 17 00:00:00 2001 From: Max Berger Date: Thu, 23 Apr 2026 22:07:47 +0200 Subject: [PATCH] UI: More compact layour, auto-scroll to user in shares --- radicale/web/internal_data/css/main.css | 101 +++++++++++++----- radicale/web/internal_data/index.html | 32 +++--- .../web/internal_data/js/utils/url_text.js | 70 +++++++++++- 3 files changed, 160 insertions(+), 43 deletions(-) diff --git a/radicale/web/internal_data/css/main.css b/radicale/web/internal_data/css/main.css index 403244b4..101e8f18 100644 --- a/radicale/web/internal_data/css/main.css +++ b/radicale/web/internal_data/css/main.css @@ -19,14 +19,17 @@ main { .container { height: auto; - min-height: 450px; - width: 350px; + min-height: 500px; + width: 95%; + max-width: 450px; + box-sizing: border-box; transition: .2s; overflow: hidden; - padding: 20px 40px; - background: #fff; + padding: 1ex 1em 1ex 1em; + background: rgb(250, 250, 250); border: 1px solid #dadce0; border-radius: 8px; + box-shadow: 2px 2px 3px #0000001a; display: block; flex-shrink: 0; margin: 0 auto; @@ -39,12 +42,55 @@ main { color: #484848; } -.container h2 { - margin: 0; +table { width: 100%; + border-collapse: collapse; + margin: 1ex 0; +} + +th, +td { text-align: left; - color: #484848; - font-size: 14pt; + font-size: 0.8em; + padding: 0.2ex 0.2em; +} + +tr { + border-bottom: 1px solid #dadce0; +} + +tr:last-child { + border-bottom: none; +} + +th { + font-size: 0.8em; + color: #666; + font-weight: normal; + text-transform: uppercase; +} + +td.fill { + width: 100%; +} + +details { + border: 1px solid #dadce0; + border-radius: 0.7ex; + padding: 0.5ex 0.7ex; + margin-bottom: 1ex; + background: white; +} + +summary { + font-weight: bold; + cursor: pointer; + padding: 0.2ex 0; + outline: none; +} + +summary:hover { + color: #2494fe; } #loginscene .infcloudlink { @@ -394,6 +440,7 @@ select { padding-left: 15px; padding-right: 15px; outline: none !important; + box-sizing: border-box; } input.inline { @@ -402,7 +449,7 @@ input.inline { input[type=text], input[type=password] { - width: calc(100% - 30px); + width: 100%; } input:active, @@ -421,13 +468,22 @@ input:focus-visible { clear: both; } +td .url-input-wrapper input { + min-height: 4ex; +} + .url-input-wrapper input { flex: 1; - min-width: 0; - padding-right: 45px !important; + min-width: 40px; + min-height: 6ex; + padding-left: 0.5em; + padding-top: 0.5ex; + padding-bottom: 0.5ex; + padding-right: 22px !important; box-sizing: border-box; width: 100% !important; margin-bottom: 0 !important; + height: 2ex; } #collectionsscene article .url-input-wrapper { @@ -436,7 +492,7 @@ input:focus-visible { .copy-btn { flex: none; - margin-left: -38px; + margin-left: -30px; background: transparent; border: none; color: #2494fe; @@ -539,6 +595,9 @@ button.inline { padding-right: 1px; min-width: 2em; margin: 0; + display: flex; + align-items: center; + justify-content: center; } @media only screen and (max-width: 600px) { @@ -547,22 +606,10 @@ button.inline { flex-wrap: nowrap; } - #collectionsscene article { - height: auto; - min-height: 375px; - } - - .container { - max-width: 280px !important; - } - #collectionsscene ul { visibility: visible !important; } - #logoutview span { - padding: 0 5px; - } } .pill { @@ -573,12 +620,12 @@ button.inline { line-height: 1; text-align: center; white-space: nowrap; - vertical-align: baseline; - border-radius: 0.375rem; + vertical-align: middle; + border-radius: 0.7ex; background-color: #6c757d; color: #fff; margin: 0 2px; - width: 1.4em; + min-width: 1.3em; } .hidden { diff --git a/radicale/web/internal_data/index.html b/radicale/web/internal_data/index.html index e577a042..40bf5e96 100644 --- a/radicale/web/internal_data/index.html +++ b/radicale/web/internal_data/index.html @@ -158,10 +158,11 @@ - + - rwro + rwro
@@ -169,14 +170,17 @@
- + - + + + + @@ -189,10 +193,11 @@ - + - rwro + rwro
@@ -200,8 +205,8 @@
- + @@ -210,6 +215,9 @@ New Share by Map + + + diff --git a/radicale/web/internal_data/js/utils/url_text.js b/radicale/web/internal_data/js/utils/url_text.js index aa86b637..08d1cf89 100644 --- a/radicale/web/internal_data/js/utils/url_text.js +++ b/radicale/web/internal_data/js/utils/url_text.js @@ -25,6 +25,9 @@ import { SERVER } from "../constants.js"; * Handles the display of URLs in input fields. */ export class UrlTextHandler { + /** @type {?HTMLCanvasElement} */ + static _canvas = null; + /** * @param {HTMLInputElement} element The input element to handle. * @param {HTMLButtonElement} copyButton The button element for copying. @@ -32,9 +35,14 @@ export class UrlTextHandler { constructor(element, copyButton) { this._element = element; this._copyButton = copyButton; + this._userIndex = -1; this._element.addEventListener("focusin", () => { this._element.setSelectionRange(0, 99999); + this._updateScroll(); + }); + this._element.addEventListener("focusout", () => { + this._updateScroll(); }); if (this._copyButton) { @@ -42,6 +50,37 @@ export class UrlTextHandler { } } + _updateScroll() { + setTimeout(() => { + this._updateScroll_now(); + }, 0); + } + + /** + * Update the scroll position of the input field. + */ + _updateScroll_now() { + if (this._userIndex === -1) { + this._element.scrollLeft = this._element.scrollWidth; + return; + } + + let scrollPos = this._element.scrollWidth; + const value = this._element.value; + const style = window.getComputedStyle(this._element); + if (!UrlTextHandler._canvas) { + UrlTextHandler._canvas = document.createElement("canvas"); + } + const context = UrlTextHandler._canvas.getContext("2d"); + if (context) { + context.font = style.font; + const prefix = value.substring(0, this._userIndex); + scrollPos = context.measureText(prefix).width; + } + + this._element.scrollLeft = scrollPos; + } + /** * Copy the current value of the input to the clipboard. */ @@ -76,10 +115,33 @@ export class UrlTextHandler { } else { this._element.value = href; } + this._update_username_index(); + this._updateScroll(); + } - // Needs timeout to work correctly. - setTimeout(() => { - this._element.scrollLeft = this._element.scrollWidth; - }, 0); + /** + * Update the username index. + */ + _update_username_index() { + const value = this._element.value; + this._userIndex = -1; + try { + const url = new URL(value); + const path = url.pathname; + const parts = path.split("/").filter(p => p !== ""); + if (parts.length >= 2) { + const user = parts[parts.length - 2]; + const search = "/" + user + "/"; + const pathIndex = path.lastIndexOf(search); + if (pathIndex !== -1) { + const fullIndex = value.lastIndexOf(path); + if (fullIndex !== -1) { + this._userIndex = fullIndex + pathIndex; + } + } + } + } catch (e) { + this._userIndex = -1; + } } }