Skip to content

Commit db967b4

Browse files
committed
feat: add penpot theme option
1 parent 8ea1efd commit db967b4

File tree

5 files changed

+100
-12
lines changed

5 files changed

+100
-12
lines changed

src/base/components/settings.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ <h2>Theme</h2>
1717
<option value="light">Light</option>
1818
<option value="dark">Dark</option>
1919
<option value="system">System</option>
20+
<option value="tab">Penpot</option>
2021
</select>
2122
</div>
2223
</div>

src/base/scripts/electron-tabs.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,23 @@ async function prepareTabReloadButton() {
8585
function tabReadyHandler(tab) {
8686
const webview = /** @type {WebviewTag} */ (tab.webview);
8787

88+
tab.once("webview-dom-ready", () => {
89+
tab.on("active", () => {
90+
webview.send(THEME_TAB_EVENTS.REQUEST_UPDATE);
91+
});
92+
});
8893
tab.element.addEventListener("contextmenu", (event) => {
8994
event.preventDefault();
9095
window.api.send("openTabMenu", tab.id);
9196
});
97+
webview.addEventListener("ipc-message", (event) => {
98+
const isThemeUpdate = event.channel === THEME_TAB_EVENTS.UPDATE;
99+
if (isThemeUpdate) {
100+
const [theme] = event.args;
101+
102+
handleInTabThemeUpdate(theme);
103+
}
104+
});
92105
webview.addEventListener("page-title-updated", () => {
93106
const newTitle = webview.getTitle();
94107
tab.setTitle(newTitle);

src/base/scripts/theme.js

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,47 @@
11
/**
22
* @typedef {Parameters<typeof window.api.setTheme>[0]} ThemeId
3+
* @typedef {ThemeId | "tab"} ThemeSetting
4+
* @typedef {import("electron").IpcMessageEvent} IpcMessageEvent
35
*/
46

57
const THEME_STORE_KEY = "theme";
8+
const THEME_TAB_EVENTS = Object.freeze({
9+
REQUEST_UPDATE: "theme-request-update",
10+
UPDATE: "theme-update",
11+
});
12+
13+
/** @type {ThemeSetting | null} */
14+
let currentThemeSetting = null;
615

716
window.addEventListener("DOMContentLoaded", () => {
8-
const themeId = /** @type {ThemeId | null} */ (
17+
currentThemeSetting = /** @type {ThemeSetting | null} */ (
918
localStorage.getItem(THEME_STORE_KEY)
1019
);
1120

12-
if (themeId) {
13-
setTheme(themeId);
21+
if (isThemeId(currentThemeSetting)) {
22+
setTheme(currentThemeSetting);
1423
}
1524

16-
prepareForm(themeId);
25+
prepareForm(currentThemeSetting);
1726
});
1827

1928
/**
20-
* @param {ThemeId | null} themeId
29+
* @param {ThemeSetting | null} themeSetting
2130
*/
22-
async function prepareForm(themeId) {
31+
async function prepareForm(themeSetting) {
2332
const { themeSelect } = await getThemeSettingsForm();
2433

25-
if (themeSelect && themeId) {
26-
themeSelect.value = themeId;
34+
if (themeSelect && themeSetting) {
35+
themeSelect.value = themeSetting;
2736
}
2837

2938
themeSelect?.addEventListener("change", (event) => {
3039
const { target } = event;
3140
const value = target instanceof HTMLSelectElement && target.value;
3241

42+
localStorage.setItem(THEME_STORE_KEY, value || "");
43+
currentThemeSetting = isThemeSetting(value) ? value : null;
44+
3345
if (isThemeId(value)) {
3446
setTheme(value);
3547
}
@@ -40,7 +52,6 @@ async function prepareForm(themeId) {
4052
* @param {ThemeId} themeId
4153
*/
4254
function setTheme(themeId) {
43-
localStorage.setItem(THEME_STORE_KEY, themeId);
4455
window.api.setTheme(themeId);
4556
}
4657

@@ -54,6 +65,17 @@ async function getThemeSettingsForm() {
5465
return { themeSelect };
5566
}
5667

68+
/**
69+
* @param {string} inTabTheme
70+
*/
71+
function handleInTabThemeUpdate(inTabTheme) {
72+
const shouldUseInTabTheme = currentThemeSetting === "tab";
73+
74+
if (shouldUseInTabTheme && isThemeId(inTabTheme)) {
75+
setTheme(inTabTheme);
76+
}
77+
}
78+
5779
/**
5880
*
5981
* @param {unknown} value
@@ -64,3 +86,15 @@ function isThemeId(value) {
6486
typeof value === "string" && ["light", "dark", "system"].includes(value)
6587
);
6688
}
89+
90+
/**
91+
*
92+
* @param {unknown} value
93+
* @returns {value is ThemeSetting}
94+
*/
95+
function isThemeSetting(value) {
96+
return (
97+
typeof value === "string" &&
98+
["light", "dark", "system", "tab"].includes(value)
99+
);
100+
}

src/base/scripts/webviews/preload.js

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// @ts-nocheck The file requires a review, before dedicating time to add quality types.
22

3+
const { ipcRenderer } = require("electron");
4+
35
// Set the title of the tab name
46
/// Instead of the tab name being "PROJECT_NAME - Penpot", this script will remove the " - Penpot" portion.
57
function SetTitleToDash() {
@@ -77,4 +79,40 @@ window.onload = function() {
7779
}
7880
};
7981
}
80-
};
82+
};
83+
84+
// Theme
85+
window.addEventListener("DOMContentLoaded", () =>
86+
onClassChange(document.body, () => dispatchThemeUpdate())
87+
);
88+
89+
ipcRenderer.on("theme-request-update", () => dispatchThemeUpdate());
90+
91+
/**
92+
* Observes a node and executes a callback on a class change.
93+
*
94+
* @param {Parameters<MutationObserver["observe"]>[0]} node
95+
* @param {function} callback
96+
*/
97+
function onClassChange(node, callback) {
98+
const observer = new MutationObserver((mutations) => {
99+
const hasClassChanged = mutations.some(
100+
({ type, attributeName }) =>
101+
type === "attributes" && attributeName === "class"
102+
);
103+
104+
if (hasClassChanged) {
105+
callback();
106+
}
107+
});
108+
109+
observer.observe(node, { attributes: true });
110+
}
111+
112+
/**
113+
* Sends an event, with a currently set theme, to the webview's host.
114+
*/
115+
function dispatchThemeUpdate() {
116+
const isLightTheme = document.body.classList.contains("light");
117+
ipcRenderer.sendToHost("theme-update", isLightTheme ? "light" : "dark");
118+
}

src/process/global.d.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
type Electron = import("electron");
2+
13
declare var api: {
24
send: (channel: string, data: unknown) => void;
3-
setTheme: (themeId: "light" | "dark" | "system") => void;
5+
setTheme: (themeId: Electron.NativeTheme["themeSource"]) => void;
46
onOpenTab: (callback: (href: string) => void) => void;
57
onTabMenuAction: (
68
callback: ({ command: string, tabId: number }) => void
79
) => void;
810
};
911

10-
declare var mainWindow: import("electron").BrowserWindow;
12+
declare var mainWindow: Electron.BrowserWindow;
1113

1214
declare var transparent: boolean;
1315
declare var AppIcon: string;

0 commit comments

Comments
 (0)