diff --git a/IDEAS.md b/IDEAS.md index 292ec2d..322702c 100644 --- a/IDEAS.md +++ b/IDEAS.md @@ -117,9 +117,15 @@ Zeigerereignis-Variante lohnt erst, wenn sie mehrfach gebraucht wird, und genau das kommt: „Widgets verschieben" braucht sie zwingend. Wird sie dort gebaut, kann die App-Liste sie später erben. -Dazu ein Knopf **A–Z**: einmal alphabetisch sortieren, danach ist es wieder -Handarbeit. Kein dauerhafter Modus, keine Einstellung, keine zwei -Sortierzustände im Kopf. +**Kein A–Z-Knopf** (Marc, 29.08.2026): erstmal zu viel. Bei einer Handvoll Apps +sortiert man einmal von Hand und rührt es nie wieder an — ein zweiter Weg +dorthin ist Zierat. Kommt zurück, wenn er fehlt. + +**Die Zifferntasten sind bei der Gelegenheit ganz weggefallen.** Sie waren ein +Pflichtfeld je App, das man beim Anlegen ausfüllen musste, und sie hätten beim +Sortieren dieselbe Frage aufgeworfen: mitwandern oder stehenbleiben? Beides +falsch. Neue Apps hängen sich jetzt einfach hinten an, die Reihenfolge macht man +mit den Pfeilen. Beim Bauen zu beachten: @@ -128,8 +134,6 @@ Beim Bauen zu beachten: - Beim Neuzeichnen müssen offene Zeilen offen bleiben. - Gespeichert wird wie bisher die ganze Liste auf einmal: Man kann probieren, ohne dass sofort alles umspringt. -- **Die Zifferntasten wandern nicht mit.** Die stehen je App fest; sonst startet - `2` morgen etwas anderes als heute. #### Und eine eigene Reihenfolge je Nutzer? — später, aber der Weg bleibt offen diff --git a/README.md b/README.md index 40dd6b2..539c33d 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,10 @@ fragt über die Domaingrenze und bleibt dort leer. | Name, Untertitel | Beschriftung auf Kachel und Symbol | | Adresse | wohin der Start springt | | Symbol | cloud · note · box · chip · folder · gear · globe · disk | -| Taste | Zifferntaste im Terminal-Modus | + +**Die Reihenfolge machst du mit den Pfeilen** ▲▼ in der Kopfzeile jeder Zeile — sie gilt +überall gleich: Streifen, Schublade, Kacheln, Schreibtischsymbole, Startmenü. Neue Apps +hängen sich hinten an. Gespeichert wird die Liste als Ganzes; der Server prüft sie noch einmal und antwortet mit dem, was er wirklich behalten hat. Eine Adresse ohne `https://` kommt als Fehlermeldung @@ -178,7 +181,7 @@ und in jeder App, ohne dass eine App neu gebaut wird. Umschalten mit **`M`**, über *Einstellungen → Betriebsart* oder im Startmenü. Gleich bleiben in beiden: Menüleiste, Palette, Einstellungen, App-Liste. -**Tasten:** `1` `2` App starten · `M` Betriebsart · `S` Einstellungen · `?` Hilfe · +**Tasten:** `M` Betriebsart · `S` Einstellungen · `?` Hilfe · `Esc` schließt Startmenü, oberstes Fenster oder Dialog — in dieser Reihenfolge. ## Wetter diff --git a/internal/store/store.go b/internal/store/store.go index 15c98b2..6e393b6 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -29,7 +29,6 @@ type App struct { Subtitle string `json:"subtitle,omitempty"` URL string `json:"url"` Glyph string `json:"glyph,omitempty"` - Key string `json:"key,omitempty"` } type User struct { @@ -120,7 +119,6 @@ func (s *Store) SetApps(apps []App) error { a.Subtitle = strings.TrimSpace(a.Subtitle) a.URL = strings.TrimSpace(a.URL) a.Glyph = strings.TrimSpace(a.Glyph) - a.Key = strings.TrimSpace(a.Key) if !idPattern.MatchString(a.ID) { return fmt.Errorf("App %d: ID muss klein geschrieben sein und mit Buchstabe oder Ziffer beginnen", i+1) @@ -140,9 +138,6 @@ func (s *Store) SetApps(apps []App) error { if a.Glyph != "" && !knownGlyphs[a.Glyph] { a.Glyph = "" } - if len([]rune(a.Key)) > 1 { - return fmt.Errorf("App %q: Taste ist ein einzelnes Zeichen", a.ID) - } for _, f := range []string{a.Name, a.Subtitle, a.URL} { if len(f) > maxFieldLen { return fmt.Errorf("App %q: ein Feld ist länger als %d Zeichen", a.ID, maxFieldLen) diff --git a/web/css/style.css b/web/css/style.css index e62cd49..bc0f08b 100644 --- a/web/css/style.css +++ b/web/css/style.css @@ -372,13 +372,6 @@ body { } .tile:active { transform: none; } -.tile-key { - position: absolute; - top: 8px; - right: 10px; - font-size: 11px; - color: var(--p-dim); -} .tile-icon { width: 46px; @@ -464,8 +457,6 @@ body { .tile-name { grid-area: name; } .tile-sub { grid-area: unter; } .tile-state { grid-area: zustand; margin: 0; white-space: nowrap; } - /* Die Zifferntaste hilft nur mit Tastatur. */ - .tile-key { display: none; } } /* ── Modal ─────────────────────────────────────────────────────────────── */ @@ -954,6 +945,23 @@ body { .approw .ae-url { margin-left: auto; color: var(--p-dim); font-size: 11px; max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .approw .ae-body { padding: 10px; display: flex; flex-direction: column; gap: 8px; } +/* Sortieren, ohne die Zeile aufzumachen: Die Pfeile sitzen in der Kopfzeile. + Die Adresse rückt dafür ein Stück nach links (kein margin-left:auto mehr an + ihr allein, sondern hier). */ +.approw .ae-move { margin-left: 8px; display: flex; gap: 2px; flex: none; } +.ae-arrow { + background: none; + border: 1px solid var(--p-dim); + color: var(--p-mid); + font: inherit; + font-size: 10px; + line-height: 1; + padding: 4px 6px; + cursor: pointer; +} +.ae-arrow:hover:not(:disabled) { background: var(--p); color: var(--bg); border-color: var(--p); text-shadow: none; } +.ae-arrow:disabled { opacity: 0.3; cursor: default; } + .ae-field { display: grid; grid-template-columns: 92px 1fr; gap: 8px; align-items: center; } .ae-field > span { font-size: 11px; color: var(--p-dim); letter-spacing: 0.1em; } .ae-field input, .ae-field select { diff --git a/web/index.html b/web/index.html index 0accdc7..eb9027e 100644 --- a/web/index.html +++ b/web/index.html @@ -92,7 +92,7 @@ diff --git a/web/js/os.js b/web/js/os.js index 27179a2..06b9207 100644 --- a/web/js/os.js +++ b/web/js/os.js @@ -277,7 +277,6 @@ function renderTiles() { a.target = state.settings.openIn === "tab" ? "_blank" : "_self"; a.rel = "noopener"; a.innerHTML = ` - ${app.key ? `[${app.key}]` : ""} ${glyphSvg(app.glyph)} ${escapeHtml(app.name)} ${escapeHtml(app.subtitle || "")} @@ -707,11 +706,17 @@ function openApps() { const GLYPHS_LIST = Object.keys(GLYPHS).filter((g) => !WETTER_GLYPHEN.includes(g)); const rowHtml = (a, i) => ` -
+
${glyphSvg(a.glyph)} ${escapeHtml(a.name || a.id || "neue app")} ${escapeHtml(a.url || "")} + + + +
-
@@ -752,6 +755,35 @@ function openApps() { render(root); }) ); + + // Merken, was offen ist — das Umsortieren zeichnet die Liste neu, und ohne + // das klappte dabei alles zu, was man gerade bearbeitet. + list.querySelectorAll(".approw").forEach((d, i) => { + d.addEventListener("toggle", () => { + if (draft[i]) draft[i].__offen = d.open; + }); + }); + + // Die Pfeile sitzen im : Ohne preventDefault klappt jeder + // Sortierklick die Zeile auf oder zu. + const tausche = (i, j) => { + if (j < 0 || j >= draft.length) return; + [draft[i], draft[j]] = [draft[j], draft[i]]; + render(root); + // Zurück auf denselben Knopf, damit man mehrmals hintereinander drücken + // kann, ohne die Maus nachzuführen. + const wieder = list.querySelector(`[data-${j < i ? "up" : "down"}="${j}"]`); + if (wieder && !wieder.disabled) wieder.focus(); + }; + + list.querySelectorAll("[data-up], [data-down]").forEach((b) => + b.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + const i = Number(b.dataset.up ?? b.dataset.down); + tausche(i, "up" in b.dataset ? i - 1 : i + 1); + }) + ); }; openPanel( @@ -768,7 +800,8 @@ function openApps() { render(root); root.querySelector("#btnAdd").addEventListener("click", () => { - draft.push({ id: "", name: "", url: "", glyph: "box", key: String(draft.length + 1), __neu: true }); + // Immer ans Ende — sortiert wird mit den Pfeilen, nicht beim Anlegen. + draft.push({ id: "", name: "", url: "", glyph: "box", __offen: true }); render(root); }); @@ -777,9 +810,10 @@ function openApps() { msg.dataset.kind = ""; msg.textContent = "speichere …"; try { - const clean = draft.map(({ __neu, ...rest }) => rest); + const clean = draft.map(({ __offen, ...rest }) => rest); state.cfg.apps = (await api("/api/apps", { method: "PUT", body: JSON.stringify(clean) })).apps; - draft = state.cfg.apps.map((a) => ({ ...a })); + const offen = new Set(draft.filter((a) => a.__offen).map((a) => a.id)); + draft = state.cfg.apps.map((a) => ({ ...a, __offen: offen.has(a.id) })); msg.textContent = `${state.cfg.apps.length} gespeichert.`; render(root); renderMenubarApps(); @@ -807,7 +841,6 @@ function openAbout() { desktop Symbole und Fenster. Die HoneyBar oben ist in beiden dieselbe — und dieselbe steckt per honeybar/honeybar.js auch in Aether und InkNote.

- ${state.cfg.apps.map((a) => `
${a.key || "·"}
${escapeHtml(a.name)} starten
`).join("")}
M
Betriebsart wechseln
S
Einstellungen
?
diese Hilfe
@@ -1137,8 +1170,6 @@ async function main() { } if (typing) return; - const app = state.cfg.apps.find((a) => a.key === e.key); - if (app) return launch(app); if (e.key === "m" || e.key === "M") setMode(state.settings.mode === "desktop" ? "terminal" : "desktop"); if (e.key === "s" || e.key === "S") openSettings(); if (e.key === "?") openAbout();