chore: Selector-UI-Prototyp als Primaerquelle sichern
Drei Varianten des Selector-Bildschirms, umschaltbar per ?variant= und ?state=, gebaut zur Beantwortung von Issue #8. Gewonnen hat B - eine Ansicht, in der die Workspace-Liste fuehrt und das Hinzufuegen Nebenaktion ist. Wegwerfcode, absichtlich nicht auf main: A und C haben ihren Zweck mit der Entscheidung erfuellt, wuerden auf main aber verrotten. Der Prototyp bindet die echte Projekt-CSS ein, ist also ohne Build im Browser zu oeffnen.
This commit is contained in:
236
prototype-selector-ui.html
Normal file
236
prototype-selector-ui.html
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<!--
|
||||||
|
PROTOTYP — WEGWERFCODE. Nicht in die App übernehmen.
|
||||||
|
|
||||||
|
Drei Varianten des Selector-Bildschirms, umschaltbar über ?variant=A|B|C,
|
||||||
|
dazu die Zustände über ?state=filled|empty|dialog.
|
||||||
|
Frage, die er beantwortet: Gitea-Issue #8 "Selector-UI: Anlegen und Öffnen".
|
||||||
|
|
||||||
|
Doppelklicken zum Öffnen. Bindet die echte CSS des Projekts ein, damit
|
||||||
|
Tokens und Komponenten nicht auseinanderlaufen.
|
||||||
|
-->
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>DioNotes — Selector-Prototyp</title>
|
||||||
|
<link rel="stylesheet" href="assets/css/tokens.css">
|
||||||
|
<link rel="stylesheet" href="assets/css/base.css">
|
||||||
|
<link rel="stylesheet" href="assets/css/layout.css">
|
||||||
|
<link rel="stylesheet" href="assets/css/components.css">
|
||||||
|
<style>
|
||||||
|
/* ---- Nur für den Prototyp. Was hier steht, ist noch kein Projekt-CSS. ---- */
|
||||||
|
|
||||||
|
body { margin: 0; background: var(--dn-bg-app); color: var(--dn-fg);
|
||||||
|
font-family: var(--dn-font-ui); font-size: var(--dn-text-base); }
|
||||||
|
|
||||||
|
/* Listeneintrag — in allen Varianten gleich, damit die Varianten sich in der
|
||||||
|
Struktur unterscheiden und nicht in der Zeilengestaltung. */
|
||||||
|
.ws-item { display: flex; align-items: center; gap: var(--dn-space-3);
|
||||||
|
width: 100%; padding: var(--dn-space-3); border: 1px solid transparent;
|
||||||
|
border-radius: var(--dn-radius); background: transparent; color: inherit;
|
||||||
|
text-align: left; cursor: pointer; font: inherit;
|
||||||
|
transition: background-color var(--dn-dur-fast) var(--dn-ease); }
|
||||||
|
.ws-item:hover { background: var(--dn-bg-raised); }
|
||||||
|
.ws-item-body { min-width: 0; flex: 1; }
|
||||||
|
.ws-item-name { color: var(--dn-fg); font-weight: 500; }
|
||||||
|
.ws-item-path { color: var(--dn-fg-faint); font-family: var(--dn-font-code);
|
||||||
|
font-size: var(--dn-text-xs); white-space: nowrap; overflow: hidden;
|
||||||
|
text-overflow: ellipsis; direction: rtl; text-align: left; }
|
||||||
|
.ws-item.is-broken .ws-item-name { color: var(--dn-fg-muted); }
|
||||||
|
|
||||||
|
.ws-badge { flex: none; padding: 2px var(--dn-space-2);
|
||||||
|
border-radius: 999px; font-size: var(--dn-text-xs); white-space: nowrap; }
|
||||||
|
.ws-badge.unreachable { background: color-mix(in oklab, var(--dn-warning) 18%, transparent); color: var(--dn-warning); }
|
||||||
|
.ws-badge.nomarker { background: color-mix(in oklab, var(--dn-fg-faint) 22%, transparent); color: var(--dn-fg-muted); }
|
||||||
|
.ws-badge.damaged { background: color-mix(in oklab, var(--dn-danger) 18%, transparent); color: var(--dn-danger); }
|
||||||
|
|
||||||
|
.ws-list { display: flex; flex-direction: column; gap: 2px; }
|
||||||
|
|
||||||
|
/* Variante B */
|
||||||
|
.vb-shell { max-width: 40rem; margin: 0 auto; padding: var(--dn-space-7) var(--dn-space-5);
|
||||||
|
display: flex; flex-direction: column; gap: var(--dn-space-5); height: 100%;
|
||||||
|
box-sizing: border-box; }
|
||||||
|
.vb-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--dn-space-4); }
|
||||||
|
.vb-title { font-size: var(--dn-text-xl); font-weight: 600; }
|
||||||
|
.vb-list-wrap { flex: 1; min-height: 0; overflow-y: auto; }
|
||||||
|
.vb-foot { border-top: 1px solid var(--dn-border); padding-top: var(--dn-space-4);
|
||||||
|
display: flex; justify-content: space-between; align-items: center; gap: var(--dn-space-3); }
|
||||||
|
|
||||||
|
/* Variante C */
|
||||||
|
.vc-shell { display: grid; grid-template-columns: 1fr 20rem; height: 100%; }
|
||||||
|
.vc-main { padding: var(--dn-space-7); display: flex; flex-direction: column;
|
||||||
|
gap: var(--dn-space-4); min-height: 0; }
|
||||||
|
.vc-side { background: var(--dn-bg-sunken); border-left: 1px solid var(--dn-border);
|
||||||
|
padding: var(--dn-space-6) var(--dn-space-5); display: flex; flex-direction: column;
|
||||||
|
gap: var(--dn-space-5); }
|
||||||
|
.vc-brand { font-size: var(--dn-text-xl); font-weight: 600; }
|
||||||
|
.vc-tag { color: var(--dn-fg-faint); font-size: var(--dn-text-xs);
|
||||||
|
letter-spacing: var(--dn-tracking-caps); text-transform: uppercase; }
|
||||||
|
.vc-list-wrap { flex: 1; min-height: 0; overflow-y: auto; }
|
||||||
|
.vc-drop { border: 1px dashed var(--dn-border-strong); border-radius: var(--dn-radius-lg);
|
||||||
|
padding: var(--dn-space-5); text-align: center; color: var(--dn-fg-muted);
|
||||||
|
font-size: var(--dn-text-sm); }
|
||||||
|
@media (max-width: 720px) { .vc-shell { grid-template-columns: 1fr; }
|
||||||
|
.vc-side { border-left: 0; border-top: 1px solid var(--dn-border); } }
|
||||||
|
|
||||||
|
/* Dialog — in allen Varianten identisch, er ist selbst kein Variantenthema */
|
||||||
|
.pt-scrim { position: fixed; inset: 0; background: rgb(0 0 0 / 0.55);
|
||||||
|
display: grid; place-items: center; z-index: 50; }
|
||||||
|
.pt-dialog { width: min(28rem, calc(100vw - 2rem)); background: var(--dn-bg-sunken);
|
||||||
|
border: 1px solid var(--dn-border-strong); border-radius: var(--dn-radius-lg);
|
||||||
|
box-shadow: var(--dn-shadow); padding: var(--dn-space-5);
|
||||||
|
display: flex; flex-direction: column; gap: var(--dn-space-4); }
|
||||||
|
.pt-dialog h2 { margin: 0; font-size: var(--dn-text-md); }
|
||||||
|
.pt-dialog p { margin: 0; color: var(--dn-fg-muted); line-height: var(--dn-leading-normal); }
|
||||||
|
.pt-dialog code { font-family: var(--dn-font-code); font-size: var(--dn-text-sm);
|
||||||
|
color: var(--dn-fg); }
|
||||||
|
.pt-actions { display: flex; justify-content: flex-end; gap: var(--dn-space-2); }
|
||||||
|
|
||||||
|
/* Umschalter — bewusst hässlich, damit er nicht als Teil des Entwurfs gelesen wird */
|
||||||
|
.pt-bar { position: fixed; bottom: var(--dn-space-4); left: 50%;
|
||||||
|
transform: translateX(-50%); z-index: 99; display: flex; align-items: center;
|
||||||
|
gap: var(--dn-space-1); padding: var(--dn-space-1); border-radius: 999px;
|
||||||
|
background: #000; border: 1px solid #555; box-shadow: 0 6px 24px rgb(0 0 0 / .5);
|
||||||
|
font-family: var(--dn-font-code); font-size: var(--dn-text-xs); }
|
||||||
|
.pt-bar button { border: 0; background: transparent; color: #fff; cursor: pointer;
|
||||||
|
padding: 6px 10px; border-radius: 999px; font: inherit; }
|
||||||
|
.pt-bar button:hover { background: #333; }
|
||||||
|
.pt-bar .pt-label { padding: 0 var(--dn-space-3); color: #fff; white-space: nowrap; }
|
||||||
|
.pt-bar .pt-sep { width: 1px; align-self: stretch; background: #444; margin: 0 4px; }
|
||||||
|
.pt-bar button.on { background: #fff; color: #000; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="main"><div id="stage"></div></div>
|
||||||
|
|
||||||
|
<div class="pt-bar">
|
||||||
|
<button id="prev" title="Vorherige Variante (←)">◀</button>
|
||||||
|
<span class="pt-label" id="label"></span>
|
||||||
|
<button id="next" title="Nächste Variante (→)">▶</button>
|
||||||
|
<span class="pt-sep"></span>
|
||||||
|
<button data-state="filled">Liste</button>
|
||||||
|
<button data-state="empty">Erststart</button>
|
||||||
|
<button data-state="dialog">Rückfrage</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const WS = [
|
||||||
|
{ name: "Notizen", path: "C:\\Users\\marvin\\Notizen" },
|
||||||
|
{ name: "DioNotes-Testdaten", path: "D:\\Projekte\\DioNotes-Testdaten" },
|
||||||
|
{ name: "Archiv 2024", path: "E:\\Backup\\Archiv 2024", badge: "unreachable", text: "nicht erreichbar" },
|
||||||
|
{ name: "Ideen", path: "C:\\Users\\marvin\\Ideen", badge: "nomarker", text: "kein Workspace mehr" },
|
||||||
|
{ name: "Rezepte", path: "C:\\Users\\marvin\\Rezepte",badge: "damaged", text: "beschädigt" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const esc = s => s.replace(/[&<>"]/g, c => ({ "&":"&","<":"<",">":">",'"':""" }[c]));
|
||||||
|
const item = w => `
|
||||||
|
<button class="ws-item ${w.badge ? "is-broken" : ""}">
|
||||||
|
<span class="ws-item-body">
|
||||||
|
<span class="ws-item-name">${esc(w.name)}</span>
|
||||||
|
<span class="ws-item-path">${esc(w.path)}</span>
|
||||||
|
</span>
|
||||||
|
${w.badge ? `<span class="ws-badge ${w.badge}">${esc(w.text)}</span>` : ""}
|
||||||
|
</button>`;
|
||||||
|
const list = () => `<div class="ws-list">${WS.map(item).join("")}</div>`;
|
||||||
|
|
||||||
|
const dialog = `
|
||||||
|
<div class="pt-scrim"><div class="pt-dialog">
|
||||||
|
<h2>Ordner ist nicht leer</h2>
|
||||||
|
<p><code>D:\\Notizen-Import</code> enthält bereits 34 Dateien. DioNotes legt darin
|
||||||
|
nur den Ordner <code>.dionotes/</code> an und verändert nichts Vorhandenes.</p>
|
||||||
|
<div class="pt-actions">
|
||||||
|
<button class="btn btn-ghost">Abbrechen</button>
|
||||||
|
<button class="btn btn-primary">Workspace anlegen</button>
|
||||||
|
</div>
|
||||||
|
</div></div>`;
|
||||||
|
|
||||||
|
/* ---- Variante A — Reiter, wie der heutige Code es anlegt ---------------- */
|
||||||
|
const A = state => `
|
||||||
|
<div class="workspace-selector-layout">
|
||||||
|
<div class="workspace-selector-links">
|
||||||
|
<a class="workspace-selector" href="#">Create new Workspace</a>
|
||||||
|
<a class="workspace-selector active" href="#">Open Workspace</a>
|
||||||
|
</div>
|
||||||
|
<div class="workspace-selector-view">
|
||||||
|
${state === "empty" ? `
|
||||||
|
<div class="empty-state">
|
||||||
|
<span class="empty-state-title">Noch kein Workspace</span>
|
||||||
|
<span>Lege einen an, um loszulegen.</span>
|
||||||
|
<button class="btn btn-primary">Ordner wählen…</button>
|
||||||
|
</div>` : list()}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
/* ---- Variante B — eine Ansicht, Liste führt, Anlegen ist Nebenaktion ---- */
|
||||||
|
const B = state => `
|
||||||
|
<div class="vb-shell">
|
||||||
|
<div class="vb-head">
|
||||||
|
<span class="vb-title">Workspace öffnen</span>
|
||||||
|
<span class="field-hint">${state === "empty" ? "" : WS.length + " bekannt"}</span>
|
||||||
|
</div>
|
||||||
|
<div class="vb-list-wrap">
|
||||||
|
${state === "empty" ? `
|
||||||
|
<div class="empty-state">
|
||||||
|
<span class="empty-state-title">Hier ist noch nichts</span>
|
||||||
|
<span>Der erste Workspace ist ein Ordner deiner Wahl.</span>
|
||||||
|
</div>` : list()}
|
||||||
|
</div>
|
||||||
|
<div class="vb-foot">
|
||||||
|
<span class="field-hint">Ein Workspace ist ein Ordner mit Notizen.</span>
|
||||||
|
<button class="btn ${state === "empty" ? "btn-primary" : ""}">Neuen anlegen…</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
/* ---- Variante C — Startbildschirm, zwei Spalten ------------------------- */
|
||||||
|
const C = state => `
|
||||||
|
<div class="vc-shell">
|
||||||
|
<div class="vc-main">
|
||||||
|
<span class="vc-tag">Zuletzt verwendet</span>
|
||||||
|
<div class="vc-list-wrap">
|
||||||
|
${state === "empty" ? `
|
||||||
|
<div class="vc-drop">Noch keine Workspaces.<br>Rechts einen Ordner wählen.</div>`
|
||||||
|
: list()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<aside class="vc-side">
|
||||||
|
<div>
|
||||||
|
<div class="vc-brand">DioNotes</div>
|
||||||
|
<span class="field-hint">Markdown-Notizen, lokal.</span>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<span class="field-label">Neuer Workspace</span>
|
||||||
|
<button class="btn btn-primary">Ordner wählen…</button>
|
||||||
|
<span class="field-hint">Legt <code>.dionotes/</code> im gewählten Ordner an.</span>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
const V = { A: [A, "Reiter (heutiger Code)"], B: [B, "Liste führt"], C: [C, "Startbildschirm"] };
|
||||||
|
const keys = Object.keys(V);
|
||||||
|
const p = new URLSearchParams(location.search);
|
||||||
|
let variant = keys.includes(p.get("variant")) ? p.get("variant") : "A";
|
||||||
|
let state = ["filled","empty","dialog"].includes(p.get("state")) ? p.get("state") : "filled";
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
const [fn, name] = V[variant];
|
||||||
|
document.getElementById("stage").innerHTML =
|
||||||
|
fn(state === "dialog" ? "filled" : state) + (state === "dialog" ? dialog : "");
|
||||||
|
document.getElementById("label").textContent = `${variant} — ${name}`;
|
||||||
|
document.querySelectorAll("[data-state]").forEach(b =>
|
||||||
|
b.classList.toggle("on", b.dataset.state === state));
|
||||||
|
history.replaceState(null, "", `?variant=${variant}&state=${state}`);
|
||||||
|
}
|
||||||
|
const cycle = d => { variant = keys[(keys.indexOf(variant) + d + keys.length) % keys.length]; render(); };
|
||||||
|
document.getElementById("prev").onclick = () => cycle(-1);
|
||||||
|
document.getElementById("next").onclick = () => cycle(1);
|
||||||
|
document.querySelectorAll("[data-state]").forEach(b =>
|
||||||
|
b.onclick = () => { state = b.dataset.state; render(); });
|
||||||
|
addEventListener("keydown", e => {
|
||||||
|
if (/^(INPUT|TEXTAREA)$/.test(e.target.tagName) || e.target.isContentEditable) return;
|
||||||
|
if (e.key === "ArrowLeft") cycle(-1);
|
||||||
|
if (e.key === "ArrowRight") cycle(1);
|
||||||
|
});
|
||||||
|
render();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user