Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 69 additions & 6 deletions src/renderer/components/Layout/MenuBar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState, useSyncExternalStore, type ChangeEvent } from 'react'
import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore, type ChangeEvent } from 'react'
import {
BarChart3,
Boxes,
Expand Down Expand Up @@ -570,12 +570,19 @@ export const MenuBar = ({
<input ref={cameraImportRef} type="file" accept=".cameras.json,.json" className="hidden" onChange={handleImportCameras} />
<input ref={avplanImportRef} type="file" accept=".avplan,.json" className="hidden" onChange={handleImportAvplan} />
<input ref={sourceMapImportRef} type="file" accept=".avsourcemap,.json" className="hidden" onChange={handleImportSourceMap} />
<div className="flex shrink-0 items-center gap-2">
<div className="flex min-w-0 shrink items-center gap-2">
<span className="hidden select-none font-semibold tracking-wide text-cp-text-secondary lg:inline">
{t('app.title', 'Cable Planner')}
</span>
<span className="hidden text-cp-text-dimmer lg:inline">│</span>

{/* Die fuenf Menues in EINER Gruppe (B-77) — sie ist es, die auf
schmalen Fenstern rollt, und nicht die ganze Kopfzeile. Rollte die
Zeile, rollte der Einstellungen-Knopf rechts mit aus dem Bild, und
genau das verhindert der Kommentar weiter unten seit B-66.
Dieselbe Bauart wie `.bc-menubar` im `multicam-planner` und
`.tb-menubar` im `light-planner`. */}
<div className="cp-menubar">
<Menu label={t('app.menu.file', 'File')}>
<MenuItem onClick={onNewProject} icon={<Icon icon={FileText} size="sm" />} shortcut={t('shortcut.ctrlN', 'Ctrl+N')}>
{t('app.menu.file.new', 'New project')}
Expand Down Expand Up @@ -1145,6 +1152,7 @@ export const MenuBar = ({
{t('app.menu.help.about', 'About Cable Planner…')}
</MenuItem>
</Menu>
</div>
</div>

<div className="flex min-w-0 flex-1 items-center justify-center gap-2">
Expand Down Expand Up @@ -1250,6 +1258,10 @@ interface MenuProps {
const Menu = ({ label, children }: MenuProps) => {
const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement | null>(null)
const knopf = useRef<HTMLButtonElement | null>(null)
const klappe = useRef<HTMLDivElement | null>(null)
/** Das Rechteck des angeklickten Titels — Ausgangspunkt der Klappe. */
const [anker, setAnker] = useState<{ links: number; oben: number } | null>(null)

useEffect(() => {
if (!open) return
Expand All @@ -1261,14 +1273,52 @@ const Menu = ({ label, children }: MenuProps) => {
const onKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') setOpen(false)
}
// ROLLEN FUEHRT NACH, GROESSE SCHLIESST (B-77). Die Menue-Gruppe rollt
// auf schmalen Fenstern waagerecht, also bewegt sich der Titel — und die
// Klappe haengt seit B-77 am Fenster und merkt das nicht von selbst.
// Schliessen beim Rollen waere falsch: liegt ein Titel ausserhalb der
// Leiste, rollt ein Klick sie erst dorthin, und das Rollereignis kommt
// NACH dem Klick. Im `multicam-planner` ging die Klappe damit auf und
// sofort wieder zu (gemessen bei 390 px).
const nachfuehren = () => {
const r = knopf.current?.getBoundingClientRect()
if (r) setAnker({ links: r.left, oben: r.bottom + 4 })
}
const zu = () => setOpen(false)
document.addEventListener('mousedown', onDocClick)
document.addEventListener('keydown', onKey)
window.addEventListener('resize', zu)
window.addEventListener('scroll', nachfuehren, true)
return () => {
document.removeEventListener('mousedown', onDocClick)
document.removeEventListener('keydown', onKey)
window.removeEventListener('resize', zu)
window.removeEventListener('scroll', nachfuehren, true)
}
}, [open])

// B-77 — DIE KLAPPE HAENGT AM FENSTER, NICHT AN DER LEISTE.
//
// Sie war ein `absolute`-Kind der Kopfzeile. Solange nichts abschneidet,
// faellt das nicht auf; sobald ein Vorfahr rollt, ist die Klappe weg. Im
// `light-planner` ist genau das passiert: ein `overflow: hidden`, das die
// Kopfzeile einzeilig halten sollte, machte aus einer 250 x 505 px grossen
// Datei-Klappe eine mit 0 px sichtbarer Flaeche — vier Menues, kein
// einziges ging auf.
//
// Gemessen wird NACH dem Einhaengen: die Breite haengt am laengsten
// Eintrag und ist auf Deutsch eine andere als auf Englisch.
useLayoutEffect(() => {
const el = klappe.current
if (!el || !anker) return
const r = el.getBoundingClientRect()
const rand = 8
const links = Math.max(rand, Math.min(anker.links, window.innerWidth - rand - r.width))
const oben = Math.max(rand, Math.min(anker.oben, window.innerHeight - rand - r.height))
el.style.left = `${links}px`
el.style.top = `${oben}px`
}, [anker, open])

// #461 — Tastatur-Navigation im offenen Menue: beim Oeffnen ersten Eintrag
// fokussieren, Pfeile/Home/End bewegen den Fokus zwischen den menuitems.
useEffect(() => {
Expand Down Expand Up @@ -1311,10 +1361,15 @@ const Menu = ({ label, children }: MenuProps) => {
}

return (
<div ref={ref} className="relative">
<div ref={ref} className="relative shrink-0">
<button
type="button"
onClick={() => setOpen((v) => !v)}
ref={knopf}
onClick={(e) => {
const r = e.currentTarget.getBoundingClientRect()
setAnker({ links: r.left, oben: r.bottom + 4 })
setOpen((v) => !v)
}}
onKeyDown={(e) => {
if (e.key === 'ArrowDown') {
e.preventDefault()
Expand All @@ -1330,7 +1385,14 @@ const Menu = ({ label, children }: MenuProps) => {
Die Schrift bleibt gleich gross: schmaler wird der Abstand, nicht
das Wort. Ob es reicht, ist keine Meinung — `mobil:check` misst es
in JEDER ausgelieferten Sprache. */
className={` px-1 py-1 text-cp-text-bright hover:bg-cp-surface-2 sm:px-2 ${open ? 'bg-cp-surface-2' : ''}`}
/* `inline-flex items-center` und nicht nur Polsterung (B-77): seit der
Knopf 32 px hoch ist, hat sein Inhalt Platz fuer eine zweite Zeile
— und weil er in einer schrumpfenden Reihe steht, NAHM er sie sich.
Gemessen: aus 46 x 26 px wurden 36 x 37 px, das Zeichen stand unter
dem Wort, und die Kopfzeile war 5 px zu hoch. Eine Flex-Zeile bricht
nicht um; nebenbei ist es dieselbe Bauart wie in den anderen vier
Apps. */
className={` inline-flex items-center px-1 py-1 text-cp-text-bright hover:bg-cp-surface-2 sm:px-2 ${open ? 'bg-cp-surface-2' : ''}`}
>
{label}
<Icon icon={ChevronDown} size={11} className="ml-1 text-cp-text-muted" />
Expand All @@ -1346,7 +1408,8 @@ const Menu = ({ label, children }: MenuProps) => {
unter dem Fensterrand und waren nicht anklickbar. Dieselbe Sorte
Fehler wie die zwei Register, die aus der Bibliothek fielen:
ein Menuepunkt, den es fuer den Nutzer nicht gibt. */
className="absolute left-0 top-full z-50 mt-1 max-h-[calc(100vh-3.5rem)] min-w-[14rem] overflow-y-auto border border-[var(--cp-border)] bg-[var(--cp-surface-1)] py-1"
ref={klappe}
className="fixed left-0 top-0 z-50 max-h-[calc(100vh-3.5rem)] min-w-[14rem] overflow-y-auto border border-[var(--cp-border)] bg-[var(--cp-surface-1)] py-1"
role="menu"
>
{children}
Expand Down
132 changes: 132 additions & 0 deletions src/renderer/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -1036,3 +1036,135 @@ input[type="range"]:disabled {
opacity: 1;
}
}

/* ───────────────────────────────────────────────────────────────────────────
* B-77 — EIN BEDIENPUNKT IST 32 PX HOCH UND 32 PX BREIT.
*
* DER ANLASS. `bedienbar:check` in der Suite misst seit B-77 vier Planer im
* echten Browser. Dieser hier stand nicht in der Liste, weil sein `dist/`
* das Electron-Layout traegt (`renderer/index.html` statt einer
* Wurzel-Datei) — eine Eigenschaft des LAUFS, keine Auskunft ueber die App.
* Als der Ordner `dist/renderer` einmal bedient wurde, kam heraus
* (2026-09-13, 390/768/1440 px):
*
* Menueknoepfe (File · Edit · Tools · View · Help) 26 px hoch
* Projektname in der Kopfzeile 22 px
* Undo / Redo 30 x 22 px
* Einstellungen 22 px
* Spalten-Griffe (‹ ›) 28 x 28 px
* Reiter der Seitenleiste (Equipment · Cables · …) 26 px
* Suchfeld der Bibliothek 28 px
* die Kategoriezeilen der Bibliothek 18 px
* Kategorie-Chips (Video · Audio · Control · …) 24 px
* Schematic / Circuit 24 px
* die vier ReactFlow-Regler 26 x 27 px
* das Stil-Feld am Kabel 49 x 17 px
*
* 62 Stueck bei 1440 px, 33 bei 390 px. Achtzehn Pixel ist eine Zeile, die
* man mit der Maus trifft und mit dem Finger nicht; die Bibliothek ist die
* Flaeche, auf der in dieser App am meisten geklickt wird.
*
* WAS SCHON GUT WAR: nichts ragt aus dem Fenster, in keiner der drei
* Breiten. Das ist die Arbeit aus B-44 Teil 3 und `mobil:check`. Aber
* `mobil:check` misst UEBERLAUF und keine Zielgroessen — er war auf allen
* 62 Punkten gruen. Zwei Laeufe, zwei Fragen.
*
* WARUM 32. WCAG 2.2 zieht die Untergrenze bei 24 px (SC 2.5.8) und
* empfiehlt 44 px (SC 2.5.5). 32 liegt auf dem 8-px-Raster aus ADR-007, ist
* die Breite einer eingeklappten Spalte, und dieselbe Zahl fuehren
* `inventory-planner`, `larszu-facility-planner`, `multicam-planner` und
* `light-planner` als `--ziel`. Eine sechste Zahl waere eine sechste
* Wahrheit.
*
* WARUM EINE GLOBALE REGEL. Die Hoehen entstehen aus Tailwind-Polsterung
* (`py-1`, `px-2`, `text-[11px]`) und nicht aus einer Hoehen-Angabe. Sie
* einzeln nachzuziehen hiesse, dieselbe Entscheidung an 62 Stellen zu
* wiederholen — und die 63., die morgen jemand schreibt, faellt wieder
* durch. `min-height` gewinnt ausserdem gegen `height`, deshalb greift die
* Regel auch bei den ReactFlow-Reglern, deren Groesse aus dem Paket kommt
* und nicht aus diesem Stilblatt.
*
* WAS AUSGENOMMEN IST, und warum:
*
* * Kontrollkaestchen, Auswahlknoepfe, Schieberegler — ein 32-px-Kaestchen
* ist kein besseres Kaestchen, und der Regler wird ueber seinen Griff
* bedient.
* * ALLES AUF DEM CANVAS (`.react-flow__node`, `.react-flow__handle` und
* was darin steht). Eine Buchse am Geraet ist kein Knopf in einer
* Leiste: ihre Groesse kommt aus dem Raster des Geraets (B-48,
* `gridSize`), und sie auf 32 px zu heben verschoebe jeden Port und
* damit jedes Kabel. Der Canvas hat seine eigene Bedienung — Zoom — und
* seine eigene Messung.
* ─────────────────────────────────────────────────────────────────────────── */
:root {
--ziel: 32px;
}

button,
summary,
[role='button'],
select,
textarea,
input:not([type='checkbox']):not([type='radio']):not([type='range']) {
min-height: var(--ziel);
}

/* Die Breite nur dort, wo ein Knopf zum Quadrat schrumpfen kann — ein
* Eingabefeld ist nie zu schmal, ein Sinnbild-Knopf ohne Beschriftung schon
* (die Spalten-Griffe massen 28 px). */
button,
summary,
[role='button'] {
min-width: var(--ziel);
}

/* ───────────────────────────────────────────────────────────────────────────
* B-77 — DIE MENUE-GRUPPE ROLLT, DIE KOPFZEILE NICHT.
*
* WARUM SIE JETZT NOETIG IST. Die 32-px-Regel oben hat die kleinen Knoepfe
* der Kopfzeile breiter gemacht (Rueckgaengig, Wiederholen und der
* Einstellungen-Knopf massen 30 px), und die Zeile hatte keinen Spielraum
* mehr. Gemessen mit `mobil:check` auf 390 x 844 in DEUTSCH: der Projektname
* endete bei 410 px, die rechte Gruppe bei 438 px. Auf Englisch passte es —
* genau deshalb misst dieser Lauf jede ausgelieferte Sprache.
*
* WARUM DIE GRUPPE UND NICHT DIE ZEILE: rollte die Kopfzeile, rollte der
* Einstellungen-Knopf rechts mit aus dem Bild. Das ist die Lage, die B-66
* beschreibt und gegen die dort die Polsterung verkleinert wurde — dieses
* Mittel ist ausgereizt. Die Gruppe traegt nur die fuenf Menues; Marke links
* und Einstellungen rechts bleiben stehen.
*
* DASS SIE ROLLEN DARF, ist keine Selbstverstaendlichkeit: ein Rollbereich
* schneidet ab, was nicht hineinpasst, und die Klappen WAREN Kinder dieser
* Zeile. Sie haengen seit B-77 am Fenster (`MenuBar.tsx`, `position: fixed`)
* — im `light-planner` ist genau diese Kombination schiefgegangen, dort war
* jede Klappe monatelang 0 px hoch sichtbar.
*
* Dieselbe Bauart wie `.bc-menubar` im `multicam-planner` und `.tb-menubar`
* im `light-planner`. Drei Apps, ein Mechanismus.
* ─────────────────────────────────────────────────────────────────────────── */
.cp-menubar {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
}

.cp-menubar::-webkit-scrollbar {
display: none;
}

/* Der Canvas nimmt die Regel zurueck: siehe oben, die Groesse eines Ports
* gehoert dem Raster und nicht dieser Zeile. */
.react-flow__node button,
.react-flow__node select,
.react-flow__node input,
.react-flow__handle,
.react-flow__edge button,
.react-flow__edge select {
min-height: 0;
min-width: 0;
}
Loading