Skip to content

Commit afe983c

Browse files
committed
Adding dark/ligth theme
- change sigma settings on theme switch - adding "data-bs-theme" attribut on html - save the theme in user preference - change btn-outline-danger to just btn-danger for visibility in dark mode
1 parent b9c421a commit afe983c

36 files changed

+1058
-87
lines changed

package-lock.json

Lines changed: 713 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
"seedrandom": "^3.0.5",
9797
"typescript": "^5.4.5",
9898
"vite": "^5.2.13",
99+
"vite-plugin-svgr": "^4.2.0",
99100
"vitest": "^1.6.0"
100101
},
101102
"overrides": {

public/gephi-logo.svg renamed to src/assets/gephi-logo.svg

Lines changed: 7 additions & 5 deletions
Loading

src/components/GraphSearch.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { ItemType } from "../core/types";
1010
import { EdgeComponentById } from "./Edge";
1111
import { NodeComponentById } from "./Node";
1212
import { SearchIcon } from "./common-icons";
13+
import { DEFAULT_SELECT_PROPS } from "./consts";
1314

1415
export interface OptionItem {
1516
id: string;
@@ -28,14 +29,13 @@ const OptionComponent = ({ data, innerProps, className, isFocused }: OptionProps
2829
const { t } = useTranslation();
2930

3031
return (
31-
<div {...innerProps} className={className} onMouseMove={undefined} onMouseOver={undefined}>
32-
<div
33-
className={cx(
34-
className,
35-
"d-flex m-1 hoverable text-ellipsis d-flex align-items-center",
36-
isFocused && "bg-light",
37-
)}
38-
>
32+
<div
33+
{...innerProps}
34+
className={cx(className, isFocused && "selected")}
35+
onMouseMove={undefined}
36+
onMouseOver={undefined}
37+
>
38+
<div className={cx(className, "d-flex m-1 hoverable text-ellipsis d-flex align-items-center")}>
3939
{data.type === "nodes" && <NodeComponentById id={data.id} />}
4040
{data.type === "edges" && <EdgeComponentById id={data.id} />}
4141
{data.type === "message" && (
@@ -112,6 +112,7 @@ export const GraphSearch: FC<GraphSearchProps> = ({ className, onChange, postPro
112112

113113
return (
114114
<AsyncSelect<Option>
115+
{...DEFAULT_SELECT_PROPS}
115116
className={className}
116117
isClearable
117118
controlShouldRenderValue={!!value}

src/components/ThemeSwitcher.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { FC } from "react";
2+
import { MdContrast, MdDarkMode, MdLightMode } from "react-icons/md";
3+
4+
import { usePreferences, usePreferencesActions } from "../core/context/dataContexts";
5+
import { Preferences } from "../core/preferences/types";
6+
import Tooltip from "./Tooltip";
7+
8+
export const ThemeSwicther: FC<unknown> = () => {
9+
const { theme } = usePreferences();
10+
const { changeTheme } = usePreferencesActions();
11+
return (
12+
<Tooltip closeOnClickContent attachment="top middle" targetAttachment="bottom middle">
13+
<button className="btn p-0 fs-4">
14+
{theme === "auto" && <MdContrast />}
15+
{theme === "light" && <MdLightMode />}
16+
{theme === "dark" && <MdDarkMode />}
17+
</button>
18+
<div className="dropdown-menu show over-modal position-relative">
19+
{(["auto", "light", "dark"] as Preferences["theme"][]).map((theme) => (
20+
<button className="dropdown-item" onClick={() => changeTheme(theme)}>
21+
{theme === "auto" && (
22+
<span>
23+
<MdContrast /> Auto
24+
</span>
25+
)}
26+
{theme === "light" && (
27+
<span>
28+
<MdLightMode /> Light
29+
</span>
30+
)}
31+
{theme === "dark" && (
32+
<span>
33+
<MdDarkMode /> Dark
34+
</span>
35+
)}
36+
</button>
37+
))}
38+
</div>
39+
</Tooltip>
40+
);
41+
};

src/components/forms/GraphModelForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ const FieldModelsComponent: FC<{ fields: FieldModelWithStats[] }> = ({ fields })
6464
</div>
6565
<button
6666
type="button"
67-
className="btn btn-outline-danger btn-sm"
67+
className="btn btn-danger btn-sm"
6868
title={`${t(`edition.delete_${field.itemType}_attributes`, { name: field.id })}`}
6969
onClick={() => {
7070
openModal({

src/core/preferences/index.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { RemoteFile } from "../graph/import/types";
44
import { atom } from "../utils/atoms";
55
import { Producer, producerToAction } from "../utils/producers";
66
import { Preferences } from "./types";
7-
import { getCurrentPreferences, serializePreferences } from "./utils";
7+
import { getAppliedTheme, getCurrentPreferences, serializePreferences } from "./utils";
88

99
/**
1010
* Producers:
@@ -25,6 +25,13 @@ const changeLocale: Producer<Preferences, [string]> = (locale) => {
2525
});
2626
};
2727

28+
const changeTheme: Producer<Preferences, [Preferences["theme"]]> = (theme) => {
29+
return (preferences) => ({
30+
...preferences,
31+
theme,
32+
});
33+
};
34+
2835
/**
2936
* Public API:
3037
* ***********
@@ -34,12 +41,18 @@ export const preferencesAtom = atom<Preferences>(getCurrentPreferences());
3441
export const preferencesActions = {
3542
addRemoteFile: producerToAction(addRemoteFile, preferencesAtom),
3643
changeLocale: producerToAction(changeLocale, preferencesAtom),
44+
changeTheme: producerToAction(changeTheme, preferencesAtom),
3745
};
3846

3947
/**
4048
* Bindings:
4149
* *********
4250
*/
43-
preferencesAtom.bind((preferences) => {
51+
preferencesAtom.bind((preferences, prevPreferences) => {
4452
localStorage.setItem("preferences", serializePreferences(preferences));
53+
54+
// Apply theme change
55+
if (prevPreferences.theme !== preferences.theme || !document.documentElement.getAttribute("data-bs-theme")) {
56+
document.documentElement.setAttribute("data-bs-theme", getAppliedTheme(preferences.theme));
57+
}
4558
});

src/core/preferences/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@ export interface Preferences {
1313
};
1414
// current locale
1515
locale: string;
16+
// theme
17+
theme: "light" | "dark" | "auto";
1618
}

src/core/preferences/utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export function getEmptyPreferences(): Preferences {
99
metrics: {},
1010
// default is the local detected by i18n
1111
locale: i18n.language,
12+
theme: "auto",
1213
};
1314
}
1415

@@ -38,3 +39,11 @@ export function parsePreferences(rawPreferences: string): Preferences | null {
3839
return null;
3940
}
4041
}
42+
43+
export function getAppliedTheme(theme: Preferences["theme"]): "light" | "dark" {
44+
if (theme === "auto") {
45+
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) return "dark";
46+
else return "light";
47+
}
48+
return theme;
49+
}

src/core/sigma/utils.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import { Attributes } from "graphology-types";
2+
import { drawDiscNodeLabel } from "sigma/rendering";
3+
import { Settings } from "sigma/settings";
4+
import { NodeDisplayData, PartialButFor } from "sigma/types";
5+
16
import { SigmaState } from "./types";
27

38
/**
@@ -12,3 +17,59 @@ export function getEmptySigmaState(): SigmaState {
1217
highlightedNodes: null,
1318
};
1419
}
20+
21+
export function drawDiscNodeHover<
22+
N extends Attributes = Attributes,
23+
E extends Attributes = Attributes,
24+
G extends Attributes = Attributes,
25+
>(
26+
context: CanvasRenderingContext2D,
27+
data: PartialButFor<NodeDisplayData, "x" | "y" | "size" | "label" | "color">,
28+
settings: Settings<N, E, G>,
29+
): void {
30+
const size = settings.labelSize,
31+
font = settings.labelFont,
32+
weight = settings.labelWeight;
33+
34+
context.font = `${weight} ${size}px ${font}`;
35+
36+
// Then we draw the label background
37+
context.fillStyle = (settings as Settings & { nodeHoverBackgoundColor?: string }).nodeHoverBackgoundColor || "#FFF";
38+
context.shadowOffsetX = 0;
39+
context.shadowOffsetY = 0;
40+
context.shadowBlur = 8;
41+
context.shadowColor = "#000";
42+
43+
const PADDING = 2;
44+
45+
if (typeof data.label === "string") {
46+
const textWidth = context.measureText(data.label).width,
47+
boxWidth = Math.round(textWidth + 5),
48+
boxHeight = Math.round(size + 2 * PADDING),
49+
radius = Math.max(data.size, size / 2) + PADDING;
50+
51+
const angleRadian = Math.asin(boxHeight / 2 / radius);
52+
const xDeltaCoord = Math.sqrt(Math.abs(Math.pow(radius, 2) - Math.pow(boxHeight / 2, 2)));
53+
54+
context.beginPath();
55+
context.moveTo(data.x + xDeltaCoord, data.y + boxHeight / 2);
56+
context.lineTo(data.x + radius + boxWidth, data.y + boxHeight / 2);
57+
context.lineTo(data.x + radius + boxWidth, data.y - boxHeight / 2);
58+
context.lineTo(data.x + xDeltaCoord, data.y - boxHeight / 2);
59+
context.arc(data.x, data.y, radius, angleRadian, -angleRadian);
60+
context.closePath();
61+
context.fill();
62+
} else {
63+
context.beginPath();
64+
context.arc(data.x, data.y, data.size + PADDING, 0, Math.PI * 2);
65+
context.closePath();
66+
context.fill();
67+
}
68+
69+
context.shadowOffsetX = 0;
70+
context.shadowOffsetY = 0;
71+
context.shadowBlur = 0;
72+
73+
// And finally we draw the label
74+
drawDiscNodeLabel(context, data, settings);
75+
}

src/locales/dev.json

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -220,10 +220,10 @@
220220
"confirm_delete_edges_one": "Are you sure you want to delete this one edge?",
221221
"confirm_delete_edges": "Are you sure you want to delete {{count}} edges?",
222222
"search_nodes": "Search on node labels...",
223-
"delete_nodes_attributes": "Delete the {{name}} node attribute",
224-
"delete_edges_attributes": "Delete the {{name}} edge attribute",
223+
"delete_nodes_attributes": "Delete the '{{name}}' node attribute",
224+
"delete_edges_attributes": "Delete the '{{name}}' edge attribute",
225225
"confirm_delete_attributes": "This deletion will remove {{nbValues}} values from the graph. Are you sure you want to delete the attribute {{name}}?",
226-
"delete_attributes_success": "The {{name}} attribute has been deleted."
226+
"delete_attributes_success": "The '{{name}'} attribute has been deleted."
227227
},
228228
"graph": {
229229
"title": "Graph",
@@ -740,13 +740,15 @@
740740
"placeholder": "Search a node ...",
741741
"no_result": "No node found",
742742
"help": "Type something to find a node",
743-
"other_result": "... and {{count}} other nodes"
743+
"other_result": "... and {{count}} other nodes",
744+
"select_all": "Select all {{count}} nodes"
744745
},
745746
"edges": {
746747
"placeholder": "Search an edge...",
747748
"no_result": "No edge found",
748749
"help": "Type something to find an edge",
749-
"other_result": "... and {{count}} other edges"
750+
"other_result": "... and {{count}} other edges",
751+
"select_all": "Select all {{count}} edges"
750752
}
751753
},
752754
"settings": {
@@ -758,6 +760,7 @@
758760
"description": "All your data will be deleted, even the settings saved locally (ie. in your browser). Are you sure ?",
759761
"success": "Application's state has been cleared"
760762
}
761-
}
763+
},
764+
"theme": "Choose a theme"
762765
}
763766
}

src/styles/_base.scss

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,21 @@
114114
--bs-btn-font-size: 0.85rem;
115115
}
116116
}
117+
118+
.btn-outline-dark {
119+
color: var(--bs-body-color);
120+
border-color: var(--bs-body-color);
121+
122+
&:hover {
123+
color: var(--bs-body-bg);
124+
background-color: var(--bs-body-color);
125+
}
126+
}
127+
128+
.nav {
129+
.nav-item {
130+
.nav-link.link-dark {
131+
color: var(--bs-btn-hover-color)!important;
132+
}
133+
}
134+
}

src/styles/_dark.scss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
@include color-mode(dark) {
2+
--toolbar-bg: #{$black};
3+
--panels-bg: #{$gray-700};
4+
--panels-bg-rgb: 73,80,87;
5+
--range-in-bg: #{$gray-800};
6+
--range-in-body: #{$gray-800};
7+
--range-out-bg: #{$gray-200};
8+
--range-out-body: #{$gray-200};
9+
}
10+

src/styles/_filters.scss

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@
6363
}
6464

6565
.global {
66-
background: $gray-400;
66+
background: var(--range-in-bg);
6767
}
6868
.filtered {
69-
background: $gray-800;
69+
background: var(--range-out-bg);
7070
}
7171
.label {
7272
position: absolute;
@@ -76,11 +76,11 @@
7676

7777
&.inside {
7878
top: 0;
79-
color: $gray-100;
79+
color: var(--range-in-body);
8080
}
8181
&.outside {
8282
bottom: 100%;
83-
color: $gray-800;
83+
color: var(--range-out-body);
8484
}
8585
}
8686
}

src/styles/_graph-caption.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
.graph-caption {
22
&:not(.collapsed) {
3-
background-color: rgba($panels-bg, 0.9);
3+
background-color: rgba(var(--panels-bg-rgb), 0.9);
44
}
55
display: flex;
66
align-items: center;

0 commit comments

Comments
 (0)