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();