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:
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>}
|
||||||
|
|||||||
Reference in New Issue
Block a user