Native Selects auf einheitliche Dropdown-Komponente umgestellt

Mehrere Stellen (ResourceManager SelectField + Material-Browser,
ImportDialog, AttributesPanel, SitePanel, ToolsPanel,
DisplayModeSelect, RichTextEditor) nutzten noch native <select>, deren
Optionsliste vom Betriebssystem gerendert wird und optisch nicht zur
eigenen Dropdown-Komponente (dunkles Kontextmenü-Popover) passt. Jetzt
durchgaengig dieselbe Optik.
This commit is contained in:
2026-07-04 05:01:03 +02:00
parent 9f6d4e8858
commit e0d71691e1
7 changed files with 85 additions and 118 deletions
+18 -21
View File
@@ -28,6 +28,7 @@ import { formatM } from "../model/types";
import { PEN_WEIGHTS } from "../model/types"; import { PEN_WEIGHTS } from "../model/types";
import type { AttributeSource } from "../model/types"; import type { AttributeSource } from "../model/types";
import { usePanelHost } from "./host"; import { usePanelHost } from "./host";
import { Dropdown } from "../ui/Dropdown";
/** UI-Zustand des 3-Optionen-Quellen-Dropdowns (nicht 1:1 das Modell-Feld: /** UI-Zustand des 3-Optionen-Quellen-Dropdowns (nicht 1:1 das Modell-Feld:
* „custom" ist abgeleitet aus „ist ein expliziter Wert gesetzt?", nicht * „custom" ist abgeleitet aus „ist ein expliziter Wert gesetzt?", nicht
@@ -77,17 +78,17 @@ export function AttributesPanel() {
ui: UiSource, ui: UiSource,
onChange: (next: UiSource) => void, onChange: (next: UiSource) => void,
) => ( ) => (
<select <Dropdown
className="attr-select" width={104}
style={{ width: 104 }}
value={ui} value={ui}
disabled={!editable} disabled={!editable}
onChange={(e) => onChange(e.target.value as UiSource)} onChange={(v) => onChange(v as UiSource)}
> options={[
<option value="layer">{t("attr.source.layer")}</option> { value: "layer", label: t("attr.source.layer") },
<option value="object">{t("attr.source.object")}</option> { value: "object", label: t("attr.source.object") },
<option value="custom">{t("attr.source.custom")}</option> { value: "custom", label: t("attr.source.custom") },
</select> ]}
/>
); );
// Farb-Swatch-Eingabe für „eigener Wert" (Vordergrund/Hintergrund). Erscheint // Farb-Swatch-Eingabe für „eigener Wert" (Vordergrund/Hintergrund). Erscheint
@@ -246,19 +247,15 @@ export function AttributesPanel() {
fillEditable, fillEditable,
hatchUi, hatchUi,
onHatchSourceChange, onHatchSourceChange,
<select <Dropdown
className="attr-select" width={104}
style={{ width: 104 }}
value={sel.fillHatchId ?? ""} value={sel.fillHatchId ?? ""}
onChange={(e) => host.onSetSelectionFill(e.target.value || null)} onChange={(v) => host.onSetSelectionFill(v || null)}
> options={[
<option value="">{t("attr.none")}</option> { value: "", label: t("attr.none") },
{project.hatches.map((h) => ( ...project.hatches.map((h) => ({ value: h.id, label: h.name })),
<option key={h.id} value={h.id}> ]}
{h.name} />,
</option>
))}
</select>,
)} )}
</div> </div>
+6 -11
View File
@@ -10,6 +10,7 @@
import type { DisplayMode } from "./types"; import type { DisplayMode } from "./types";
import { t } from "../i18n"; import { t } from "../i18n";
import { Dropdown } from "../ui/Dropdown";
/** /**
* Beschriftungs-Keys je Modus (Werte englisch, vgl. types.ts) — exakt die * Beschriftungs-Keys je Modus (Werte englisch, vgl. types.ts) — exakt die
@@ -41,18 +42,12 @@ export function DisplayModeSelect({
}) { }) {
const label = title ?? t("display.title"); const label = title ?? t("display.title");
return ( return (
<select <Dropdown
className="display-mode" triggerClassName="display-mode"
value={value} value={value}
title={label} title={label}
aria-label={label} onChange={(v) => onChange(v as DisplayMode)}
onChange={(e) => onChange(e.target.value as DisplayMode)} options={MODE_OPTIONS.map((o) => ({ value: o.value, label: t(o.labelKey) }))}
> />
{MODE_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{t(o.labelKey)}
</option>
))}
</select>
); );
} }
+6 -11
View File
@@ -17,6 +17,7 @@ import type { TranslationKey } from "../i18n";
import { usePanelHost } from "./host"; import { usePanelHost } from "./host";
import type { ContextObject } from "../model/types"; import type { ContextObject } from "../model/types";
import { ContextImportDialog } from "../ui/ContextImportDialog"; import { ContextImportDialog } from "../ui/ContextImportDialog";
import { Dropdown } from "../ui/Dropdown";
/** Typ-spezifisches Badge-Label (i18n-Key) je Kontext-Objekt-Art. */ /** Typ-spezifisches Badge-Label (i18n-Key) je Kontext-Objekt-Art. */
function typeBadgeKey(obj: ContextObject): TranslationKey { function typeBadgeKey(obj: ContextObject): TranslationKey {
@@ -85,18 +86,12 @@ export function SitePanel() {
{contourSets.length > 0 && ( {contourSets.length > 0 && (
<div className="site-terrain-row"> <div className="site-terrain-row">
{contourSets.length > 1 ? ( {contourSets.length > 1 ? (
<select <Dropdown
className="site-select"
value={effectiveSet} value={effectiveSet}
onChange={(e) => setSelectedSet(e.target.value)} onChange={(v) => setSelectedSet(v)}
aria-label={t("site.contourSet")} title={t("site.contourSet")}
> options={contourSets.map((cs) => ({ value: cs.id, label: cs.name }))}
{contourSets.map((cs) => ( />
<option key={cs.id} value={cs.id}>
{cs.name}
</option>
))}
</select>
) : ( ) : (
<span className="site-terrain-src" title={t("site.contourSet")}> <span className="site-terrain-src" title={t("site.contourSet")}>
{contourSets[0].name} {contourSets[0].name}
+15 -18
View File
@@ -13,6 +13,7 @@ import { t } from "../i18n";
import { usePanelHost } from "./host"; import { usePanelHost } from "./host";
import type { ToolId } from "./host"; import type { ToolId } from "./host";
import { TOOL_ORDER, getTool } from "../tools/tools"; import { TOOL_ORDER, getTool } from "../tools/tools";
import { Dropdown } from "../ui/Dropdown";
// ── Icons ────────────────────────────────────────────────────────────────── // ── Icons ──────────────────────────────────────────────────────────────────
// Keine echten Asset-Icons vorhanden: schlichte Inline-SVG-Glyphen je Werkzeug // Keine echten Asset-Icons vorhanden: schlichte Inline-SVG-Glyphen je Werkzeug
@@ -153,17 +154,15 @@ export function ToolsPanel() {
{host.activeTool === "wall" && ( {host.activeTool === "wall" && (
<label className="tools-field"> <label className="tools-field">
<span>{t("tool.wallType")}</span> <span>{t("tool.wallType")}</span>
<select <Dropdown
value={host.activeWallTypeId} value={host.activeWallTypeId}
disabled={!host.toolsEnabled} disabled={!host.toolsEnabled}
onChange={(e) => host.onActiveWallTypeId(e.target.value)} onChange={(v) => host.onActiveWallTypeId(v)}
> options={host.project.wallTypes.map((wt) => ({
{host.project.wallTypes.map((wt) => ( value: wt.id,
<option key={wt.id} value={wt.id}> label: wt.name,
{wt.name} }))}
</option> />
))}
</select>
</label> </label>
)} )}
@@ -174,17 +173,15 @@ export function ToolsPanel() {
{host.activeTool === "ceiling" && ( {host.activeTool === "ceiling" && (
<label className="tools-field"> <label className="tools-field">
<span>{t("tool.ceilingType")}</span> <span>{t("tool.ceilingType")}</span>
<select <Dropdown
value={host.activeWallTypeId} value={host.activeWallTypeId}
disabled={!host.toolsEnabled} disabled={!host.toolsEnabled}
onChange={(e) => host.onActiveWallTypeId(e.target.value)} onChange={(v) => host.onActiveWallTypeId(v)}
> options={host.project.wallTypes.map((wt) => ({
{host.project.wallTypes.map((wt) => ( value: wt.id,
<option key={wt.id} value={wt.id}> label: wt.name,
{wt.name} }))}
</option> />
))}
</select>
</label> </label>
)} )}
+10 -16
View File
@@ -9,6 +9,7 @@
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { t } from "../i18n"; import { t } from "../i18n";
import { docToHtml } from "./renderHtml"; import { docToHtml } from "./renderHtml";
import { Dropdown } from "../ui/Dropdown";
import { import {
applyMark, applyMark,
applyPreset, applyPreset,
@@ -574,15 +575,13 @@ export function RichTextEditor({
); );
const handlePreset = useCallback( const handlePreset = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => { (id: string) => {
const id = e.target.value;
const preset = effectivePresets.find((p) => p.id === id); const preset = effectivePresets.find((p) => p.id === id);
if (!preset) return; if (!preset) return;
const range = selRange ?? { start: 0, end: 0 }; const range = selRange ?? { start: 0, end: 0 };
const newDoc = applyPreset(valueRef.current, range, preset); const newDoc = applyPreset(valueRef.current, range, preset);
onChangeRef.current(newDoc); onChangeRef.current(newDoc);
setEditorHtml(newDoc, selRange, true); setEditorHtml(newDoc, selRange, true);
e.target.value = "";
}, },
[effectivePresets, selRange, setEditorHtml], [effectivePresets, selRange, setEditorHtml],
); );
@@ -688,21 +687,16 @@ export function RichTextEditor({
{/* Presets */} {/* Presets */}
{effectivePresets.length > 0 && ( {effectivePresets.length > 0 && (
<select <Dropdown
className="rt-preset-select" triggerClassName="rt-preset-select"
defaultValue="" value=""
onChange={handlePreset} onChange={handlePreset}
title={t("rt.preset")} title={t("rt.preset")}
> options={[
<option value="" disabled> { value: "", label: `${t("rt.preset")}`, disabled: true },
{t("rt.preset")} ...effectivePresets.map((p) => ({ value: p.id, label: p.label })),
</option> ]}
{effectivePresets.map((p) => ( />
<option key={p.id} value={p.id}>
{p.label}
</option>
))}
</select>
)} )}
</div> </div>
+9 -11
View File
@@ -25,6 +25,7 @@ import {
distinctLayers, distinctLayers,
} from "../io/dxfToDrawings"; } from "../io/dxfToDrawings";
import type { CategoryMode } from "../io/dxfToDrawings"; import type { CategoryMode } from "../io/dxfToDrawings";
import { Dropdown } from "./Dropdown";
/** Strukturierte Import-Entscheidung, die der Dialog beim Bestätigen liefert. */ /** Strukturierte Import-Entscheidung, die der Dialog beim Bestätigen liefert. */
export interface ImportDecision { export interface ImportDecision {
@@ -226,18 +227,15 @@ export function ImportDialog({
<span>{t("import.target.existing")}</span> <span>{t("import.target.existing")}</span>
</label> </label>
{targetKind === "existing" && ( {targetKind === "existing" && (
<select <Dropdown
className="imp-select"
value={existingLevelId} value={existingLevelId}
onChange={(e) => setExistingLevelId(e.target.value)} onChange={(v) => setExistingLevelId(v)}
aria-label={t("import.target.existing")} title={t("import.target.existing")}
> options={project.drawingLevels.map((z) => ({
{project.drawingLevels.map((z) => ( value: z.id,
<option key={z.id} value={z.id}> label: `${z.name} · ${t(kindBadgeKey(z.kind))}`,
{z.name} · {t(kindBadgeKey(z.kind))} }))}
</option> />
))}
</select>
)} )}
<label className="imp-radio"> <label className="imp-radio">
<input <input
+21 -30
View File
@@ -43,6 +43,7 @@ import {
type AmbientResolution, type AmbientResolution,
} from "../materials/ambientcg"; } from "../materials/ambientcg";
import { requestMaterialPreview, cancelMaterialPreview } from "../materials/spherePreview"; import { requestMaterialPreview, cancelMaterialPreview } from "../materials/spherePreview";
import { Dropdown } from "./Dropdown";
import { EyeIcon } from "./EyeIcon"; import { EyeIcon } from "./EyeIcon";
import { HatchSwatch, LineSwatch, WallTypeSwatch } from "./hatchPreview"; import { HatchSwatch, LineSwatch, WallTypeSwatch } from "./hatchPreview";
import type { SwatchLayer } from "./hatchPreview"; import type { SwatchLayer } from "./hatchPreview";
@@ -154,7 +155,7 @@ function ColorField({
); );
} }
/** Pill-Dropdown (erbt das globale select-Styling). */ /** Pill-Dropdown (nutzt die gemeinsame Dropdown-Komponente statt <select>). */
function SelectField<T extends string>({ function SelectField<T extends string>({
value, value,
onChange, onChange,
@@ -165,17 +166,11 @@ function SelectField<T extends string>({
options: { value: T; label: string }[]; options: { value: T; label: string }[];
}) { }) {
return ( return (
<select <Dropdown
className="res-select"
value={value} value={value}
onChange={(e) => onChange(e.target.value as T)} onChange={(v) => onChange(v as T)}
> options={options}
{options.map((o) => ( />
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
); );
} }
@@ -623,28 +618,24 @@ function AmbientBrowser({
<button className="res-add" onClick={submitSearch}> <button className="res-add" onClick={submitSearch}>
{t("material.browse.search")} {t("material.browse.search")}
</button> </button>
<select <Dropdown
className="res-select"
value={category} value={category}
onChange={(e) => setCategory(e.target.value)} onChange={(v) => setCategory(v)}
> options={[
<option value="">{t("material.browse.category.all")}</option> { value: "", label: t("material.browse.category.all") },
{AMBIENT_CATEGORIES.map((c) => ( ...AMBIENT_CATEGORIES.map((c) => ({ value: c, label: c })),
<option key={c} value={c}> ]}
{c} />
</option> <Dropdown
))}
</select>
<select
className="res-select"
value={resolution} value={resolution}
title={t("material.browse.resolution")} title={t("material.browse.resolution")}
onChange={(e) => setResolution(e.target.value as AmbientResolution)} onChange={(v) => setResolution(v as AmbientResolution)}
> options={[
<option value="1K">1K</option> { value: "1K", label: "1K" },
<option value="2K">2K</option> { value: "2K", label: "2K" },
<option value="4K">4K</option> { value: "4K", label: "4K" },
</select> ]}
/>
</div> </div>
{error && <div className="mat-browse-error">{error}</div>} {error && <div className="mat-browse-error">{error}</div>}