Add manual target drag and drop
This commit is contained in:
parent
ffa28f6182
commit
a4eee37ab9
4 changed files with 229 additions and 35 deletions
|
|
@ -21,6 +21,7 @@ func RegisterRoutes(mux *http.ServeMux) {
|
|||
mux.HandleFunc("/view", viewerHandler)
|
||||
mux.HandleFunc("/view/message", messageHandler)
|
||||
mux.HandleFunc("/view/target", targetMessageHandler)
|
||||
mux.HandleFunc("/view/manual-copy", manualCopyHandler)
|
||||
mux.HandleFunc("/view/forward", forwardHandler)
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ func homeHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// renderShell ist das Grundgeruest im Outlook-2013-Look:
|
||||
// [ Ribbon: Postfaecher | Betrachten | Betrachen | Einstellungen | Mail-Graveyard ]
|
||||
// [ Ordnerbaum | Nachrichtenliste | Original | Zielkopie ]
|
||||
// [ Archiv-mbox | Nachrichtenliste | Quelle/Target ]
|
||||
func renderShell(w http.ResponseWriter, active, readingPane string) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
|
||||
|
|
@ -53,7 +54,7 @@ func renderShell(w http.ResponseWriter, active, readingPane string) {
|
|||
</nav>
|
||||
<div class="ribbon-title">Mail-Graveyard</div>
|
||||
</header>
|
||||
<div class="four-pane">
|
||||
<div class="three-pane">
|
||||
<aside class="pane pane-tree" id="tree"
|
||||
hx-get="/view" hx-trigger="load" hx-target="#tree" hx-swap="innerHTML">
|
||||
<div class="pane-head">Postfächer</div>
|
||||
|
|
@ -64,17 +65,13 @@ func renderShell(w http.ResponseWriter, active, readingPane string) {
|
|||
</section>
|
||||
<div class="pane-resizer" data-resize-index="1"></div>
|
||||
<section class="pane pane-read" id="read">
|
||||
<div class="pane-head">Original</div>
|
||||
<div class="pane-head">Quelle</div>
|
||||
<div class="read-body">%s</div>
|
||||
</section>
|
||||
<div class="pane-resizer" data-resize-index="2"></div>
|
||||
<section class="pane pane-read pane-target" id="read-target">
|
||||
%s
|
||||
</section>
|
||||
</div>
|
||||
<footer class="statusbar"><span id="mig-status"
|
||||
hx-get="/migrate/status" hx-trigger="every 3s" hx-swap="innerHTML">bereit</span></footer>
|
||||
</body></html>`, active, readingPane, renderInitialTargetPane())
|
||||
</body></html>`, active, readingPane)
|
||||
}
|
||||
|
||||
// --- Stubs fuer die noch nicht gebauten Handler (TODO Codex) ---
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Der Viewer betrachtet die LOKALEN mbox-Dateien im Browser (Outlook-2013-
|
||||
|
|
@ -53,10 +54,10 @@ func messageHandler(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
msg, err := mail.ReadMessage(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
renderReadPanePair(w, account, folder, targetAccount, index, raw, "(unlesbar)", "", "", string(raw))
|
||||
renderReadPaneWithSource(w, account, folder, targetAccount, index, "(unlesbar)", "", "", string(raw))
|
||||
return
|
||||
}
|
||||
renderReadPanePair(w, account, folder, targetAccount, index, raw,
|
||||
renderReadPaneWithSource(w, account, folder, targetAccount, index,
|
||||
decodeHeader(msg.Header.Get("Subject")),
|
||||
decodeHeader(msg.Header.Get("From")),
|
||||
decodeHeader(msg.Header.Get("Date")),
|
||||
|
|
@ -84,7 +85,7 @@ func targetMessageHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
fmt.Fprint(w, renderTargetPane(account, folder, targetAccount, index, raw))
|
||||
fmt.Fprint(w, renderTargetPane(account, folder, targetAccount, index, raw, ""))
|
||||
}
|
||||
|
||||
func forwardHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -157,7 +158,8 @@ func renderMessageList(w http.ResponseWriter, account, folder, q string) {
|
|||
if from == "" {
|
||||
from = "(ohne Absender)"
|
||||
}
|
||||
fmt.Fprintf(w, `<a class="msg-row" href="#" hx-get="/view/message?source=%s&folder=%s&index=%d" hx-include="#target-account-select" hx-target="#read" hx-swap="innerHTML"><span class="msg-from">%s</span><span class="msg-subject">%s</span><span class="msg-date">%s</span></a>`,
|
||||
fmt.Fprintf(w, `<a class="msg-row" href="#" draggable="true" data-source="%s" data-folder="%s" data-index="%d" hx-get="/view/message?source=%s&folder=%s&index=%d" hx-include="#target-account-select" hx-target="#read" hx-swap="innerHTML"><span class="msg-from">%s</span><span class="msg-subject">%s</span><span class="msg-date">%s</span></a>`,
|
||||
html.EscapeString(sourceDisplayLabel(account)), html.EscapeString(folder), entry.Index,
|
||||
urlEsc(sourceDisplayLabel(account)), urlEsc(folder), entry.Index,
|
||||
html.EscapeString(from), html.EscapeString(subject), html.EscapeString(entry.Date))
|
||||
}
|
||||
|
|
@ -168,24 +170,60 @@ func renderMessageList(w http.ResponseWriter, account, folder, q string) {
|
|||
}
|
||||
}
|
||||
|
||||
func renderReadPanePair(w http.ResponseWriter, account, folder, targetAccount string, index int, raw []byte, subject, from, date, body string) {
|
||||
func manualCopyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
account := resolveViewerAccount(r.FormValue("account"), r.FormValue("source"))
|
||||
folder := r.FormValue("folder")
|
||||
targetAccount := r.FormValue("target_account")
|
||||
index, err := strconv.Atoi(r.FormValue("index"))
|
||||
if err != nil {
|
||||
http.Error(w, "bad index", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
path, err := mboxPath(account, folder)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
raw, err := ReadMboxMessage(path, index)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
note, err := appendManualTargetCopy(account, folder, targetAccount, raw)
|
||||
if err != nil {
|
||||
note = "Fehler beim manuellen Umzug: " + err.Error()
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
renderReadPane(w, "Original", subject, from, date, body)
|
||||
targetHTML := renderTargetPane(account, folder, targetAccount, index, raw)
|
||||
fmt.Fprintf(w, `<div id="read-target" hx-swap-oob="innerHTML">%s</div>`, targetHTML)
|
||||
fmt.Fprint(w, renderTargetPane(account, folder, targetAccount, index, raw, note))
|
||||
}
|
||||
|
||||
func renderReadPane(w io.Writer, title, subject, from, date, body string) {
|
||||
func renderReadPaneWithSource(w http.ResponseWriter, account, folder, targetAccount string, index int, subject, from, date, body string) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
renderReadPane(w, "Quelle", account, folder, targetAccount, index, subject, from, date, body)
|
||||
}
|
||||
|
||||
func renderReadPane(w io.Writer, title, account, folder, targetAccount string, index int, subject, from, date, body string) {
|
||||
if subject == "" {
|
||||
subject = "(ohne Betreff)"
|
||||
}
|
||||
fmt.Fprintf(w, `<div class="pane-head">%s</div><div class="read-body"><div class="read-head"><h1 class="read-subject">%s</h1><div class="read-meta">Von: %s<br>Datum: %s</div></div><pre class="read-pre">%s</pre></div>`,
|
||||
html.EscapeString(title), html.EscapeString(subject), html.EscapeString(from), html.EscapeString(date), html.EscapeString(body))
|
||||
fmt.Fprintf(w, `<div class="pane-head read-toolbar"><span>%s</span><button class="btn secondary compact" type="button" hx-get="/view/target?source=%s&folder=%s&index=%d&target_account=%s" hx-target="#read" hx-swap="innerHTML">Target</button></div><div class="read-body"><div class="read-head"><h1 class="read-subject">%s</h1><div class="read-meta">Von: %s<br>Datum: %s</div></div><pre class="read-pre">%s</pre></div>`,
|
||||
html.EscapeString(title), urlEsc(sourceDisplayLabel(account)), urlEsc(folder), index, urlEsc(targetAccount), html.EscapeString(subject), html.EscapeString(from), html.EscapeString(date), html.EscapeString(body))
|
||||
}
|
||||
|
||||
func renderTargetPane(account, folder, targetAccount string, index int, raw []byte) string {
|
||||
func renderTargetPane(account, folder, targetAccount string, index int, raw []byte, note string) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(renderTargetAccountSelect(account, folder, targetAccount, index))
|
||||
if note != "" {
|
||||
class := "ok"
|
||||
if strings.HasPrefix(note, "Fehler") {
|
||||
class = "bad"
|
||||
}
|
||||
fmt.Fprintf(&b, `<div class="audit-note %s">%s</div>`, class, html.EscapeString(note))
|
||||
}
|
||||
if match, ok, err := findTargetIMAPCopy(account, targetAccount, raw); ok {
|
||||
renderTargetMatch(&b, match)
|
||||
return b.String()
|
||||
|
|
@ -220,10 +258,7 @@ func renderTargetMatch(b *strings.Builder, match forwardedMatch) {
|
|||
}
|
||||
|
||||
func renderInitialTargetPane() string {
|
||||
var b strings.Builder
|
||||
b.WriteString(renderTargetAccountSelect("", "", "", -1))
|
||||
b.WriteString(`<div class="read-body"><div class="ef-empty">Nachricht waehlen.</div></div>`)
|
||||
return b.String()
|
||||
return `<div class="pane-head">Quelle</div><div class="read-body"><div class="ef-empty">Nachricht waehlen.</div></div>`
|
||||
}
|
||||
|
||||
func renderReadContent(w io.Writer, subject, from, date, body string) {
|
||||
|
|
@ -399,9 +434,9 @@ func messageIDHeader(raw []byte) string {
|
|||
func renderTargetAccountSelect(account, folder, selected string, index int) string {
|
||||
options := targetAccountOptions()
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, `<div class="pane-head target-head"><span>Zielkopie</span><form class="target-select-form"`)
|
||||
fmt.Fprintf(&b, `<div class="pane-head target-head"><span>Target</span><form class="target-select-form"`)
|
||||
if account != "" && folder != "" && index >= 0 {
|
||||
fmt.Fprintf(&b, ` hx-get="/view/target" hx-target="#read-target" hx-swap="innerHTML" hx-trigger="change from:#target-account-select"`)
|
||||
fmt.Fprintf(&b, ` hx-get="/view/target" hx-target="#read" hx-swap="innerHTML" hx-trigger="change from:#target-account-select"`)
|
||||
}
|
||||
fmt.Fprintf(&b, `>
|
||||
<input type="hidden" name="source" value="%s">
|
||||
|
|
@ -416,10 +451,73 @@ func renderTargetAccountSelect(account, folder, selected string, index int) stri
|
|||
}
|
||||
fmt.Fprintf(&b, `<option value="%s"%s>%s</option>`, html.EscapeString(opt.Value), attr, html.EscapeString(opt.Label))
|
||||
}
|
||||
b.WriteString(`</select></form></div>`)
|
||||
fmt.Fprintf(&b, `</select></form><button class="btn secondary compact" type="button" hx-get="/view/message?source=%s&folder=%s&index=%d&target_account=%s" hx-target="#read" hx-swap="innerHTML">Quelle</button></div>`,
|
||||
urlEsc(sourceDisplayLabel(account)), urlEsc(folder), index, urlEsc(selected))
|
||||
if account != "" && folder != "" && index >= 0 {
|
||||
fmt.Fprintf(&b, `<div class="drop-zone" data-drop-target="manual-copy" data-source="%s" data-folder="%s" data-index="%d">Mail hier ablegen fuer manuellen Umzug</div>`,
|
||||
html.EscapeString(sourceDisplayLabel(account)), html.EscapeString(folder), index)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func appendManualTargetCopy(account, folder, targetAccount string, raw []byte) (string, error) {
|
||||
id := messageIDHeader(raw)
|
||||
if id == "" {
|
||||
return "", fmt.Errorf("mail ohne Message-ID kann nicht sauber dedupliziert werden")
|
||||
}
|
||||
if _, ok, err := findTargetIMAPCopy(account, targetAccount, raw); ok {
|
||||
return "Schon vorhanden: Zielkopie wurde per Message-ID gefunden.", nil
|
||||
} else if err != nil {
|
||||
return "", err
|
||||
}
|
||||
a, ok := accountByMboxDir(account)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("kein Konto zur mbox %s gefunden", account)
|
||||
}
|
||||
if strings.TrimSpace(targetAccount) != "" && !strings.EqualFold(strings.TrimSpace(a.DstUser), strings.TrimSpace(targetAccount)) {
|
||||
candidates := accountsForTargetUser(targetAccount)
|
||||
if len(candidates) == 0 {
|
||||
return "", fmt.Errorf("kein Zielkonto %s gefunden", targetAccount)
|
||||
}
|
||||
a = candidates[0]
|
||||
}
|
||||
dst, err := OpenIMAPTarget(a)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer dst.Close()
|
||||
targets, err := dst.Folders()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
dstFolder := MapSourceToTarget(folder, nil, "/", dst.Delim(), targets, "")
|
||||
if dstFolder == "" {
|
||||
dstFolder = folder
|
||||
}
|
||||
if err := dst.EnsureFolder(dstFolder); err != nil {
|
||||
return "", err
|
||||
}
|
||||
msg := RawMessage{MessageID: id, Body: raw, InternalDate: messageDate(raw)}
|
||||
if err := dst.Append(dstFolder, msg); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if a.ID != 0 {
|
||||
_ = MarkCopied(a.ID, folder, id)
|
||||
}
|
||||
return "Manuell kopiert nach " + strings.TrimSpace(a.DstUser) + " / " + dstFolder + ".", nil
|
||||
}
|
||||
|
||||
func messageDate(raw []byte) time.Time {
|
||||
msg, err := mail.ReadMessage(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
if t, err := mail.ParseDate(msg.Header.Get("Date")); err == nil {
|
||||
return t
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
type viewerAccount struct {
|
||||
Name string
|
||||
SourceLabel string
|
||||
|
|
|
|||
85
frontend-js/dist/app.js
vendored
85
frontend-js/dist/app.js
vendored
|
|
@ -1,6 +1,6 @@
|
|||
(function () {
|
||||
const key = "mail-graveyard-pane-widths";
|
||||
const minWidths = [160, 220, 260, 260];
|
||||
const key = "mail-graveyard-pane-widths-v2";
|
||||
const minWidths = [160, 220, 300];
|
||||
const handleWidth = 6;
|
||||
|
||||
function panes(container) {
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
function clampWidths(widths, total) {
|
||||
const next = widths.map((w, i) => Math.max(minWidths[i], Math.round(w || minWidths[i])));
|
||||
const max = Math.max(total - handleWidth * 3, minWidths.reduce((a, b) => a + b, 0));
|
||||
const max = Math.max(total - handleWidth * (next.length - 1), minWidths.reduce((a, b) => a + b, 0));
|
||||
const sum = next.reduce((a, b) => a + b, 0);
|
||||
if (sum > max) {
|
||||
let extra = sum - max;
|
||||
|
|
@ -31,14 +31,14 @@
|
|||
const total = container.getBoundingClientRect().width;
|
||||
const w = clampWidths(widths, total);
|
||||
container.style.gridTemplateColumns =
|
||||
`${w[0]}px ${handleWidth}px ${w[1]}px ${handleWidth}px ${w[2]}px ${handleWidth}px ${w[3]}px`;
|
||||
`${w[0]}px ${handleWidth}px ${w[1]}px ${handleWidth}px minmax(${w[2]}px, 1fr)`;
|
||||
localStorage.setItem(key, JSON.stringify(w));
|
||||
}
|
||||
|
||||
function restore(container) {
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(key) || "null");
|
||||
if (Array.isArray(saved) && saved.length === 4) {
|
||||
if (Array.isArray(saved) && saved.length === 3) {
|
||||
applyWidths(container, saved);
|
||||
}
|
||||
} catch (_) {
|
||||
|
|
@ -90,4 +90,79 @@
|
|||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => bind(document.querySelector(".four-pane")));
|
||||
window.addEventListener("DOMContentLoaded", () => bind(document.querySelector(".three-pane")));
|
||||
})();
|
||||
|
||||
(function () {
|
||||
function rowPayload(row) {
|
||||
return {
|
||||
source: row.dataset.source || "",
|
||||
folder: row.dataset.folder || "",
|
||||
index: row.dataset.index || ""
|
||||
};
|
||||
}
|
||||
|
||||
document.addEventListener("dragstart", (event) => {
|
||||
const row = event.target.closest(".msg-row[draggable='true']");
|
||||
if (!row || !event.dataTransfer) return;
|
||||
event.dataTransfer.effectAllowed = "copy";
|
||||
event.dataTransfer.setData("application/json", JSON.stringify(rowPayload(row)));
|
||||
event.dataTransfer.setData("text/plain", `${row.dataset.source || ""}/${row.dataset.folder || ""}#${row.dataset.index || ""}`);
|
||||
row.classList.add("dragging");
|
||||
});
|
||||
|
||||
document.addEventListener("dragend", (event) => {
|
||||
const row = event.target.closest(".msg-row");
|
||||
if (row) row.classList.remove("dragging");
|
||||
});
|
||||
|
||||
document.addEventListener("dragover", (event) => {
|
||||
const zone = event.target.closest("[data-drop-target='manual-copy']");
|
||||
if (!zone) return;
|
||||
event.preventDefault();
|
||||
zone.classList.add("drag-over");
|
||||
if (event.dataTransfer) event.dataTransfer.dropEffect = "copy";
|
||||
});
|
||||
|
||||
document.addEventListener("dragleave", (event) => {
|
||||
const zone = event.target.closest("[data-drop-target='manual-copy']");
|
||||
if (zone) zone.classList.remove("drag-over");
|
||||
});
|
||||
|
||||
document.addEventListener("drop", async (event) => {
|
||||
const zone = event.target.closest("[data-drop-target='manual-copy']");
|
||||
if (!zone) return;
|
||||
event.preventDefault();
|
||||
zone.classList.remove("drag-over");
|
||||
|
||||
let payload = {};
|
||||
try {
|
||||
payload = JSON.parse(event.dataTransfer.getData("application/json") || "{}");
|
||||
} catch (_) {
|
||||
payload = {};
|
||||
}
|
||||
if (!payload.source) {
|
||||
payload = {
|
||||
source: zone.dataset.source || "",
|
||||
folder: zone.dataset.folder || "",
|
||||
index: zone.dataset.index || ""
|
||||
};
|
||||
}
|
||||
|
||||
const select = document.querySelector("#target-account-select");
|
||||
const form = new URLSearchParams();
|
||||
form.set("source", payload.source);
|
||||
form.set("folder", payload.folder);
|
||||
form.set("index", payload.index);
|
||||
form.set("target_account", select ? select.value : "");
|
||||
zone.textContent = "Kopiere...";
|
||||
|
||||
const response = await fetch("/view/manual-copy", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" },
|
||||
body: form.toString()
|
||||
});
|
||||
const html = await response.text();
|
||||
document.querySelector("#read").innerHTML = html;
|
||||
});
|
||||
})();
|
||||
|
|
|
|||
30
frontend-js/dist/style.css
vendored
30
frontend-js/dist/style.css
vendored
|
|
@ -48,14 +48,14 @@ body.ol2013 {
|
|||
.tab:hover { background: var(--ol-blue-dark); color: #fff; }
|
||||
.tab.active { background: #fff; color: var(--ol-blue); border-bottom-color: #fff; }
|
||||
|
||||
/* Vier-Spalten-Viewer */
|
||||
/* Viewer */
|
||||
.three-pane,
|
||||
.four-pane {
|
||||
flex: 1 1 auto;
|
||||
display: grid;
|
||||
min-height: 0; /* erlaubt Scrollen in den Panes */
|
||||
}
|
||||
.three-pane { grid-template-columns: 220px 340px 1fr; }
|
||||
.three-pane { grid-template-columns: 220px 6px 340px 6px minmax(300px, 1fr); }
|
||||
.four-pane { grid-template-columns: 220px 6px 340px 6px minmax(280px, 1fr) 6px minmax(280px, 1fr); }
|
||||
.pane {
|
||||
border-right: 1px solid var(--ol-border);
|
||||
|
|
@ -84,7 +84,7 @@ body.resizing { cursor: col-resize; user-select: none; }
|
|||
}
|
||||
.target-head {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
|
@ -124,6 +124,7 @@ a.tree-node { display: block; color: inherit; text-decoration: none; }
|
|||
display: flex; flex-direction: column; gap: 2px;
|
||||
}
|
||||
.msg-row:hover { background: #eef6fd; }
|
||||
.msg-row.dragging { opacity: .55; }
|
||||
.msg-row.active { background: var(--ol-sel); border-left: 3px solid var(--ol-blue); padding-left: 9px; }
|
||||
.msg-from { font-weight: 600; }
|
||||
.msg-subject { color: var(--ol-text); }
|
||||
|
|
@ -133,6 +134,12 @@ a.tree-node { display: block; color: inherit; text-decoration: none; }
|
|||
|
||||
/* Lesebereich */
|
||||
.read-body { padding: 16px 20px; }
|
||||
.read-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
.read-head { border-bottom: 1px solid var(--ol-border); padding-bottom: 10px; margin-bottom: 12px; }
|
||||
.read-subject { font-size: 17px; font-weight: 600; margin: 0 0 6px; }
|
||||
.read-meta { color: var(--ol-muted); font-size: 12px; }
|
||||
|
|
@ -145,6 +152,19 @@ a.tree-node { display: block; color: inherit; text-decoration: none; }
|
|||
}
|
||||
.ef-empty { color: var(--ol-muted); padding: 24px; }
|
||||
.audit-note { padding: 10px 16px; border-bottom: 1px solid var(--ol-border); font-size: 12px; }
|
||||
.drop-zone {
|
||||
margin: 10px 16px 0;
|
||||
padding: 12px;
|
||||
border: 1px dashed var(--ol-sel-bd);
|
||||
background: #f6fbff;
|
||||
color: var(--ol-muted);
|
||||
text-align: center;
|
||||
}
|
||||
.drop-zone.drag-over {
|
||||
background: var(--ol-sel);
|
||||
color: var(--ol-text);
|
||||
border-color: var(--ol-blue);
|
||||
}
|
||||
|
||||
/* Buttons / Formulare (flach, 2013er) */
|
||||
.btn {
|
||||
|
|
@ -154,6 +174,7 @@ a.tree-node { display: block; color: inherit; text-decoration: none; }
|
|||
.btn:hover { background: var(--ol-blue-dark); }
|
||||
.btn.secondary { background: #e1e1e1; color: var(--ol-text); border: 1px solid var(--ol-border); }
|
||||
.btn.secondary:hover { background: #d5d5d5; }
|
||||
.btn.compact { padding: 3px 9px; }
|
||||
.input, select, textarea {
|
||||
border: 1px solid #abadb3; padding: 5px 7px; font: inherit; width: 100%;
|
||||
background: #fff;
|
||||
|
|
@ -172,6 +193,9 @@ a.tree-node { display: block; color: inherit; text-decoration: none; }
|
|||
.bad { color: #d13438; }
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.three-pane {
|
||||
grid-template-columns: 200px 6px 320px 6px minmax(300px, 1fr);
|
||||
}
|
||||
.four-pane {
|
||||
grid-template-columns: 200px 6px 320px 6px minmax(280px, 1fr) 6px minmax(280px, 1fr);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue