Farbfelder: Hex-Code separat editierbar, Swatch öffnet die Palette
ColorHexField (neu, geteilt) trennt die zwei Klickziele: Swatch/ natives Farb-Input öffnet weiter den Picker, der Hex-Text daneben ist jetzt ein eigenes Eingabefeld (Enter übernimmt, Esc verwirft, ungültige Eingabe fällt auf den letzten gültigen Wert zurück). Ersetzt die bisherige reine Text-Anzeige in SettingsDialog + AttributesPanel.
This commit is contained in:
@@ -29,6 +29,7 @@ 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";
|
import { Dropdown } from "../ui/Dropdown";
|
||||||
|
import { ColorHexField } from "../ui/ColorHexField";
|
||||||
|
|
||||||
/** 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
|
||||||
@@ -95,11 +96,7 @@ export function AttributesPanel() {
|
|||||||
// nur, wenn die Quelle „custom" ist — sonst zeigt allein der Dropdown den
|
// nur, wenn die Quelle „custom" ist — sonst zeigt allein der Dropdown den
|
||||||
// Zustand.
|
// Zustand.
|
||||||
const colorValue = (value: string | undefined, onSet: (color: string) => void) => (
|
const colorValue = (value: string | undefined, onSet: (color: string) => void) => (
|
||||||
<label className="attr-color">
|
<ColorHexField value={value ?? "#808080"} onChange={onSet} />
|
||||||
<span className="attr-color-swatch" style={{ background: value ?? "#808080" }} />
|
|
||||||
<input type="color" value={value ?? "#808080"} onChange={(e) => onSet(e.target.value)} />
|
|
||||||
<span className="attr-color-hex">{value ?? "#808080"}</span>
|
|
||||||
</label>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Eine Attribut-Zeile: Label + Quellen-Dropdown + (nur bei „eigener Wert")
|
// Eine Attribut-Zeile: Label + Quellen-Dropdown + (nur bei „eigener Wert")
|
||||||
@@ -170,18 +167,7 @@ export function AttributesPanel() {
|
|||||||
{/* Farbe — gilt für Wand UND Drawing2D (Kontrakt setzt beide). */}
|
{/* Farbe — gilt für Wand UND Drawing2D (Kontrakt setzt beide). */}
|
||||||
<span className="attr-key">{t("attr.color")}</span>
|
<span className="attr-key">{t("attr.color")}</span>
|
||||||
<span className="attr-val">
|
<span className="attr-val">
|
||||||
<label className="attr-color">
|
<ColorHexField value={sel.color} onChange={host.onSetSelectionColor} />
|
||||||
<span
|
|
||||||
className="attr-color-swatch"
|
|
||||||
style={{ background: sel.color }}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
value={sel.color}
|
|
||||||
onChange={(e) => host.onSetSelectionColor(e.target.value)}
|
|
||||||
/>
|
|
||||||
<span className="attr-color-hex">{sel.color}</span>
|
|
||||||
</label>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Strichstärke (mm) — Wand/Decke/Drawing2D, mit Nach-Ebene/Nach-Bauteil/
|
{/* Strichstärke (mm) — Wand/Decke/Drawing2D, mit Nach-Ebene/Nach-Bauteil/
|
||||||
|
|||||||
@@ -4018,6 +4018,14 @@ body {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
}
|
||||||
|
/* Swatch + natives Farb-Input — EIGENER Klick-Bereich (öffnet die Palette).
|
||||||
|
Der Hex-Text daneben ist jetzt ein separates <input> (ColorHexField),
|
||||||
|
damit er unabhängig editierbar ist statt nur die Palette zu öffnen. */
|
||||||
|
.attr-color-swatch-wrap {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.attr-color-swatch {
|
.attr-color-swatch {
|
||||||
@@ -4041,6 +4049,26 @@ body {
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
/* Editierbarer Hex-Code: sieht wie reiner Text aus, bis man klickt/fokussiert
|
||||||
|
— dann Rahmen + helle Schrift als Editier-Signal. */
|
||||||
|
.attr-color-hex-input {
|
||||||
|
width: 6.5ch;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 1px 3px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
.attr-color-hex-input:hover:not(:disabled) {
|
||||||
|
border-color: var(--border);
|
||||||
|
}
|
||||||
|
.attr-color-hex-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--input);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
.attr-color input[type="color"]:disabled {
|
.attr-color input[type="color"]:disabled {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
// Farbfeld: Swatch (öffnet die native Farbpalette) + separat editierbarer
|
||||||
|
// Hex-Code (Tippen/Enter übernimmt, Esc verwirft). Ersetzt die frühere reine
|
||||||
|
// Text-Anzeige des Hex-Werts, die nicht anklickbar/editierbar war.
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export function ColorHexField({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
onChange: (hex: string) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}) {
|
||||||
|
const [text, setText] = useState(value.toUpperCase());
|
||||||
|
|
||||||
|
// Extern geänderter Wert (z. B. Preset-Klick) synchron halten, solange das
|
||||||
|
// Feld nicht gerade selbst bearbeitet wird (kein Cursor-Sprung beim Tippen).
|
||||||
|
useEffect(() => setText(value.toUpperCase()), [value]);
|
||||||
|
|
||||||
|
const commit = (raw: string) => {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
const withHash = trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
|
||||||
|
if (/^#[0-9a-fA-F]{6}$/.test(withHash)) {
|
||||||
|
onChange(withHash.toLowerCase());
|
||||||
|
setText(withHash.toUpperCase());
|
||||||
|
} else {
|
||||||
|
setText(value.toUpperCase()); // ungültig → letzten gültigen Wert zeigen
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="attr-color">
|
||||||
|
<label className="attr-color-swatch-wrap" title="Farbe wählen">
|
||||||
|
<span className="attr-color-swatch" style={{ background: value }} />
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={value}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="attr-color-hex attr-color-hex-input"
|
||||||
|
value={text}
|
||||||
|
disabled={disabled}
|
||||||
|
spellCheck={false}
|
||||||
|
onChange={(e) => setText(e.target.value)}
|
||||||
|
onBlur={(e) => commit(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
commit(e.currentTarget.value);
|
||||||
|
e.currentTarget.blur();
|
||||||
|
} else if (e.key === "Escape") {
|
||||||
|
setText(value.toUpperCase());
|
||||||
|
e.currentTarget.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { ACCENT_SWATCHES } from "../theme/accents";
|
import { ACCENT_SWATCHES } from "../theme/accents";
|
||||||
import type { Project } from "../model/types";
|
import type { Project } from "../model/types";
|
||||||
|
import { ColorHexField } from "./ColorHexField";
|
||||||
|
|
||||||
export interface SettingsDialogProps {
|
export interface SettingsDialogProps {
|
||||||
marqueeColor: string;
|
marqueeColor: string;
|
||||||
@@ -145,11 +146,7 @@ function AccentColorField({
|
|||||||
<div className="settings-color-field">
|
<div className="settings-color-field">
|
||||||
<div className="settings-color-head">
|
<div className="settings-color-head">
|
||||||
<span className="settings-field-label">{label}</span>
|
<span className="settings-field-label">{label}</span>
|
||||||
<label className="attr-color">
|
<ColorHexField value={value} onChange={onChange} />
|
||||||
<span className="attr-color-swatch" style={{ background: value }} />
|
|
||||||
<input type="color" value={value} onChange={(e) => onChange(e.target.value)} />
|
|
||||||
<span className="attr-color-hex">{value.toUpperCase()}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-accent-presets" role="group" aria-label={label}>
|
<div className="settings-accent-presets" role="group" aria-label={label}>
|
||||||
{ACCENT_SWATCHES.map((a) => (
|
{ACCENT_SWATCHES.map((a) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user