| Layer | Library | Version | Role |
|---|---|---|---|
| UI Framework | React | 19 | Component rendering |
| Language | TypeScript | 6 | Type safety across the entire codebase |
| Canvas Engine | Fabric.js | 7 | 2D canvas rendering, hit-testing, selection handles |
| State Management | MobX | 6 | Observable models, reactive rendering pipeline |
| React–MobX Bridge | mobx-react-lite | 4 | observer() HOC for React components |
| Styling | Tailwind CSS | 4 | Utility-first styles |
| UI Components | shadcn/ui + Radix UI | — | Accessible primitives (popover, slider, etc.) |
| Icons | Lucide React + react-icons | — | Toolbar and control icons |
| Build Tool | Vite | 8 | Dev server + production build |
| ID Generation | nanoid | 5 | Unique element IDs |
src/
├── main.tsx # React root mount (wrapped with TooltipProvider)
├── App.tsx # Top-level router / layout
├── index.css # Global styles
│
├── store/
│ └── RootStore.ts # Singleton store — creates and holds all managers
│
├── components/
│ ├── icons.tsx # Shared icon wrappers
│ └── ui/ # Generic shadcn/ui components
│ ├── button.tsx
│ ├── button-group.tsx
│ ├── dropdown-menu.tsx
│ ├── kbd.tsx # Key cap badge component
│ ├── popover.tsx
│ ├── scroll-area.tsx
│ ├── separator.tsx
│ ├── slider.tsx
│ ├── tooltip.tsx # Tooltip wrapper components
│ └── spinner.tsx
│
├── lib/
│ └── utils.ts # clsx/tailwind-merge helper
│
└── modules/
├── excileboard/
│ ├── ExcileBoard.tsx # Root whiteboard page layout
│ │
│ ├── types/
│ │ ├── element.ts # ElementType union, strokStyleType
│ │ ├── style.ts # StrokeStyle type
│ │ └── tools.ts # ToolType union
│ │
│ ├── canvas/
│ │ ├── components/
│ │ │ ├── WhiteBoard.tsx # <canvas> mount, calls canvasManager.init()
│ │ │ └── ZoomControls.tsx # +/- zoom buttons with tooltip key badges
│ │ └── managers/
│ │ ├── CanvasManager.ts
│ │ ├── CanvasZoomManager.ts
│ │ ├── CanvasPanningManager.ts
│ │ └── FabricSyncManager.ts
│ │
│ ├── elements/
│ │ └── managers/
│ │ ├── BaseElementManager.ts # Abstract base — id, x, y, w, h, style
│ │ ├── ElementManager.ts # Map<id, element> — the element registry
│ │ ├── ElementFactoryManager.ts # Registry pattern — creates elements by type
│ │ └── [subclass element managers: Rect, Circle, Diamond, Line, Arrow, Draw, Text]
│ │
│ ├── board/
│ │ ├── components/
│ │ │ ├── BoardSearch.tsx
│ │ │ └── RenameBoardDialog.tsx # Dialog for board renaming
│ │ ├── managers/
│ │ │ ├── BoardManager.ts # Manages active board load, save & autosave reaction
│ │ │ ├── BoardListManager.ts # Manages lists, updates meta & deletion of board list
│ │ │ └── BoardStorageManager.ts # Direct LocalStorage I/O helper methods
│ │ └── providers/
│ │ └── BoardProvider.tsx # Attaches board state & shortcut listeners
│ │
│ ├── tools/
│ │ ├── components/
│ │ │ └── [ToolBar and selector controls (Stroke, Background, Opacity, etc.)]
│ │ └── managers/
│ │ ├── ToolManager.ts
│ │ ├── StyleManager.ts
│ │ └── EraserManager.ts
│ │
│ ├── selection/
│ │ └── SelectionManager.tsx
│ │
│ └── ui/
│ ├── ToolControls.tsx # Right-side property panel (shown on selection)
│ ├── ToolbarMenu.tsx # Top menu bar
│ └── ViewPortControls.tsx # Zoom % display + zoom buttons
│
└── shortcuts/
├── types.ts # ShortcutHandler type
├── managers/
│ └── ShortcutManager.ts # Key event registers & window listener management
└── components/
└── ShortcutDialog.tsx # Dialog showcasing available shortcuts
RootStore
├── CanvasManager (canvas lifecycle + event routing)
│ ├── CanvasZoomManager (zoom level, gesture/wheel)
│ ├── CanvasPanningManager (two-finger / mouse wheel pan)
│ └── FabricSyncManager (MobX → Fabric rendering bridge)
├── ElementManager (element registry — the source of truth)
│ └── [per-type element managers — all extend BaseElementManager]
│ ├── RectangleElementManager
│ ├── CircleElementManager
│ ├── DiamondElementManager
│ ├── LineElementManager
│ ├── ArrowElementManager
│ ├── DrawElementManager
│ └── TextElementManager
├── ToolManager (active tool, drag-to-create lifecycle)
├── StyleManager (current style defaults + apply to selection)
├── EraserManager (hit-test erase + fade-out animation)
├── SelectionManager (selected element IDs + style sync)
├── ShortcutManager (keyboard shortcut registry & event listener)
├── BoardManager (active board lifecycle, name, serialization)
├── BoardListManager (collection of all local boards metadata)
└── BoardStorageManager (LocalStorage async save/load/delete core interface)
File: src/store/RootStore.ts
The singleton container. Instantiates every manager and passes this to each so they can communicate peer-to-peer via this.root.someManager without circular imports. There is no prop-drilling or React Context — all state is accessed through this one reference.
File: src/modules/excileboard/canvas/managers/CanvasManager.ts
Owns the fabric.Canvas instance.
- Calls
canvas.init(el)when the<canvas>DOM element is mounted. - Registers all canvas-level events:
mouse:down,mouse:move,mouse:up,mouse:wheel,text:changed,text:editing:exited,selection:created/updated/cleared. - Routes pointer events to
ToolManager.onPointerDown/Move/Up. - Reacts to
toolManager.activeToolchanges: togglesisDrawingMode, setsdefaultCursor/hoverCursor, enables/disablesselectable/eventedon all objects, createsPencilBrushfor the pencil tool. - Holds sub-managers
CanvasZoomManager,CanvasPanningManager,FabricSyncManager.
File: src/modules/excileboard/canvas/managers/CanvasZoomManager.ts
Handles zoom level.
onWheelZoom(opt)— called whenctrlKeyis held during a wheel event (trackpad pinch or ctrl+scroll). Clamps zoom to a min/max range and zooms toward the pointer (canvas.zoomToPoint).toPercentage()— converts the Fabric zoom factor to a 0–100% display value.zoomPercentage— observable used byZoomControlsto display the current zoom level.setZoom(value)/zoomIn()/zoomOut()— programmatic zoom for the +/- buttons.
File: src/modules/excileboard/canvas/managers/CanvasPanningManager.ts
Handles canvas panning.
onWheelPan(opt)— called on plain wheel scroll (noctrlKey). Appliescanvas.relativePan(new Point(-deltaX, -deltaY))to scroll the viewport.
File: src/modules/excileboard/canvas/managers/FabricSyncManager.ts
The reactive bridge between MobX models and Fabric.js objects. This is the most complex manager.
Responsibilities:
start()— sets up a top-level reaction onelementManager.elementsthat callssync()whenever elements are added or removed.sync()— adds new Fabric objects for new elements, removes Fabric objects for deleted elements.createFabricObject(el)— creates the correct Fabric shape for each element type:Rect,Ellipse,Path(diamond),Polyline(line/arrow),Path(draw/pencil),Textbox(text).watchElement(el, fabricObj)— sets up two separate MobX reactions per element:- Geometry reaction — fires when
x/y/width/heightchange → callsapplyToFabricto reposition/resize the shape. - Style reaction — fires when
strokeColor/fillColor/strokeWidth/opacity/strokeStylechange → callsapplyStylewhich only sets visual properties, never geometry (prevents drift).
- Geometry reaction — fires when
applyToFabric(el, obj)— shape-aware full sync: rebuilds Polygon points, Ellipse rx/ry, diamond Path via_setPath, Textbox text/width.applyStyle(el, obj)— sets onlystroke/fill/strokeWidth/opacity/strokeDashArray. For diamonds, rebuilds the Path data via_setPath.handleObjectModified— writes Fabric drag/resize edits back into the element model (bakesscaleX/scaleYintowidth/height).handlePathCreated— captures a completed pencil stroke as aDrawElementManagermodel.handleTextChanged— auto-grows aTextboxwidth whileautoWidthis true (Excalidraw-style single-line growth). Uses a sentinel"|"character to correctly measure trailing spaces.watchTextElement— firestextbox.enterEditing()whenel.isEditingbecomes true; listens forediting:exitedto commit the text back to the model and remove empty elements.watchArrowElement— reacts to arrow endpoints AND bound element geometry so the arrow redraws when a connected shape moves.resolveArrowEndpoints/edgePoint— snaps arrow endpoints to the bounding-box border of bound elements.elementIdAt(x, y)— hit-tests the canvas to find a bindable element at a scene point (used by the arrow tool for binding).
File: src/modules/excileboard/elements/managers/ElementManager.ts
The element registry — the single source of truth for all canvas elements.
- Stores elements in a
Map<string, BaseElementManager>(insertion order = z-order). add(el)/remove(el)/removeById(id)/get(id)— CRUD operations.allcomputed getter returns all elements as an array.- MobX makes the map observable so
FabricSyncManagercan react to insertions and deletions.
File: src/modules/excileboard/elements/managers/BaseElementManager.ts
Abstract base class for all element types. Every element has:
id— unique nanoid string.x, y, width, height, angle— geometry (all MobX observable).strokeColor, fillColor, strokeWidth, strokeStyle, opacity, cornorRadius, fontFamily, fontSize, textAlign— style (all observable).update(props)— generic partial update viaObject.assign.move(dx, dy)— translates the element.- Uses
makeObservable(notmakeAutoObservable) because MobX forbidsmakeAutoObservableon subclassed classes.
Each extends BaseElementManager and adds type-specific observable fields:
| Class | Type string | Extra fields |
|---|---|---|
RectangleElementManager |
"rectangle" |
cornorRadius (inherited, used for rx/ry) |
CircleElementManager |
"circle" |
— |
DiamondElementManager |
"diamond" |
cornorRadius (controls rounded tips via SVG Path) |
LineElementManager |
"line" |
— |
ArrowElementManager |
"arrow" |
x1, y1, x2, y2, startBindingId, endBindingId |
DrawElementManager |
"draw" |
pathData (Fabric path command array from PencilBrush) |
TextElementManager |
"text" |
text, fontSize, fontFamily, isEditing, autoWidth |
File: src/modules/excileboard/elements/managers/ElementFactoryManager.ts
Registry pattern for element creation.
REGISTRYmaps eachElementTypestring to its constructor.create(type, x, y, w, h, style)— instantiates the correct subclass without anyswitchin the caller.
File: src/modules/excileboard/tools/managers/ToolManager.ts
Manages the active drawing tool and the drag-to-create lifecycle.
activeTool— observable. Changing it triggers theCanvasManagerreaction that reconfigures the canvas cursor and selection mode.setActiveTool(tool)— sets the active tool. Reverts to"hand"automatically after placing a shape.onPointerDown(x, y)— starts a drag: creates a draft element viaElementFactoryManager, adds it toElementManager. Special cases for"eraser"(delegates toEraserManager),"text"(immediately enters editing),"arrow"(binds start to element under cursor).onPointerMove(x, y)— updates the draft'swidth/heightas the user drags. For arrows, updatesx2/y2.onPointerUp()— commits the draft. Discards zero-size accidental clicks. For arrows, binds the end to any element under the tip.
File: src/modules/excileboard/tools/managers/StyleManager.ts
Holds the current style defaults and applies them to selected elements.
- Observable fields:
strokeColor, fillColor, strokeWidth, strokeStyle, opacity, fontSize, fontFamily, cornorRadius. applyFromElement(el)— when an element is selected, syncs the panel's values to match that element.setStrokeColor(c)/setFillColor(c)/setStrokeWidth(w)/setStrokStyle(s)/setCornorRadius(r)— update the observable and push the change to all selected elements viaupdateSelected.updateSelected(updates)— iteratesselectionManager.selectedIds, callsel.update(updates)on each.FabricSyncManager's style reaction picks up the change and re-renders without touching geometry.currentDefaultsgetter — returns a style object used byToolManagerwhen creating new elements.
File: src/modules/excileboard/tools/managers/EraserManager.ts
Handles eraser tool logic.
startErasing()/stopErasing()— setisErasingflag, cleared byToolManager.onPointerUp.eraseAtPoint(x, y)— hit-tests the top-most canvas object at the scene point. Skips objects already fading out (erasedIdsset).animateErase(obj, id)— runs a 180ms opacity fade-out viaobj.animate({ opacity: 0 }, ...), then callselementManager.removeById(id). The model removal triggersFabricSyncManager.sync()which removes the Fabric object and disposes its per-element reactions.
File: src/modules/excileboard/selection/SelectionManager.tsx
Tracks which elements are currently selected.
selectedIds— observableSet<string>.setSelectedIds(ids[])— replaces the selection and callsstyleManager.applyFromElement(el)on the first selected element to sync the property panel.clearSelection()— empties the set.selectedElementscomputed — resolves IDs to element models.hasSelectioncomputed —truewhen anything is selected (used to conditionally show the property panel).- Fed by
FabricSyncManager.handleSelectionCreated/Clearedwhich maps Fabric's selection events to element IDs.
File: src/modules/excileboard/board/managers/BoardManager.ts
Manages the currently active drawing board state and coordinates automated storage actions.
open(id)— loads the board data and initializes the auto-save loop.setName(name)— updates the board title string.startAutoSave()— sets up a MobXreactionto watch the board name, background color, elements list, zoom level, and scroll positions. Debounces and dispatches saves to local storage automatically on any mutations.stopAutoSave()/dispose()— tears down the auto-save observer.createBoard()— generates a new random Nanoid ID and launches an empty canvas workspace.
File: src/modules/excileboard/board/managers/BoardListManager.ts
Manages the collection metadata of all boards stored locally on the client browser.
loadList()— reads the board collection array from local storage.updateMeta(meta)— upserts metadata record for a board (name, timestamps) and shifts the target board to the top of the collection list.deleteBoard(id)— deletes the board database record and filters it out of the list collection.renameBoard(id, name)— renames the board in the metadata list, re-saves its data, and notifiesBoardManagerif it's currently open.
File: src/modules/excileboard/board/managers/BoardStorageManager.ts
A lightweight wrapper providing direct LocalStorage I/O methods.
save(data)— serializes and savesBoardDatato localStorage.load(id)— retrieves and deserializesBoardDatafrom localStorage.delete(id)— deletes the key matchingboard:<id>from localStorage.
File: src/modules/shortcuts/managers/ShortcutManager.ts
Handles global keyboard shortcuts registration, key normalization, and dispatching.
init()— registers base shortcuts:ctrl+z(undo),ctrl+shift+z(redo),ctrl+=/ctrl++/ctrl+-(zoom), and1–9(tool selection).register(key, handler)/unregister(key)— binds/unbinds key combinations to callbacks.listen()— binds the global event listener to window keydown events. Skips invoking shortcuts when an input field is active (isInputFocused()).destroy()— safely tears down the window keydown listener on page cleanup to prevent duplicate handlers.getShortcutsList()— exposes structured shortcut data for UI reference dialogs.
User interaction (mouse / keyboard)
│
▼
CanvasManager ──────────────────────────────────────────────────────┐
(Fabric events) │
│ │
├─► ToolManager.onPointerDown/Move/Up │
│ │ │
│ ├─► ElementFactoryManager.create() │
│ │ └─► ElementManager.add(el) ◄──────────┐ │
│ │ │ │
│ └─► EraserManager.eraseAtPoint() │ │
│ └─► ElementManager.removeById() │ │
│ │ │
└─► FabricSyncManager (reacts to ElementManager) │ │
│ │ │
├─► sync() ── creates / removes Fabric objects │ │
├─► watchElement() per-element reactions │ │
│ ├─► geometry reaction → applyToFabric() │ │
│ └─► style reaction → applyStyle() │ │
│ │ │
└─► handleObjectModified ───────────────────────┘ │
(writes Fabric edits back into model) │
│
StyleManager ◄──────────────────────────────────────────────────────┘
(selection:created → applyFromElement → UI property panel)
Keyboard Key Press (keydown on window)
│
▼
ShortcutManager.listen()
│
├──► isInputFocused() checks? ────► [YES] ──► Ignore (user is typing in input)
│
▼ [NO]
parseKey(e) (Normalize modifiers + key) (e.g. "ctrl+shift+z", "1", "ctrl+=")
│
▼
registry.get(key)
│
▼
Executes Registered Handler Callback
│
├─► "ctrl+z" / "ctrl+shift+z" ──► historyManager.undo() / redo()
├─► "ctrl+=" / "ctrl+-" ──► canvasManager.zoomManager.zoomIn() / zoomOut()
└─► "1" to "9" ──► toolManager.setActiveTool(value)
Rule: Element models are the single source of truth. FabricSyncManager only reads models to render — it never holds rendering state. The only writes back to models are from handleObjectModified, handlePathCreated, and handleTextChanged (Fabric → model sync after user interaction).