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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ playwright-report/
test-results/
playwright/.cache/
.env.local
.claude/
.claude/
redesign.plan.md
47 changes: 46 additions & 1 deletion app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,29 @@ body {

:root {
--mobile-breakpoint: 1024px;
--radius: 0.625rem;
--radius: 0.75rem;

/* Typography */
--tx-font-body: 13px ui-sans-serif, system-ui, sans-serif;
--tx-font-label: 11px ui-sans-serif, system-ui, sans-serif;
--tx-font-shortcut: 10px ui-sans-serif, system-ui, sans-serif;

/* Shadows - Light Mode */
--tx-shadow-toolbar: 0 2px 8px -1px rgba(0, 0, 0, 0.08), 0 1px 4px -1px rgba(0, 0, 0, 0.04);
--tx-shadow-dropdown: 0 4px 12px -1px rgba(0, 0, 0, 0.1), 0 2px 6px -1px rgba(0, 0, 0, 0.06);
--tx-shadow-dialog: 0 8px 24px -4px rgba(0, 0, 0, 0.12), 0 4px 12px -2px rgba(0, 0, 0, 0.08);

/* Backdrop */
--tx-backdrop-blur: 12px;
--tx-backdrop-overlay: rgba(0, 0, 0, 0.4);

/* Sizing */
--tx-toolbar-btn: 36px;
--tx-toolbar-btn-mobile: 32px;
--tx-toolbar-icon: 18px;
--tx-control-btn: 28px;
--tx-control-icon: 15px;
--tx-menu-icon: 16px;
--background: oklch(1 0 0);
--foreground: oklch(0.129 0.042 264.695);
--card: oklch(1 0 0);
Expand Down Expand Up @@ -144,6 +166,14 @@ body {
--sidebar-accent-foreground: oklch(0.984 0.003 247.858);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.551 0.027 264.364);

/* Shadows - Dark Mode */
--tx-shadow-toolbar: 0 2px 8px -1px rgba(0, 0, 0, 0.3), 0 1px 4px -1px rgba(0, 0, 0, 0.2);
--tx-shadow-dropdown: 0 4px 12px -1px rgba(0, 0, 0, 0.4), 0 2px 6px -1px rgba(0, 0, 0, 0.3);
--tx-shadow-dialog: 0 8px 24px -4px rgba(0, 0, 0, 0.5), 0 4px 12px -2px rgba(0, 0, 0, 0.4);

/* Backdrop - Dark Mode */
--tx-backdrop-overlay: rgba(0, 0, 0, 0.6);
}

@layer base {
Expand Down Expand Up @@ -172,6 +202,21 @@ body {
color: transparent;
}

@keyframes icon-pulse {
0%, 100% {
opacity: 0.4;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.05);
}
}

.loading-icon-pulse {
animation: icon-pulse 2s ease-in-out infinite;
}

/* Custom cursors for board tools - !important needed to override Plait's inline cursor styles */
.board-wrapper.eraser-cursor {
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21'/%3E%3Cpath d='M22 21H7'/%3E%3Cpath d='m5 11 9 9'/%3E%3C/svg%3E") 10 10, auto !important;
Expand Down
7 changes: 7 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions features/board/grid/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export { GRID_BACKGROUND_COLORS } from '@thinkix/shared';

import type { BoardBackground as BoardBackgroundType, GridDensity } from '@thinkix/shared';

export const DEFAULT_GRID_DENSITY = 16;
export const DEFAULT_GRID_DENSITY = 24;

export const GRID_DENSITIES: GridDensity[] = [8, 12, 16, 24, 32, 48];

Expand Down Expand Up @@ -43,5 +43,5 @@ export interface ViewportBounds {
export const DEFAULT_BOARD_BACKGROUND: BoardBackgroundType = {
type: 'blank',
density: DEFAULT_GRID_DENSITY,
showMajor: true,
showMajor: false,
};
76 changes: 34 additions & 42 deletions features/board/hooks/use-board-state.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use client';

import {
createContext,
useContext,
Expand Down Expand Up @@ -28,15 +28,15 @@ import { LaserPointer } from '../utils';
import { setHanddrawn, isHanddrawn } from '../plugins/handdrawn-mode';
import { setIsPenMode } from '../plugins/add-pen-mode';
import posthog from 'posthog-js';

type BoardContextValueTyped = BoardContextValue<PlaitBoard>;

const BoardContext = createContext<BoardContextValueTyped | null>(null);

interface BoardProviderProps {
children: ReactNode;
}

function getStoredHanddrawn(): boolean {
if (typeof window === 'undefined') return false;
try {
Expand All @@ -45,32 +45,32 @@ function getStoredHanddrawn(): boolean {
return false;
}
}

function setStoredHanddrawn(enabled: boolean): void {
try {
localStorage.setItem(STORAGE_KEYS.HANDDRAWN, String(enabled));
} catch {
// Ignore storage errors
}
}


function detectMobile(): boolean {
if (typeof window === 'undefined') return false;
const isTouchDevice = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
const isSmallScreen = window.innerWidth < MOBILE_BREAKPOINT;
const isMobileUA = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
return isMobileUA || (isTouchDevice && isSmallScreen);
}

function debounce<T extends (...args: unknown[]) => void>(fn: T, delay: number): T {
let timeoutId: ReturnType<typeof setTimeout>;
return ((...args: unknown[]) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
}) as T;
}

export function BoardProvider({ children }: BoardProviderProps) {
const [board, setBoard] = useState<PlaitBoard | null>(null);
const [state, setState] = useState<BoardState>(() => ({
Expand All @@ -82,11 +82,9 @@ export function BoardProvider({ children }: BoardProviderProps) {
isMobile: detectMobile(),
isPencilMode: false,
}));

const boardRef = useRef<PlaitBoard | null>(null);
const laserPointerRef = useRef<LaserPointer | null>(null);
const handdrawnAppliedRef = useRef(false);

useEffect(() => {
const handleResize = debounce(() => {
const isMobile = detectMobile();
Expand All @@ -97,40 +95,34 @@ export function BoardProvider({ children }: BoardProviderProps) {
return prev;
});
}, 150);

window.addEventListener('resize', handleResize);
window.addEventListener('orientationchange', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
window.removeEventListener('orientationchange', handleResize);
};
}, []);

useEffect(() => {
boardRef.current = board;

if (board && state.handdrawn && !handdrawnAppliedRef.current) {
setHanddrawn(board, true, 'excalidraw');
handdrawnAppliedRef.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [board]);

const setActiveTool = useCallback(
(tool: DrawingTool) => {
const currentBoard = boardRef.current;

setState((prev) => ({ ...prev, activeTool: tool }));

if (!currentBoard) return;

posthog.capture('tool_selected', { tool });

if (laserPointerRef.current) {
laserPointerRef.current.destroy();
laserPointerRef.current = null;
}

if (tool === 'image') {
selectImage(currentBoard, 400, (imageItem) => {
DrawTransforms.insertImage(currentBoard, imageItem);
Expand All @@ -139,30 +131,30 @@ export function BoardProvider({ children }: BoardProviderProps) {
});
return;
}

if (tool === 'laser') {
BoardTransforms.updatePointerType(currentBoard, PlaitPointerType.hand);
setCreationMode(currentBoard, BoardCreationMode.dnd);
laserPointerRef.current = new LaserPointer();
laserPointerRef.current.init(currentBoard);
return;
}

if (tool === 'eraser') {
BoardTransforms.updatePointerType(currentBoard, 'eraser');
setCreationMode(currentBoard, BoardCreationMode.drawing);
return;
}

if (tool === 'stickyNote') {
BoardTransforms.updatePointerType(currentBoard, STICKY_NOTE_POINTER);
setCreationMode(currentBoard, BoardCreationMode.drawing);
return;
}

const pointerType = TOOL_TO_POINTER[tool];
BoardTransforms.updatePointerType(currentBoard, pointerType);

if (DRAWING_TOOLS.has(tool)) {
setCreationMode(currentBoard, BoardCreationMode.drawing);
} else {
Expand All @@ -171,15 +163,15 @@ export function BoardProvider({ children }: BoardProviderProps) {
},
[]
);

const setCurrentBoardId = useCallback((id: string | null) => {
setState((prev) => ({ ...prev, currentBoardId: id }));
}, []);

const setSaveStatus = useCallback((status: SaveStatus) => {
setState((prev) => ({ ...prev, saveStatus: status }));
}, []);

const toggleHanddrawn = useCallback(() => {
const currentBoard = boardRef.current;
const newMode = currentBoard ? !isHanddrawn(currentBoard) : true;
Expand All @@ -190,7 +182,7 @@ export function BoardProvider({ children }: BoardProviderProps) {
posthog.capture('handdrawn_mode_toggled', { enabled: newMode });
setState((prev) => ({ ...prev, handdrawn: newMode }));
}, []);

const setPencilMode = useCallback((enabled: boolean) => {
const currentBoard = boardRef.current;
if (currentBoard) {
Expand All @@ -199,20 +191,20 @@ export function BoardProvider({ children }: BoardProviderProps) {
posthog.capture('pencil_mode_toggled', { enabled });
setState((prev) => ({ ...prev, isPencilMode: enabled }));
}, []);

useEffect(() => {
const handleToolChange = (e: CustomEvent<{ tool: DrawingTool }>) => {
if (e.detail?.tool) {
setState((prev) => ({ ...prev, activeTool: e.detail.tool }));
}
};

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

const value = useMemo<BoardContextValue>(
() => ({
board,
Expand All @@ -227,17 +219,17 @@ export function BoardProvider({ children }: BoardProviderProps) {
}),
[board, state, setActiveTool, setCurrentBoardId, setSaveStatus, toggleHanddrawn, setPencilMode]
);

return (
<BoardContext.Provider value={value}>{children}</BoardContext.Provider>
);
}


export function useBoardState(): BoardContextValueTyped {
const context = useContext(BoardContext);
if (!context) {
throw new Error('useBoardState must be used within BoardProvider');
}
return context;
}
}
Loading
Loading