feat(color): add hex code display and click-to-copy functionality
Some checks failed
CI / style (push) Has been cancelled
CI / build-client (push) Has been cancelled
CI / build-amd64 (push) Has been cancelled
CI / build-tester (push) Has been cancelled
CI / test (cockroachdb) (push) Has been cancelled
CI / test (mariadb) (push) Has been cancelled
CI / test (postgres) (push) Has been cancelled
CI / test (sqlite) (push) Has been cancelled
CI / build-arm64 (push) Has been cancelled
CI / build-armv7 (push) Has been cancelled
CI / publish-images (push) Has been cancelled
CI / publish-release (push) Has been cancelled

Enhances SpoolIcon component with optional showHex prop:
- Displays hex color code next to swatch when enabled
- Click to copy hex code to clipboard
- Shows tooltip with copy hint
- Supports both single and multi-color filaments

Enabled on detail pages:
- Filament show page
- Spool show page

Added translation keys for copy feedback messages.

Closes #33

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-15 23:33:21 -06:00
parent 48bd516c0f
commit 267fadfc54
4 changed files with 53 additions and 11 deletions

View File

@@ -52,6 +52,11 @@
"validationError": "Validation error: {{error}}" "validationError": "Validation error: {{error}}"
}, },
"kofi": "Tip me on Ko-fi", "kofi": "Tip me on Ko-fi",
"color": {
"click_to_copy": "Click to copy",
"copied": "Copied {{color}} to clipboard",
"copy_failed": "Failed to copy to clipboard"
},
"loading": "Loading", "loading": "Loading",
"version": "Version", "version": "Version",
"unknown": "Unknown", "unknown": "Unknown",

View File

@@ -1,17 +1,22 @@
import { CopyOutlined } from "@ant-design/icons";
import { message, Tooltip } from "antd";
import { useTranslate } from "@refinedev/core";
import "./spoolIcon.css"; import "./spoolIcon.css";
interface Props { interface Props {
color: string | { colors: string[]; vertical: boolean }; color: string | { colors: string[]; vertical: boolean };
size?: "small" | "large"; size?: "small" | "large";
no_margin? : boolean no_margin?: boolean;
showHex?: boolean;
} }
export default function SpoolIcon(props: Readonly<Props>) { export default function SpoolIcon(props: Readonly<Props>) {
const t = useTranslate();
let dirClass = "vertical"; let dirClass = "vertical";
let cols = []; let cols: string[] = [];
let size = props.size ? props.size : "small"; const size = props.size ? props.size : "small";
let no_margin = props.no_margin ? "no-margin" : ""; const no_margin = props.no_margin ? "no-margin" : "";
if (typeof props.color === "string") { if (typeof props.color === "string") {
cols = [props.color]; cols = [props.color];
} else { } else {
@@ -19,16 +24,49 @@ export default function SpoolIcon(props: Readonly<Props>) {
cols = props.color.colors; cols = props.color.colors;
} }
return ( // Normalize hex codes (remove # if present, uppercase)
const normalizedCols = cols.map((col) => col.replace("#", "").toUpperCase());
const hexDisplay = normalizedCols.length === 1 ? `#${normalizedCols[0]}` : normalizedCols.map((c) => `#${c}`).join(", ");
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(hexDisplay);
message.success(t("color.copied", { color: hexDisplay }));
} catch {
message.error(t("color.copy_failed"));
}
};
const swatchContent = (
<div className={"spool-icon " + dirClass + " " + size + " " + no_margin}> <div className={"spool-icon " + dirClass + " " + size + " " + no_margin}>
{cols.map((col) => ( {normalizedCols.map((col) => (
<div <div
key={col} key={col}
style={{ style={{
backgroundColor: "#" + col.replace("#", ""), backgroundColor: "#" + col,
}} }}
></div> ></div>
))} ))}
</div> </div>
); );
if (props.showHex) {
return (
<Tooltip title={`${t("color.click_to_copy")}: ${hexDisplay}`}>
<div
className="spool-icon-with-hex"
onClick={copyToClipboard}
style={{ cursor: "pointer", display: "inline-flex", alignItems: "center", gap: 8 }}
>
{swatchContent}
<span className="hex-code" style={{ fontFamily: "monospace", fontSize: "0.85em", opacity: 0.8 }}>
{hexDisplay}
<CopyOutlined style={{ marginLeft: 4, fontSize: "0.9em" }} />
</span>
</div>
</Tooltip>
);
}
return swatchContent;
} }

View File

@@ -88,8 +88,7 @@ export const FilamentShow: React.FC<IResourceComponentsProps> = () => {
<Title level={5}>{t("filament.fields.name")}</Title> <Title level={5}>{t("filament.fields.name")}</Title>
<TextField value={record?.name} /> <TextField value={record?.name} />
<Title level={5}>{t("filament.fields.color_hex")}</Title> <Title level={5}>{t("filament.fields.color_hex")}</Title>
{colorObj && <SpoolIcon color={colorObj} size="large" no_margin />} {colorObj && <SpoolIcon color={colorObj} size="large" no_margin showHex />}
{record?.color_hex && <TextField value={`#${record?.color_hex}`} />}
<Title level={5}>{t("filament.fields.material")}</Title> <Title level={5}>{t("filament.fields.material")}</Title>
<TextField value={record?.material} /> <TextField value={record?.material} />
<Title level={5}>{t("filament.fields.price")}</Title> <Title level={5}>{t("filament.fields.price")}</Title>

View File

@@ -163,7 +163,7 @@ export const SpoolShow: React.FC<IResourceComponentsProps> = () => {
<Title level={5}>{t("spool.fields.id")}</Title> <Title level={5}>{t("spool.fields.id")}</Title>
<NumberField value={record?.id ?? ""} /> <NumberField value={record?.id ?? ""} />
<Title level={5}>{t("spool.fields.filament")}</Title> <Title level={5}>{t("spool.fields.filament")}</Title>
{colorObj && <SpoolIcon color={colorObj} size="large" no_margin />} {colorObj && <SpoolIcon color={colorObj} size="large" no_margin showHex />}
<TextField value={record ? filamentURL(record?.filament) : ""} /> <TextField value={record ? filamentURL(record?.filament) : ""} />
<Title level={5}>{t("spool.fields.price")}</Title> <Title level={5}>{t("spool.fields.price")}</Title>
<TextField value={spoolPrice(record)} /> <TextField value={spoolPrice(record)} />