Skip to content
Merged
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
23 changes: 15 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,11 +303,18 @@ For packages containing React components (JSX):

## Known Issues & Solutions

### Text Editor with Slate
- **Issue**: React 19's `root.render()` can reset component state, causing text to be lost
- **Solution**: Custom text renderer with persistent editor instance and local update tracking
- **Do NOT**: Switch to `@plait-board/react-text` or attempt to use Lexical (deeply integrated with Slate)

### Default Chinese Text
- **Issue**: New text elements show "文本" (Chinese for "text")
- **Solution**: `normalizeTextValue()` function replaces with empty string
### Tool Change Event System
- **Issue**: Plait doesn't provide hooks for tool changes triggered internally (e.g., after creating elements)
- **Solution**: `withToolSync` plugin monkey-patches `BoardTransforms.updatePointerType` to emit custom events
- **Event**: `CUSTOM_EVENTS.TOOL_CHANGE` dispatched when pointer transitions to selection mode
- **Usage**: Listen with `window.addEventListener(CUSTOM_EVENTS.TOOL_CHANGE, handler)`
- **Location**: `features/board/plugins/with-tool-sync.ts`
- **Why this approach**:
- Enables React state integration for tool tracking
- Supports conditional rendering based on active tool
- Triggers side effects (analytics, UI updates) on tool changes
- Handles internal Plait pointer changes (e.g., after element creation)
- **Alternative**: Drawnix reads `board.pointer` directly (simpler but no React state)
- **Tradeoff**: More complex but provides better React integration
- **Note**: Monitor Plait updates for native event support

2 changes: 2 additions & 0 deletions features/board/components/BoardCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { withScribble } from '../plugins/scribble';
import { withEraser } from '../plugins/with-eraser';
import { withStickyNote } from '../plugins/with-sticky-note';
import { withHanddrawn } from '../plugins/handdrawn-mode';
import { withToolSync } from '../plugins/with-tool-sync';
import { withGrid } from '../grid';
import { useBoardState } from '../hooks/use-board-state';
import { DRAWING_TOOLS } from '@/shared/constants';
Expand Down Expand Up @@ -71,6 +72,7 @@ const createPlugins = (onPencilModeChange?: (isPencilMode: boolean) => void): Pl
withEraser,
withStickyNote,
withHanddrawn,
withToolSync,
withPinchZoom,
];

Expand Down
8 changes: 4 additions & 4 deletions features/board/grid/grid-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { PlaitBoard, ThemeColorMode } from '@plait/core';
import type { BoardBackground, GridType, ViewportBounds } from './types';
import { DEFAULT_BOARD_BACKGROUND, GRID_BACKGROUND_COLORS, GRID_DENSITIES } from './types';
import type { GridDensity } from '@thinkix/shared';
import { STORAGE_KEYS } from '@/shared/constants';

const VALID_GRID_TYPES: GridType[] = ['dot', 'square', 'blueprint', 'isometric', 'ruled', 'blank'];
import { getViewportBounds } from './utils/world-to-screen';
Expand All @@ -16,7 +17,6 @@ import {
RuledGridRenderer,
} from './renderers';

const GRID_STORAGE_KEY = 'thinkix:grid-background';
const GRID_MAX_RETRIES = 10;
const GRID_INIT_DEBOUNCE_MS = 100;

Expand Down Expand Up @@ -61,7 +61,7 @@ function isValidDensity(density: unknown): density is GridDensity {
function getStoredGridConfig(): BoardBackground | null {
if (!isBrowser()) return null
try {
const stored = localStorage.getItem(GRID_STORAGE_KEY)
const stored = localStorage.getItem(STORAGE_KEYS.GRID_BACKGROUND)
if (stored) {
const parsed = JSON.parse(stored)
if (
Expand All @@ -77,7 +77,7 @@ function getStoredGridConfig(): BoardBackground | null {
showMajor: parsed.showMajor ?? DEFAULT_BOARD_BACKGROUND.showMajor,
}
}
localStorage.removeItem(GRID_STORAGE_KEY)
localStorage.removeItem(STORAGE_KEYS.GRID_BACKGROUND)
}
} catch {
return null
Expand All @@ -88,7 +88,7 @@ function getStoredGridConfig(): BoardBackground | null {
function setStoredGridConfig(config: BoardBackground): void {
if (!isBrowser()) return
try {
localStorage.setItem(GRID_STORAGE_KEY, JSON.stringify(config))
localStorage.setItem(STORAGE_KEYS.GRID_BACKGROUND, JSON.stringify(config))
} catch {
console.warn('Failed to save grid config to localStorage')
}
Expand Down
12 changes: 6 additions & 6 deletions features/board/hooks/use-board-state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
DEFAULT_TOOL,
STICKY_NOTE_POINTER,
MOBILE_BREAKPOINT,
CUSTOM_EVENTS,
STORAGE_KEYS,
} from '@/shared/constants';
import type { BoardState, BoardContextValue, DrawingTool } from '@thinkix/shared';
import type { SaveStatus } from '@thinkix/storage';
Expand All @@ -35,20 +37,18 @@ interface BoardProviderProps {
children: ReactNode;
}

const HANDDRAWN_STORAGE_KEY = 'thinkix:handdrawn';

function getStoredHanddrawn(): boolean {
if (typeof window === 'undefined') return false;
try {
return localStorage.getItem(HANDDRAWN_STORAGE_KEY) === 'true';
return localStorage.getItem(STORAGE_KEYS.HANDDRAWN) === 'true';
} catch {
return false;
}
}

function setStoredHanddrawn(enabled: boolean): void {
try {
localStorage.setItem(HANDDRAWN_STORAGE_KEY, String(enabled));
localStorage.setItem(STORAGE_KEYS.HANDDRAWN, String(enabled));
} catch {
// Ignore storage errors
}
Expand Down Expand Up @@ -207,9 +207,9 @@ export function BoardProvider({ children }: BoardProviderProps) {
}
};

window.addEventListener('thinkix:toolchange', handleToolChange as EventListener);
window.addEventListener(CUSTOM_EVENTS.TOOL_CHANGE, handleToolChange as EventListener);
return () => {
window.removeEventListener('thinkix:toolchange', handleToolChange as EventListener);
window.removeEventListener(CUSTOM_EVENTS.TOOL_CHANGE, handleToolChange as EventListener);
};
}, []);

Expand Down
4 changes: 0 additions & 4 deletions features/board/plugins/with-sticky-note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,6 @@ export const withStickyNote: PlaitPlugin = (board: PlaitBoard) => {

BoardTransforms.updatePointerType(board, PlaitPointerType.selection);

window.dispatchEvent(new CustomEvent('thinkix:toolchange', {
detail: { tool: 'select' }
}));

isCreating = false;
startPoint = null;
return;
Expand Down
47 changes: 47 additions & 0 deletions features/board/plugins/with-tool-sync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
PlaitBoard,
PlaitPlugin,
PlaitPointerType,
BoardTransforms,
} from '@plait/core';
import { CUSTOM_EVENTS } from '@/shared/constants';

const boardPointerStates = new WeakMap<PlaitBoard, string | undefined>();
let isPatched = false;
const originalUpdatePointerType = BoardTransforms.updatePointerType;


function patchUpdatePointerType() {
if (isPatched) return;

try {
isPatched = true;

BoardTransforms.updatePointerType = <T extends string>(board: PlaitBoard, pointer: T) => {
const previousPointer = boardPointerStates.get(board);
originalUpdatePointerType(board, pointer);
boardPointerStates.set(board, pointer);

if (
previousPointer &&
previousPointer !== PlaitPointerType.selection &&
pointer === PlaitPointerType.selection
) {
window.dispatchEvent(
new CustomEvent(CUSTOM_EVENTS.TOOL_CHANGE, {
detail: { tool: 'select' },
})
);
}
};
} catch (error) {
console.error('Failed to patch updatePointerType:', error);
isPatched = false;
}
}

export const withToolSync: PlaitPlugin = (board: PlaitBoard) => {
patchUpdatePointerType();
boardPointerStates.set(board, board.pointer);
return board;
};
Loading
Loading