Skip to content

Commit 4dadfc5

Browse files
authored
[ENG-1036] Fix the relation color not picking by forcing colors to strictly match Tldraw palette (#557)
* relation type color picking * fix a dumb dumb bug * address PR comments * fix type check
1 parent 598b866 commit 4dadfc5

7 files changed

Lines changed: 181 additions & 23 deletions

File tree

apps/obsidian/src/components/RelationshipTypeSettings.tsx

Lines changed: 97 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,94 @@
1-
import { useState } from "react";
1+
import { useState, useRef, useEffect } from "react";
22
import { DiscourseRelationType } from "~/types";
33
import { Notice } from "obsidian";
44
import { usePlugin } from "./PluginContext";
55
import generateUid from "~/utils/generateUid";
66
import { ConfirmationModal } from "./ConfirmationModal";
7+
import {
8+
TLDRAW_COLOR_NAMES,
9+
TLDRAW_COLOR_LABELS,
10+
DEFAULT_TLDRAW_COLOR,
11+
COLOR_PALETTE,
12+
type TldrawColorName,
13+
} from "~/utils/tldrawColors";
14+
import { getContrastColor } from "~/utils/colorUtils";
15+
16+
type ColorPickerProps = {
17+
value: string;
18+
onChange: (color: TldrawColorName) => void;
19+
};
20+
21+
const ColorPicker = ({ value, onChange }: ColorPickerProps) => {
22+
const [isOpen, setIsOpen] = useState(false);
23+
const dropdownRef = useRef<HTMLDivElement>(null);
24+
25+
useEffect(() => {
26+
const handleClickOutside = (event: MouseEvent) => {
27+
if (
28+
dropdownRef.current &&
29+
!dropdownRef.current.contains(event.target as Node)
30+
) {
31+
setIsOpen(false);
32+
}
33+
};
34+
35+
if (isOpen) {
36+
document.addEventListener("mousedown", handleClickOutside);
37+
return () => {
38+
document.removeEventListener("mousedown", handleClickOutside);
39+
};
40+
}
41+
}, [isOpen]);
42+
43+
const currentColor = value as TldrawColorName;
44+
const bgColor = COLOR_PALETTE[currentColor] ?? COLOR_PALETTE.black;
45+
const textColor = getContrastColor(bgColor ?? DEFAULT_TLDRAW_COLOR);
46+
47+
return (
48+
<div ref={dropdownRef} className="relative min-w-32">
49+
<button
50+
type="button"
51+
onClick={() => setIsOpen(!isOpen)}
52+
className="flex w-full items-center justify-between rounded border px-3 py-2 text-left"
53+
style={{ backgroundColor: bgColor, color: textColor }}
54+
>
55+
<span className="flex items-center gap-2">
56+
<span
57+
className="inline-block h-3 w-3 rounded-full border-2 border-solid"
58+
style={{ backgroundColor: bgColor, border: `${textColor}` }}
59+
/>
60+
{TLDRAW_COLOR_LABELS[currentColor]}
61+
</span>
62+
<span className="text-sm">{isOpen ? "▲" : "▼"}</span>
63+
</button>
64+
65+
{isOpen && (
66+
<div className="absolute z-50 mt-1 max-h-40 w-full overflow-y-auto">
67+
{TLDRAW_COLOR_NAMES.map((colorName) => {
68+
const bgColor = COLOR_PALETTE[colorName] ?? COLOR_PALETTE.black;
69+
return (
70+
<button
71+
key={colorName}
72+
type="button"
73+
onClick={() => {
74+
onChange(colorName);
75+
setIsOpen(false);
76+
}}
77+
className="flex w-full flex-row justify-start gap-2 rounded-none px-3 py-2"
78+
>
79+
<span
80+
className="inline-block h-3 w-3 rounded-full"
81+
style={{ backgroundColor: bgColor }}
82+
/>
83+
{TLDRAW_COLOR_LABELS[colorName]}
84+
</button>
85+
);
86+
})}
87+
</div>
88+
)}
89+
</div>
90+
);
91+
};
792

893
const RelationshipTypeSettings = () => {
994
const plugin = usePlugin();
@@ -24,11 +109,14 @@ const RelationshipTypeSettings = () => {
24109
id: newId,
25110
label: "",
26111
complement: "",
27-
color: "#000000",
112+
color: DEFAULT_TLDRAW_COLOR,
28113
};
29114
}
30-
31-
updatedRelationTypes[index][field] = value;
115+
if (field === "color") {
116+
updatedRelationTypes[index].color = value as TldrawColorName;
117+
} else {
118+
updatedRelationTypes[index][field] = value;
119+
}
32120
setRelationTypes(updatedRelationTypes);
33121
setHasUnsavedChanges(true);
34122
};
@@ -42,7 +130,7 @@ const RelationshipTypeSettings = () => {
42130
id: newId,
43131
label: "",
44132
complement: "",
45-
color: "#000000",
133+
color: DEFAULT_TLDRAW_COLOR,
46134
},
47135
];
48136
setRelationTypes(updatedRelationTypes);
@@ -53,7 +141,7 @@ const RelationshipTypeSettings = () => {
53141
const relationType = relationTypes[index] || {
54142
label: "Unnamed",
55143
complement: "",
56-
color: "#000000",
144+
color: DEFAULT_TLDRAW_COLOR,
57145
};
58146
const modal = new ConfirmationModal(plugin.app, {
59147
title: "Delete Relation Type",
@@ -132,14 +220,11 @@ const RelationshipTypeSettings = () => {
132220
}
133221
className="flex-1"
134222
/>
135-
<input
136-
type="color"
223+
<ColorPicker
137224
value={relationType.color}
138-
onChange={(e) =>
139-
handleRelationTypeChange(index, "color", e.target.value)
225+
onChange={(color) =>
226+
handleRelationTypeChange(index, "color", color)
140227
}
141-
className="w-12 h-8 rounded border"
142-
title="Relation color"
143228
/>
144229
<button
145230
onClick={() => confirmDeleteRelationType(index)}

apps/obsidian/src/components/canvas/DiscourseRelationTool.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getRelationTypeById } from "~/utils/typeUtils";
66
import { DiscourseRelationShape } from "./shapes/DiscourseRelationShape";
77
import { getNodeTypeById } from "~/utils/typeUtils";
88
import { showToast } from "./utils/toastUtils";
9+
import { DEFAULT_TLDRAW_COLOR } from "~/utils/tldrawColors";
910

1011
type RelationToolContext = {
1112
plugin: DiscourseGraphPlugin;
@@ -250,6 +251,7 @@ class Pointing extends StateNode {
250251
props: {
251252
relationTypeId: relationToolContext.relationTypeId,
252253
text: relationType?.label ?? "",
254+
color: relationType?.color ?? DEFAULT_TLDRAW_COLOR,
253255
scale: this.editor.user.getIsDynamicResizeMode()
254256
? 1 / this.editor.getZoomLevel()
255257
: 1,

apps/obsidian/src/components/canvas/overlays/RelationPanel.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { getFrontmatterForFile } from "~/components/canvas/shapes/discourseNodeShapeUtils";
1717
import { getRelationTypeById } from "~/utils/typeUtils";
1818
import { showToast } from "~/components/canvas/utils/toastUtils";
19+
import { DEFAULT_TLDRAW_COLOR } from "~/utils/tldrawColors";
1920

2021
type GroupedRelation = {
2122
key: string;
@@ -379,7 +380,7 @@ export const RelationsPanel = ({
379380
dash: "draw",
380381
size: "m",
381382
fill: "none",
382-
color: "black",
383+
color: relationType?.color ?? DEFAULT_TLDRAW_COLOR,
383384
labelColor: "black",
384385
bend: 0,
385386
// Will be updated by bindings

apps/obsidian/src/components/canvas/utils/relationUtils.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import {
6767
TLDefaultSizeStyle,
6868
PI2,
6969
TLArcInfo,
70+
TLDefaultColorThemeColor,
7071
} from "tldraw";
7172
import type {
7273
RelationBindings,
@@ -77,6 +78,7 @@ import {
7778
DiscourseRelationShape,
7879
DiscourseRelationUtil,
7980
} from "~/components/canvas/shapes/DiscourseRelationShape";
81+
import { DEFAULT_TLDRAW_COLOR } from "~/utils/tldrawColors";
8082

8183
let defaultPixels: { white: string; black: string } | null = null;
8284
let globalRenderIndex = 0;
@@ -1872,11 +1874,18 @@ export const ArrowSvg = track(function ArrowSvg({
18721874
// color: string;
18731875
}) {
18741876
const editor = useEditor();
1875-
// const theme = useDefaultColorTheme();
1877+
const theme = useDefaultColorTheme();
18761878
const info = getArrowInfo(editor, shape);
18771879
const bounds = Box.ZeroFix(editor.getShapeGeometry(shape).bounds);
18781880
const bindings = getArrowBindings(editor, shape);
18791881

1882+
// Ensure color is a valid tldraw color name, fallback to black if not
1883+
const colorName =
1884+
shape.props.color && theme[shape.props.color as keyof typeof theme]
1885+
? (shape.props.color as keyof typeof theme)
1886+
: DEFAULT_TLDRAW_COLOR;
1887+
const colorHex = (theme[colorName] as TLDefaultColorThemeColor).solid;
1888+
18801889
const changeIndex = React.useMemo<number>(() => {
18811890
return editor.environment.isSafari ? (globalRenderIndex += 1) : 0;
18821891
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -2006,7 +2015,7 @@ export const ArrowSvg = track(function ArrowSvg({
20062015
</defs>
20072016
<g
20082017
fill="none"
2009-
stroke={shape.props.color}
2018+
stroke={colorHex}
20102019
strokeWidth={strokeWidth}
20112020
strokeLinejoin="round"
20122021
strokeLinecap="round"
@@ -2030,18 +2039,18 @@ export const ArrowSvg = track(function ArrowSvg({
20302039
</g>
20312040
{as && maskStartArrowhead && shape.props.fill !== "none" && (
20322041
<ShapeFill
2033-
// theme={theme}
2042+
theme={theme}
20342043
d={as}
2035-
color={shape.props.color}
2044+
color={colorHex}
20362045
fill={shape.props.fill}
20372046
scale={shape.props.scale}
20382047
/>
20392048
)}
20402049
{ae && maskEndArrowhead && shape.props.fill !== "none" && (
20412050
<ShapeFill
2042-
// theme={theme}
2051+
theme={theme}
20432052
d={ae}
2044-
color={shape.props.color}
2053+
color={colorHex}
20452054
fill={shape.props.fill}
20462055
scale={shape.props.scale}
20472056
/>

apps/obsidian/src/constants.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,19 @@ export const DEFAULT_RELATION_TYPES: Record<string, DiscourseRelationType> = {
2929
id: generateUid("relation"),
3030
label: "supports",
3131
complement: "is supported by",
32-
color: "#099268",
32+
color: "green",
3333
},
3434
opposes: {
3535
id: generateUid("relation"),
3636
label: "opposes",
3737
complement: "is opposed by",
38-
color: "#e03131",
38+
color: "red",
3939
},
4040
informs: {
4141
id: generateUid("relation"),
4242
label: "informs",
4343
complement: "is informed by",
44-
color: "#adb5bd",
44+
color: "grey",
4545
},
4646
};
4747

apps/obsidian/src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { TFile } from "obsidian";
2+
import { TldrawColorName } from "./utils/tldrawColors";
23

34
export type DiscourseNode = {
45
id: string;
@@ -16,7 +17,7 @@ export type DiscourseRelationType = {
1617
id: string;
1718
label: string;
1819
complement: string;
19-
color: string;
20+
color: TldrawColorName;
2021
};
2122

2223
export type DiscourseRelation = {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/* eslint-disable @typescript-eslint/naming-convention */
2+
/**
3+
* Tldraw color names that can be used for relation types.
4+
* These match the defaultColorNames from tldraw's TLColorStyle.
5+
*/
6+
export const TLDRAW_COLOR_NAMES = [
7+
"black",
8+
"grey",
9+
"light-violet",
10+
"violet",
11+
"blue",
12+
"light-blue",
13+
"yellow",
14+
"orange",
15+
"green",
16+
"light-green",
17+
"light-red",
18+
"red",
19+
"white",
20+
] as const;
21+
22+
export type TldrawColorName = (typeof TLDRAW_COLOR_NAMES)[number];
23+
24+
/**
25+
* Human-readable labels for tldraw color names
26+
*/
27+
export const TLDRAW_COLOR_LABELS: Record<TldrawColorName, string> = {
28+
black: "Black",
29+
grey: "Grey",
30+
"light-violet": "Light Violet",
31+
violet: "Violet",
32+
blue: "Blue",
33+
"light-blue": "Light Blue",
34+
yellow: "Yellow",
35+
orange: "Orange",
36+
green: "Green",
37+
"light-green": "Light Green",
38+
"light-red": "Light Red",
39+
red: "Red",
40+
white: "White",
41+
};
42+
43+
export const DEFAULT_TLDRAW_COLOR: TldrawColorName = "black";
44+
45+
// from @tldraw/editor/editor.css
46+
export const COLOR_PALETTE: Record<string, string> = {
47+
black: "#1d1d1d",
48+
blue: "#4263eb",
49+
green: "#099268",
50+
grey: "#adb5bd",
51+
"light-blue": "#4dabf7",
52+
"light-green": "#40c057",
53+
"light-red": "#ff8787",
54+
"light-violet": "#e599f7",
55+
orange: "#f76707",
56+
red: "#e03131",
57+
violet: "#ae3ec9",
58+
white: "#ffffff",
59+
yellow: "#ffc078",
60+
};

0 commit comments

Comments
 (0)