Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
{
"cSpell.words": [
"drawnix"
]
}
"cSpell.words": ["drawnix"]
}
71 changes: 64 additions & 7 deletions apps/web/src/app/app.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import { useState, useEffect } from 'react';
import { initializeData } from './initialize-data';
import { Drawnix } from '@drawnix/drawnix';
import { PlaitBoard, PlaitElement, PlaitTheme, Viewport } from '@plait/core';
import {
PlaitBoard,
PlaitElement,
PlaitTheme,
Viewport,
ThemeColorMode,
BoardTransforms,
} from '@plait/core';
import { BoardChangeData } from '@plait-board/react-board';
import localforage from 'localforage';
import { loadThemeFromStorage, saveThemeToStorage } from '@drawnix/drawnix';

// 1个月后移出删除兼容
const OLD_DRAWNIX_LOCAL_DATA_KEY = 'drawnix-local-data';
Expand All @@ -23,20 +32,42 @@ export function App() {

useEffect(() => {
const loadData = async () => {
// Load saved theme from localStorage
const savedTheme = loadThemeFromStorage();

const storedData = await localforage.getItem(MAIN_BOARD_CONTENT_KEY);
if (storedData) {
setValue(storedData as any);
const data = storedData as any;
// Always use theme from localStorage, not from stored data
setValue({
children: data.children,
viewport: data.viewport,
theme: { themeColorMode: savedTheme || ThemeColorMode.default },
});
return;
}
const localData = localStorage.getItem(OLD_DRAWNIX_LOCAL_DATA_KEY);
if (localData) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to remove the feedback logic later, so you can ignore it.

const parsedData = JSON.parse(localData);
setValue(parsedData);
await localforage.setItem(MAIN_BOARD_CONTENT_KEY, parsedData);
// Use theme from localStorage, not from old data
setValue({
children: parsedData.children,
viewport: parsedData.viewport,
theme: { themeColorMode: savedTheme || ThemeColorMode.default },
});
// Save only children and viewport to new storage
await localforage.setItem(MAIN_BOARD_CONTENT_KEY, {
children: parsedData.children,
viewport: parsedData.viewport,
});
localStorage.removeItem(OLD_DRAWNIX_LOCAL_DATA_KEY);
return;
}
setValue({ children: initializeData });
// For new users, use saved theme or default
setValue({
children: initializeData,
theme: { themeColorMode: savedTheme || ThemeColorMode.default },
});
};

loadData();
Expand All @@ -46,11 +77,37 @@ export function App() {
value={value.children}
viewport={value.viewport}
theme={value.theme}
onChange={(value) => {
localforage.setItem(MAIN_BOARD_CONTENT_KEY, value);
//@ts-ignore
onChange={(boardData: BoardChangeData) => {
// Save theme to localStorage when it changes
if (boardData.theme?.themeColorMode) {
saveThemeToStorage(boardData.theme.themeColorMode);
}

// Don't save theme to localforage - only save content and viewport
const newValue = {
children: boardData.children,
viewport: boardData.viewport,
theme: boardData.theme, // Keep for state but don't persist
};
setValue(newValue);

// Only save children and viewport to localforage, not theme
Copy link
Contributor

@pubuzhixing8 pubuzhixing8 Aug 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am curious that why not save theme to localforage and save it to localStorage.

I think is fine that save viewport, children, and theme to one place.

localforage.setItem(MAIN_BOARD_CONTENT_KEY, {
children: boardData.children,
viewport: boardData.viewport,
});
}}
afterInit={(board) => {
console.log('board initialized');

// Ensure the saved theme is applied after board initialization
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the theme could be initialized correctly, whether the logic maybe is unnecessary.

const savedTheme = loadThemeFromStorage();
if (savedTheme && board.theme.themeColorMode !== savedTheme) {
console.log('Applying saved theme after board init:', savedTheme);
BoardTransforms.updateThemeColor(board, savedTheme);
}

console.log(
`add __drawnix__web__debug_log to window, so you can call add log anywhere, like: window.__drawnix__web__console('some thing')`
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const OpenFile = () => {
data-testid="open-button"
onSelect={() => {
loadFromJSON(board).then((data) => {
clearAndLoad(data.elements, data.viewport);
clearAndLoad(data.elements, data.viewport, data.theme);
});
}}
icon={OpenFileIcon}
Expand All @@ -84,13 +84,15 @@ export const SaveAsImage = () => {
saveAsImage(board, true);
}}
submenu={
<Menu onSelect={() => {
const itemSelectEvent = new CustomEvent(EVENT.MENU_ITEM_SELECT, {
bubbles: true,
cancelable: true,
});
menuContentProps.onSelect?.(itemSelectEvent);
}}>
<Menu
onSelect={() => {
const itemSelectEvent = new CustomEvent(EVENT.MENU_ITEM_SELECT, {
bubbles: true,
cancelable: true,
});
menuContentProps.onSelect?.(itemSelectEvent);
}}
>
<MenuItem
onSelect={() => {
saveAsImage(board, true);
Expand Down
3 changes: 2 additions & 1 deletion packages/drawnix/src/components/toolbar/creation-toolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React from 'react';
import classNames from 'classnames';
import { Island } from '../island';
import Stack from '../stack';
Expand Down Expand Up @@ -156,7 +157,7 @@ export const CreationToolbar = () => {
<Stack.Row gap={1}>
{BUTTONS.map((button, index) => {
if (appState.isMobile && button.pointer === PlaitPointerType.hand) {
return <></>;
return <React.Fragment key={index}></React.Fragment>;
}
if (button.key === PopupKey.shape) {
return (
Expand Down
4 changes: 2 additions & 2 deletions packages/drawnix/src/components/toolbar/theme-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export const ThemeToolbar = () => {
>
<select
onChange={(e) => {
const value = (e.target as HTMLSelectElement).value;
BoardTransforms.updateThemeColor(board, value as ThemeColorMode);
const value = (e.target as HTMLSelectElement).value as ThemeColorMode;
BoardTransforms.updateThemeColor(board, value);
}}
value={theme.themeColorMode}
>
Expand Down
1 change: 1 addition & 0 deletions packages/drawnix/src/data/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const serializeAsJSON = (board: PlaitBoard): string => {
source: 'web',
elements: board.children,
viewport: board.viewport,
theme: board.theme,
};

return JSON.stringify(data, null, 2);
Expand Down
7 changes: 4 additions & 3 deletions packages/drawnix/src/data/types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { PlaitElement, Viewport } from '@plait/core';
import { PlaitElement, PlaitTheme, Viewport } from '@plait/core';

export interface DrawnixExportedData {
type: DrawnixExportedType.drawnix;
version: number;
source: 'web';
elements: PlaitElement[];
viewport: Viewport;
theme?: PlaitTheme;
}

export enum DrawnixExportedType {
drawnix = 'drawnix'
}
drawnix = 'drawnix',
}
69 changes: 67 additions & 2 deletions packages/drawnix/src/utils/image.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,79 @@
import { getSelectedElements, PlaitBoard } from '@plait/core';
import { getSelectedElements, PlaitBoard, ThemeColorMode } from '@plait/core';
import { base64ToBlob, boardToImage, download } from './common';
import { fileOpen } from '../data/filesystem';
import { IMAGE_MIME_TYPES } from '../constants';
import { insertImage } from '../data/image';

// Get the actual background color from the board container
const getThemeBackgroundColor = (board: PlaitBoard): string => {
try {
const boardContainer = PlaitBoard.getBoardContainer(board);

// Check multiple elements in the hierarchy for background color
const elementsToCheck = [
boardContainer,
boardContainer.parentElement,
boardContainer.closest('.drawnix'),
document.body,
].filter(Boolean);

for (const element of elementsToCheck) {
if (element) {
const computedStyle = window.getComputedStyle(element as Element);
const backgroundColor = computedStyle.backgroundColor;

// If we find a non-transparent background color, use it
if (
backgroundColor &&
backgroundColor !== 'transparent' &&
backgroundColor !== 'rgba(0, 0, 0, 0)' &&
backgroundColor !== 'initial' &&
backgroundColor !== 'inherit'
) {
return backgroundColor;
}
}
}

// If no background color found, use fallback
return getThemeBackgroundColorFallback(board.theme.themeColorMode);
} catch (error) {
console.warn(
'Could not get background color from DOM, using fallback',
error
);
return getThemeBackgroundColorFallback(board.theme.themeColorMode);
}
};

// Fallback theme background colors mapping
const getThemeBackgroundColorFallback = (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can get the active themeColors data through PlaitBoard.getThemeColors(board);.

You can encapsulate e method exactly to get the background color of the drawing board in this way.

themeColorMode: ThemeColorMode
): string => {
switch (themeColorMode) {
case ThemeColorMode.dark:
return '#121212'; // Dark theme background (darker)
case ThemeColorMode.starry:
return '#0a0a1a'; // Starry theme background (very dark blue/purple)
case ThemeColorMode.retro:
return '#f4f1e8'; // Retro theme background (warm cream)
case ThemeColorMode.colorful:
return '#ffffff'; // Colorful theme background (pure white)
case ThemeColorMode.soft:
return '#f8f9fa'; // Soft theme background (very light gray)
case ThemeColorMode.default:
default:
return '#ffffff'; // Default theme background (white)
}
};

export const saveAsImage = (board: PlaitBoard, isTransparent: boolean) => {
const selectedElements = getSelectedElements(board);
const themeBackgroundColor = getThemeBackgroundColor(board);

boardToImage(board, {
elements: selectedElements.length > 0 ? selectedElements : undefined,
fillStyle: isTransparent ? 'transparent' : 'white',
fillStyle: isTransparent ? 'transparent' : themeBackgroundColor,
}).then((image) => {
if (image) {
const ext = isTransparent ? 'png' : 'jpg';
Expand Down
3 changes: 2 additions & 1 deletion packages/drawnix/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from './color';
export * from './common';
export * from './image';
export * from './property';
export * from './utility-types';
export * from './utility-types';
export * from './theme-storage';
34 changes: 34 additions & 0 deletions packages/drawnix/src/utils/theme-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ThemeColorMode } from '@plait/core';

const THEME_STORAGE_KEY = 'drawnix-theme';

export const saveThemeToStorage = (theme: ThemeColorMode): void => {
try {
localStorage.setItem(THEME_STORAGE_KEY, theme);
} catch (error) {
console.warn('Failed to save theme to localStorage:', error);
}
};

export const loadThemeFromStorage = (): ThemeColorMode | null => {
try {
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY);
if (
savedTheme &&
Object.values(ThemeColorMode).includes(savedTheme as ThemeColorMode)
) {
return savedTheme as ThemeColorMode;
}
} catch (error) {
console.warn('Failed to load theme from localStorage:', error);
}
return null;
};

export const clearThemeFromStorage = (): void => {
try {
localStorage.removeItem(THEME_STORAGE_KEY);
} catch (error) {
console.warn('Failed to clear theme from localStorage:', error);
}
};