From 46e4d319268cde02114a6cfe8cd0d8f1b836d7ba Mon Sep 17 00:00:00 2001 From: Zakher Masri Date: Thu, 10 Apr 2025 09:22:27 +0300 Subject: [PATCH 1/4] Add custom theme variables and integrate ThemeCustomizer component - Introduced CustomThemeVariable interface for managing theme variables. - Added customThemeVariablesAtom for state management of custom theme variables. - Updated ThemeSection to include ThemeCustomizer for enhanced theme customization options. --- src/components/theme-customizer.tsx | 95 +++++++++++++++++++++++++++++ src/global-state.ts | 11 ++++ src/routes/_appRoot.settings.tsx | 48 ++++++++------- 3 files changed, 133 insertions(+), 21 deletions(-) create mode 100644 src/components/theme-customizer.tsx diff --git a/src/components/theme-customizer.tsx b/src/components/theme-customizer.tsx new file mode 100644 index 00000000..e7ab2e7b --- /dev/null +++ b/src/components/theme-customizer.tsx @@ -0,0 +1,95 @@ +import { useAtom } from "jotai" +import React from "react" +import { themeAtom } from "../global-state" +import { Button } from "./button" +import { Dialog } from "./dialog" +import { FormControl } from "./form-control" + +const THEME_VARIABLES = { + "--color-text": "Text", + "--color-text-secondary": "Secondary Text", + "--color-text-tertiary": "Tertiary Text", + "--color-text-danger": "Danger Text", + "--color-bg": "Background", + "--color-bg-secondary": "Secondary Background", + "--color-bg-tertiary": "Tertiary Background", + "--color-border-primary": "Primary Border", + "--color-border-secondary": "Secondary Border", + "--color-border-focus": "Focus Border", +} as const + +type ThemeVariable = keyof typeof THEME_VARIABLES + +export function ThemeCustomizer() { + const [theme, setTheme] = useAtom(themeAtom) + const [isDialogOpen, setIsDialogOpen] = React.useState(false) + const [colorValues, setColorValues] = React.useState>(() => { + // Get initial colors from CSS variables + const colors = {} as Record + const style = getComputedStyle(document.documentElement) + Object.keys(THEME_VARIABLES).forEach((variable) => { + colors[variable as ThemeVariable] = style.getPropertyValue(variable).trim() || "#000000" + }) + return colors + }) + + const handleColorChange = (variable: ThemeVariable, value: string) => { + setColorValues((prev) => ({ + ...prev, + [variable]: value, + })) + } + + const handleSave = () => { + // Apply colors to CSS variables + Object.entries(colorValues).forEach(([variable, value]) => { + document.documentElement.style.setProperty(variable, value) + }) + setIsDialogOpen(false) + } + + return ( + + + + + +
+ {(Object.entries(THEME_VARIABLES) as [ThemeVariable, string][]).map( + ([variable, label]) => ( +
+ +
+ handleColorChange(variable, e.target.value)} + className="h-8 w-8 rounded cursor-pointer" + /> + handleColorChange(variable, e.target.value)} + className="flex-grow px-2 py-1 rounded border border-border-secondary bg-bg-secondary" + /> +
+
+
+ ), + )} +
+ + +
+
+
+
+ ) +} diff --git a/src/global-state.ts b/src/global-state.ts index d88237cb..f4a4d7d0 100644 --- a/src/global-state.ts +++ b/src/global-state.ts @@ -735,6 +735,17 @@ export const weeklyTemplateAtom = selectAtom(templatesAtom, (templates) => export const themeAtom = atomWithStorage<"default" | "eink">("theme", "default") +export interface CustomThemeVariable { + name: string + value: string + category: "text" | "background" | "border" | "syntax" +} + +export const customThemeVariablesAtom = atomWithStorage( + "custom_theme_variables", + [], +) + export const fontAtom = atomWithStorage<"sans" | "serif" | "handwriting">("font", "sans") export const sidebarAtom = atomWithStorage<"expanded" | "collapsed">("sidebar", "expanded") diff --git a/src/routes/_appRoot.settings.tsx b/src/routes/_appRoot.settings.tsx index 90b9e58d..dce60279 100644 --- a/src/routes/_appRoot.settings.tsx +++ b/src/routes/_appRoot.settings.tsx @@ -24,6 +24,7 @@ import { } from "../global-state" import { useEditorSettings } from "../hooks/editor-settings" import { cx } from "../utils/cx" +import { ThemeCustomizer } from "../components/theme-customizer" export const Route = createFileRoute("/_appRoot/settings")({ component: RouteComponent, @@ -158,27 +159,32 @@ function ThemeSection() { return ( - setTheme(value as "default" | "eink")} - className="flex flex-col gap-3 coarse:gap-4" - name="theme" - > -
- - -
-
- - -
-
+
+ setTheme(value as "default" | "eink")} + className="flex flex-col gap-3 coarse:gap-4" + name="theme" + > +
+ + +
+
+ + +
+
+ +
+ +
) } From 750727779b1a35dbaa6f6af8425632db1aa84e17 Mon Sep 17 00:00:00 2001 From: Zakher Masri Date: Thu, 10 Apr 2025 09:35:39 +0300 Subject: [PATCH 2/4] Refactor ThemeCustomizer to use global state and implement theme saving - Replaced useAtom with useSetAtom for managing theme state. - Integrated functionality to generate and save theme.css with updated color values. - Added useLoadTheme hook in RouteComponent to load custom themes on app initialization. --- src/components/theme-customizer.tsx | 21 ++++++++++++++--- src/hooks/theme.ts | 36 +++++++++++++++++++++++++++++ src/routes/_appRoot.tsx | 5 ++++ 3 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 src/hooks/theme.ts diff --git a/src/components/theme-customizer.tsx b/src/components/theme-customizer.tsx index e7ab2e7b..e30d32b8 100644 --- a/src/components/theme-customizer.tsx +++ b/src/components/theme-customizer.tsx @@ -1,6 +1,6 @@ -import { useAtom } from "jotai" +import { useSetAtom } from "jotai" import React from "react" -import { themeAtom } from "../global-state" +import { globalStateMachineAtom } from "../global-state" import { Button } from "./button" import { Dialog } from "./dialog" import { FormControl } from "./form-control" @@ -21,7 +21,7 @@ const THEME_VARIABLES = { type ThemeVariable = keyof typeof THEME_VARIABLES export function ThemeCustomizer() { - const [theme, setTheme] = useAtom(themeAtom) + const send = useSetAtom(globalStateMachineAtom) const [isDialogOpen, setIsDialogOpen] = React.useState(false) const [colorValues, setColorValues] = React.useState>(() => { // Get initial colors from CSS variables @@ -45,6 +45,21 @@ export function ThemeCustomizer() { Object.entries(colorValues).forEach(([variable, value]) => { document.documentElement.style.setProperty(variable, value) }) + + // Generate theme.css content + const cssContent = `:root { +${Object.entries(colorValues) + .map(([variable, value]) => ` ${variable}: ${value};`) + .join("\n")} +} +` + // Save theme.css to the repo + send({ + type: "WRITE_FILES", + markdownFiles: { "theme.css": cssContent }, + commitMessage: "Update theme colors", + }) + setIsDialogOpen(false) } diff --git a/src/hooks/theme.ts b/src/hooks/theme.ts new file mode 100644 index 00000000..9c0adc6c --- /dev/null +++ b/src/hooks/theme.ts @@ -0,0 +1,36 @@ +import { useAtomValue } from "jotai" +import React from "react" +import { isRepoClonedAtom } from "../global-state" +import { fs } from "../utils/fs" +import { REPO_DIR } from "../utils/git" + +export function useLoadTheme() { + const isRepoCloned = useAtomValue(isRepoClonedAtom) + + React.useEffect(() => { + if (!isRepoCloned) return + + async function loadTheme() { + try { + // Try to read theme.css from the repo + const content = await fs.promises.readFile(`${REPO_DIR}/theme.css`, "utf8") + + // Create a style element + const style = document.createElement("style") + style.setAttribute("id", "custom-theme") + style.textContent = content.toString() + + // Remove any existing custom theme + document.getElementById("custom-theme")?.remove() + + // Add the new style element + document.head.appendChild(style) + } catch (error) { + // If theme.css doesn't exist, that's fine - we'll use default theme + console.debug("No custom theme found:", error) + } + } + + loadTheme() + }, [isRepoCloned]) +} diff --git a/src/routes/_appRoot.tsx b/src/routes/_appRoot.tsx index 918d13ee..30082767 100644 --- a/src/routes/_appRoot.tsx +++ b/src/routes/_appRoot.tsx @@ -23,6 +23,7 @@ import { } from "../global-state" import { useSearchNotes } from "../hooks/search" import { useValueRef } from "../hooks/value-ref" +import { useLoadTheme } from "../hooks/theme" export const Route = createFileRoute("/_appRoot")({ component: RouteComponent, @@ -49,6 +50,9 @@ function RouteComponent() { const { online } = useNetworkState() const rootRef = React.useRef(null) + // Load custom theme if available + useLoadTheme() + // Sync when the app becomes visible again useEvent("visibilitychange", () => { if (document.visibilityState === "visible" && online) { @@ -174,6 +178,7 @@ function RouteComponent() { ] sendVoiceConversation({ type: "ADD_TOOLS", tools }) + return () => { sendVoiceConversation({ type: "REMOVE_TOOLS", toolNames: tools.map((tool) => tool.name) }) } From 7ab4c28f2634eadb1604fca64a030bfb67984cf1 Mon Sep 17 00:00:00 2001 From: Zakher Masri Date: Thu, 10 Apr 2025 20:53:40 +0300 Subject: [PATCH 3/4] Remove CustomThemeVariable interface and customThemeVariablesAtom from global state management --- src/global-state.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/global-state.ts b/src/global-state.ts index f4a4d7d0..d88237cb 100644 --- a/src/global-state.ts +++ b/src/global-state.ts @@ -735,17 +735,6 @@ export const weeklyTemplateAtom = selectAtom(templatesAtom, (templates) => export const themeAtom = atomWithStorage<"default" | "eink">("theme", "default") -export interface CustomThemeVariable { - name: string - value: string - category: "text" | "background" | "border" | "syntax" -} - -export const customThemeVariablesAtom = atomWithStorage( - "custom_theme_variables", - [], -) - export const fontAtom = atomWithStorage<"sans" | "serif" | "handwriting">("font", "sans") export const sidebarAtom = atomWithStorage<"expanded" | "collapsed">("sidebar", "expanded") From 2f94f63f05072656746891601852ef548b1665bc Mon Sep 17 00:00:00 2001 From: Zakher Masri Date: Fri, 11 Apr 2025 06:02:24 +0300 Subject: [PATCH 4/4] chore: use radix colors + move theme.css to .lumen --- src/components/theme-customizer.tsx | 107 ++++++++------------------ src/hooks/theme.ts | 113 ++++++++++++++++++++++++++-- 2 files changed, 139 insertions(+), 81 deletions(-) diff --git a/src/components/theme-customizer.tsx b/src/components/theme-customizer.tsx index e30d32b8..e6206cbb 100644 --- a/src/components/theme-customizer.tsx +++ b/src/components/theme-customizer.tsx @@ -1,65 +1,21 @@ -import { useSetAtom } from "jotai" import React from "react" -import { globalStateMachineAtom } from "../global-state" import { Button } from "./button" import { Dialog } from "./dialog" -import { FormControl } from "./form-control" - -const THEME_VARIABLES = { - "--color-text": "Text", - "--color-text-secondary": "Secondary Text", - "--color-text-tertiary": "Tertiary Text", - "--color-text-danger": "Danger Text", - "--color-bg": "Background", - "--color-bg-secondary": "Secondary Background", - "--color-bg-tertiary": "Tertiary Background", - "--color-border-primary": "Primary Border", - "--color-border-secondary": "Secondary Border", - "--color-border-focus": "Focus Border", -} as const - -type ThemeVariable = keyof typeof THEME_VARIABLES +import { Tooltip } from "./tooltip" +import { ThemeColor, THEME_COLORS_MAP, getCurrentTheme, applyTheme, useSaveTheme } from "../hooks/theme" export function ThemeCustomizer() { - const send = useSetAtom(globalStateMachineAtom) const [isDialogOpen, setIsDialogOpen] = React.useState(false) - const [colorValues, setColorValues] = React.useState>(() => { - // Get initial colors from CSS variables - const colors = {} as Record - const style = getComputedStyle(document.documentElement) - Object.keys(THEME_VARIABLES).forEach((variable) => { - colors[variable as ThemeVariable] = style.getPropertyValue(variable).trim() || "#000000" - }) - return colors - }) + const [currentTheme, setCurrentTheme] = React.useState(() => getCurrentTheme()) + const saveTheme = useSaveTheme() - const handleColorChange = (variable: ThemeVariable, value: string) => { - setColorValues((prev) => ({ - ...prev, - [variable]: value, - })) + const handleThemeChange = (color: ThemeColor) => { + setCurrentTheme(color) + applyTheme(color) } - const handleSave = () => { - // Apply colors to CSS variables - Object.entries(colorValues).forEach(([variable, value]) => { - document.documentElement.style.setProperty(variable, value) - }) - - // Generate theme.css content - const cssContent = `:root { -${Object.entries(colorValues) - .map(([variable, value]) => ` ${variable}: ${value};`) - .join("\n")} -} -` - // Save theme.css to the repo - send({ - type: "WRITE_FILES", - markdownFiles: { "theme.css": cssContent }, - commitMessage: "Update theme colors", - }) - + const handleSave = async () => { + await saveTheme(currentTheme) setIsDialogOpen(false) } @@ -72,29 +28,28 @@ ${Object.entries(colorValues)
- {(Object.entries(THEME_VARIABLES) as [ThemeVariable, string][]).map( - ([variable, label]) => ( -
- -
- handleColorChange(variable, e.target.value)} - className="h-8 w-8 rounded cursor-pointer" - /> - handleColorChange(variable, e.target.value)} - className="flex-grow px-2 py-1 rounded border border-border-secondary bg-bg-secondary" - /> -
-
-
- ), - )} +
+ +
+ {Object.entries(THEME_COLORS_MAP).map(([color, value]) => ( + + + + + {color} + + ))} +
+