Browse Source

Merge 1e21cb8a9c into f06484c6ab

pull/10469/merge
Himanshugulhane27 2 days ago committed by GitHub
parent
commit
34042f6566
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 105
      INDEX_FEATURE_IMPLEMENTATION.md
  2. 50
      packages/excalidraw/actions/actionIndex.ts
  3. 6
      packages/excalidraw/actions/index.ts
  4. 5
      packages/excalidraw/actions/types.ts
  5. 2
      packages/excalidraw/appState.ts
  6. 44
      packages/excalidraw/components/IndexSection/IndexButton.tsx
  7. 198
      packages/excalidraw/components/IndexSection/IndexSection.scss
  8. 63
      packages/excalidraw/components/IndexSection/IndexSection.test.tsx
  9. 311
      packages/excalidraw/components/IndexSection/IndexSection.tsx
  10. 2
      packages/excalidraw/components/IndexSection/index.ts
  11. 4
      packages/excalidraw/components/LayerUI.tsx
  12. 30
      packages/excalidraw/components/icons.tsx
  13. 13
      packages/excalidraw/locales/en.json
  14. 10
      packages/excalidraw/types.ts
  15. 3927
      yarn.lock

105
INDEX_FEATURE_IMPLEMENTATION.md

@ -0,0 +1,105 @@ @@ -0,0 +1,105 @@
# Index Section Feature Implementation
## Overview
This implementation adds a tab area with an index section for navigating large Excalidraw canvases, as requested in feature request #10465.
## Features Implemented
### 1. Index Section Component (`IndexSection.tsx`)
- **Location**: `packages/excalidraw/components/IndexSection/`
- **Functionality**:
- Display list of saved navigation points (pins)
- Add new pins with custom names
- Navigate to saved locations
- Delete existing pins
- Show coordinates for each pin
### 2. Index Button (`IndexButton.tsx`)
- **Location**: `packages/excalidraw/components/IndexSection/`
- **Functionality**:
- Trigger button to open/close the index section
- Positioned in the top-right UI area
- Uses MapPin icon for visual clarity
### 3. Navigation Features
- **Pin Creation**:
- Click "Add Pin" to create a navigation point
- If elements are selected, pin will be associated with the first selected element
- If no elements selected, pin will be created at current viewport center
- **Navigation**:
- Click the eye icon to navigate to a pin location
- If pin is associated with an element, it will be selected automatically
- Smooth animated navigation to the target location
### 4. UI Integration
- **LayerUI Integration**: Added IndexButton to the top-right UI area
- **Responsive Design**: Component adapts to different screen sizes
- **Theme Support**: Supports both light and dark themes
### 5. Localization
- **English Strings**: Added to `locales/en.json`
- **Keys Added**:
- `indexSection.title`: "Index"
- `indexSection.addPin`: "Add Pin"
- `indexSection.enterName`: "Enter pin name..."
- `indexSection.empty`: "No pins added yet"
- `indexSection.help`: "Select elements or click 'Add Pin' to create navigation points"
- `indexSection.goTo`: "Go to location"
### 6. Icons
- **New Icons Added**: MapPin, Eye, Trash icons for the interface
- **Location**: Added to `components/icons.tsx`
## Files Created/Modified
### New Files:
1. `components/IndexSection/IndexSection.tsx` - Main component
2. `components/IndexSection/IndexSection.scss` - Styles
3. `components/IndexSection/IndexButton.tsx` - Trigger button
4. `components/IndexSection/index.ts` - Exports
### Modified Files:
1. `components/LayerUI.tsx` - Added IndexButton integration
2. `components/icons.tsx` - Added new icons
3. `locales/en.json` - Added localization strings
## Usage Instructions
1. **Opening the Index**: Click the MapPin icon in the top-right area of the interface
2. **Adding Pins**:
- Select elements you want to bookmark (optional)
- Click "Add Pin" button
- Enter a descriptive name for the pin
- Press Enter or click "Add" to save
3. **Navigating**: Click the eye icon next to any pin to navigate to that location
4. **Managing Pins**: Click the trash icon to delete unwanted pins
## Technical Implementation Details
### State Management
- Uses React hooks (useState, useCallback) for local state management
- Integrates with Excalidraw's app state for navigation and element selection
### Navigation Logic
- For element-associated pins: Uses `scrollToContent()` with element reference
- For coordinate-based pins: Updates viewport scroll position directly
- Smooth animations provided by Excalidraw's built-in navigation system
### Styling
- Follows Excalidraw's design system and CSS variable conventions
- Responsive design with proper overflow handling
- Theme-aware styling for light/dark mode compatibility
## Future Enhancements (Not Implemented)
- Persistence across sessions (would require integration with Excalidraw's data layer)
- Pin categories/grouping
- Import/export pin collections
- Keyboard shortcuts for quick navigation
- Pin thumbnails/previews
## Testing Recommendations
1. Test pin creation with and without selected elements
2. Verify navigation works correctly for both element-based and coordinate-based pins
3. Test UI responsiveness on different screen sizes
4. Verify theme compatibility (light/dark modes)
5. Test localization with different languages (when translations are added)

50
packages/excalidraw/actions/actionIndex.ts

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
import { CaptureUpdateAction } from "@excalidraw/element";
import { register } from "./register";
import type { IndexItem } from "../types";
export const actionAddIndexItem = register({
name: "addIndexItem",
label: "Add index item",
trackEvent: { category: "canvas" },
predicate: () => true,
perform: (elements, appState, formData: IndexItem) => {
return {
appState: {
indexItems: [...appState.indexItems, formData],
},
captureUpdate: CaptureUpdateAction.UPDATE,
};
},
});
export const actionRemoveIndexItem = register({
name: "removeIndexItem",
label: "Remove index item",
trackEvent: { category: "canvas" },
predicate: () => true,
perform: (elements, appState, formData: { id: string }) => {
return {
appState: {
indexItems: appState.indexItems.filter(item => item.id !== formData.id),
},
captureUpdate: CaptureUpdateAction.UPDATE,
};
},
});
export const actionUpdateIndexItem = register({
name: "updateIndexItem",
label: "Update index item",
trackEvent: { category: "canvas" },
predicate: () => true,
perform: (elements, appState, formData: { id: string; updates: Partial<IndexItem> }) => {
return {
appState: {
indexItems: appState.indexItems.map(item =>
item.id === formData.id ? { ...item, ...formData.updates } : item
),
},
captureUpdate: CaptureUpdateAction.UPDATE,
};
},
});

6
packages/excalidraw/actions/index.ts

@ -89,3 +89,9 @@ export { actionToggleLinearEditor } from "./actionLinearEditor"; @@ -89,3 +89,9 @@ export { actionToggleLinearEditor } from "./actionLinearEditor";
export { actionToggleSearchMenu } from "./actionToggleSearchMenu";
export { actionToggleCropEditor } from "./actionCropEditor";
export {
actionAddIndexItem,
actionRemoveIndexItem,
actionUpdateIndexItem,
} from "./actionIndex";

5
packages/excalidraw/actions/types.ts

@ -142,7 +142,10 @@ export type ActionName = @@ -142,7 +142,10 @@ export type ActionName =
| "wrapSelectionInFrame"
| "toggleLassoTool"
| "toggleShapeSwitch"
| "togglePolygon";
| "togglePolygon"
| "addIndexItem"
| "removeIndexItem"
| "updateIndexItem";
export type PanelComponentProps = {
elements: readonly ExcalidrawElement[];

2
packages/excalidraw/appState.ts

@ -128,6 +128,7 @@ export const getDefaultAppState = (): Omit< @@ -128,6 +128,7 @@ export const getDefaultAppState = (): Omit<
lockedMultiSelections: {},
activeLockedId: null,
bindMode: "orbit",
indexItems: [],
};
};
@ -254,6 +255,7 @@ const APP_STATE_STORAGE_CONF = (< @@ -254,6 +255,7 @@ const APP_STATE_STORAGE_CONF = (<
lockedMultiSelections: { browser: true, export: true, server: true },
activeLockedId: { browser: false, export: false, server: false },
bindMode: { browser: true, export: false, server: false },
indexItems: { browser: true, export: true, server: true },
});
const _clearAppStateForStorage = <

44
packages/excalidraw/components/IndexSection/IndexButton.tsx

@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
import React, { useState } from "react";
import { Button } from "../Button";
import { MapPin } from "../icons";
import { IndexSection } from "./IndexSection";
import { t } from "../../i18n";
import type { AppClassProperties, AppState } from "../../types";
interface IndexButtonProps {
app: AppClassProperties;
appState: AppState;
}
export const IndexButton: React.FC<IndexButtonProps> = ({ app, appState }) => {
const [showIndex, setShowIndex] = useState(false);
return (
<>
<Button
type="button"
size="small"
onClick={() => setShowIndex(true)}
title={t("indexSection.title")}
>
<MapPin size={16} />
</Button>
{showIndex && (
<div
style={{
position: "fixed",
top: "80px",
right: "20px",
zIndex: 1000,
}}
>
<IndexSection
app={app}
appState={appState}
onClose={() => setShowIndex(false)}
/>
</div>
)}
</>
);
};

198
packages/excalidraw/components/IndexSection/IndexSection.scss

@ -0,0 +1,198 @@ @@ -0,0 +1,198 @@
.index-section {
width: 280px;
max-height: 400px;
background: var(--island-bg-color);
border: 1px solid var(--default-border-color);
border-radius: var(--space-factor);
box-shadow: var(--shadow-island);
display: flex;
flex-direction: column;
overflow: hidden;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-factor) calc(var(--space-factor) * 1.5);
border-bottom: 1px solid var(--default-border-color);
background: var(--button-gray-1);
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: var(--text-color-primary);
}
button {
padding: 2px 6px;
font-size: 16px;
line-height: 1;
color: var(--text-color-primary);
&:hover {
background: var(--button-gray-2);
}
}
}
&__content {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
&__add-item {
padding: var(--space-factor);
border-bottom: 1px solid var(--default-border-color);
}
&__add-form {
display: flex;
flex-direction: column;
gap: calc(var(--space-factor) * 0.5);
input {
width: 100%;
padding: 6px 8px;
border: 1px solid var(--default-border-color);
border-radius: 4px;
font-size: 12px;
background: var(--input-bg);
color: var(--text-color-primary);
&:focus {
outline: none;
border-color: var(--color-primary);
}
}
}
&__add-buttons {
display: flex;
gap: calc(var(--space-factor) * 0.5);
button {
flex: 1;
padding: 4px 8px;
font-size: 12px;
}
}
&__items {
flex: 1;
overflow-y: auto;
padding: calc(var(--space-factor) * 0.5);
}
&__empty {
padding: calc(var(--space-factor) * 2);
text-align: center;
color: var(--text-color-primary);
p {
margin: 0 0 calc(var(--space-factor) * 0.5) 0;
font-size: 12px;
&.index-section__help {
color: var(--text-color-secondary);
font-size: 11px;
}
}
}
&__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
&__item {
border-radius: 4px;
transition: background-color 0.1s ease;
&:hover {
background: var(--button-gray-1);
}
}
&__item-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 8px;
gap: 8px;
}
&__item-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
&__item-name {
font-size: 12px;
font-weight: 500;
color: var(--text-color-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
&:hover {
background: var(--button-gray-1);
border-radius: 2px;
padding: 1px 2px;
margin: -1px -2px;
}
}
&__item-coords {
font-size: 10px;
color: var(--text-color-secondary);
font-family: var(--font-family-code);
}
&__item-actions {
display: flex;
gap: 2px;
flex-shrink: 0;
button {
padding: 4px;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-color-secondary);
&:hover {
background: var(--button-gray-2);
color: var(--text-color-primary);
}
svg {
display: block;
}
}
}
}
// Dark theme adjustments
.theme--dark {
.index-section {
&__header {
background: var(--button-gray-2);
}
&__item:hover {
background: var(--button-gray-2);
}
}
}

63
packages/excalidraw/components/IndexSection/IndexSection.test.tsx

@ -0,0 +1,63 @@ @@ -0,0 +1,63 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { IndexSection } from "./IndexSection";
import { getDefaultAppState } from "../../appState";
import type { AppClassProperties, IndexItem } from "../../types";
// Mock the actions
jest.mock("../../actions/actionIndex", () => ({
actionAddIndexItem: {
perform: jest.fn(() => ({ captureUpdate: "UPDATE" })),
},
actionRemoveIndexItem: {
perform: jest.fn(() => ({ captureUpdate: "UPDATE" })),
},
actionUpdateIndexItem: {
perform: jest.fn(() => ({ captureUpdate: "UPDATE" })),
},
}));
const mockApp = {
scene: {
getElements: jest.fn(() => []),
getSelectedElements: jest.fn(() => []),
getNonDeletedElementsMap: jest.fn(() => new Map()),
},
syncActionResult: jest.fn(),
} as unknown as AppClassProperties;
const mockAppState = {
...getDefaultAppState(),
width: 800,
height: 600,
scrollX: 0,
scrollY: 0,
indexItems: [] as IndexItem[],
};
describe("IndexSection", () => {
it("renders empty state correctly", () => {
render(
<IndexSection
app={mockApp}
appState={mockAppState}
onClose={jest.fn()}
/>
);
expect(screen.getByText("No pins added yet")).toBeInTheDocument();
});
it("shows add pin form when button is clicked", () => {
render(
<IndexSection
app={mockApp}
appState={mockAppState}
onClose={jest.fn()}
/>
);
fireEvent.click(screen.getByText("Add Pin"));
expect(screen.getByPlaceholderText("Enter pin name...")).toBeInTheDocument();
});
});

311
packages/excalidraw/components/IndexSection/IndexSection.tsx

@ -0,0 +1,311 @@ @@ -0,0 +1,311 @@
import React, { useState, useCallback, useEffect } from "react";
import clsx from "clsx";
import { t } from "../../i18n";
import { Island } from "../Island";
import { Button } from "../Button";
import { TextField } from "../TextField";
import { Trash, MapPin, Eye } from "../icons";
import type { AppClassProperties, AppState, IndexItem } from "../../types";
import type { ExcalidrawElement } from "@excalidraw/element/types";
import { sceneCoordsToViewportCoords } from "@excalidraw/common";
import { CaptureUpdateAction } from "@excalidraw/element";
import { actionAddIndexItem, actionRemoveIndexItem, actionUpdateIndexItem } from "../../actions/actionIndex";
import "./IndexSection.scss";
interface IndexSectionProps {
app: AppClassProperties;
appState: AppState;
onClose: () => void;
}
export const IndexSection: React.FC<IndexSectionProps> = ({
app,
appState,
onClose,
}) => {
const [newItemName, setNewItemName] = useState("");
const [isAddingItem, setIsAddingItem] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [editingName, setEditingName] = useState("");
// Clean up orphaned pins when elements are deleted
useEffect(() => {
const elementsMap = app.scene.getNonDeletedElementsMap();
const orphanedItems = appState.indexItems.filter(
item => item.elementId && !elementsMap.has(item.elementId)
);
orphanedItems.forEach(item => {
app.syncActionResult(actionRemoveIndexItem.perform(
app.scene.getElements(),
appState,
{ id: item.id },
app
));
});
}, [app.scene.getElements(), appState.indexItems, app]);
const addIndexItem = useCallback(() => {
if (!newItemName.trim()) return;
const selectedElements = app.scene.getSelectedElements(appState);
let x = appState.scrollX + appState.width / 2;
let y = appState.scrollY + appState.height / 2;
let elementId: string | undefined;
// If an element is selected, use its position
if (selectedElements.length > 0) {
const element = selectedElements[0];
x = element.x + element.width / 2;
y = element.y + element.height / 2;
elementId = element.id;
}
const newItem: IndexItem = {
id: Date.now().toString(),
name: newItemName.trim(),
x,
y,
elementId,
timestamp: Date.now(),
};
app.syncActionResult(actionAddIndexItem.perform(
app.scene.getElements(),
appState,
newItem,
app
));
setNewItemName("");
setIsAddingItem(false);
}, [newItemName, app, appState]);
const removeIndexItem = useCallback((id: string) => {
app.syncActionResult(actionRemoveIndexItem.perform(
app.scene.getElements(),
appState,
{ id },
app
));
}, [app, appState]);
const startEditing = useCallback((item: IndexItem) => {
setEditingId(item.id);
setEditingName(item.name);
}, []);
const saveEdit = useCallback(() => {
if (!editingId || !editingName.trim()) return;
app.syncActionResult(actionUpdateIndexItem.perform(
app.scene.getElements(),
appState,
{ id: editingId, updates: { name: editingName.trim() } },
app
));
setEditingId(null);
setEditingName("");
}, [editingId, editingName, app, appState]);
const cancelEdit = useCallback(() => {
setEditingId(null);
setEditingName("");
}, []);
const navigateToItem = useCallback((item: IndexItem) => {
// If the item is associated with an element, navigate to it and select it
if (item.elementId) {
const element = app.scene.getNonDeletedElementsMap().get(item.elementId);
if (element) {
app.scrollToContent([element], {
animate: true,
fitToContent: true,
});
app.syncActionResult({
appState: {
selectedElementIds: { [item.elementId]: true },
},
captureUpdate: CaptureUpdateAction.NEVER,
});
return;
}
}
// Navigate to the coordinates by updating the viewport
const centerX = item.x - appState.width / 2;
const centerY = item.y - appState.height / 2;
app.syncActionResult({
appState: {
scrollX: centerX,
scrollY: centerY,
},
captureUpdate: CaptureUpdateAction.NEVER,
});
}, [app, appState.width, appState.height]);
const getCurrentViewportCenter = () => {
return {
x: appState.scrollX + appState.width / 2,
y: appState.scrollY + appState.height / 2,
};
};
return (
<div className="index-section">
<div className="index-section__header">
<h3>{t("indexSection.title")}</h3>
<Button
type="button"
size="small"
onClick={onClose}
title={t("buttons.close")}
>
×
</Button>
</div>
<div className="index-section__content">
<div className="index-section__add-item">
{!isAddingItem ? (
<Button
type="button"
size="small"
onClick={() => setIsAddingItem(true)}
title={t("indexSection.addItem")}
>
<MapPin size={16} />
{t("indexSection.addPin")}
</Button>
) : (
<div className="index-section__add-form">
<TextField
value={newItemName}
onChange={(value) => setNewItemName(value)}
placeholder={t("indexSection.enterName")}
onKeyDown={(e) => {
if (e.key === "Enter") {
addIndexItem();
} else if (e.key === "Escape") {
setIsAddingItem(false);
setNewItemName("");
}
}}
autoFocus
/>
<div className="index-section__add-buttons">
<Button
type="button"
size="small"
onClick={addIndexItem}
disabled={!newItemName.trim()}
>
{t("buttons.add")}
</Button>
<Button
type="button"
size="small"
onClick={() => {
setIsAddingItem(false);
setNewItemName("");
}}
>
{t("buttons.cancel")}
</Button>
</div>
</div>
)}
</div>
<div className="index-section__items">
{appState.indexItems.length === 0 ? (
<div className="index-section__empty">
<p>{t("indexSection.empty")}</p>
<p className="index-section__help">
{t("indexSection.help")}
</p>
</div>
) : (
<ul className="index-section__list">
{appState.indexItems.map((item) => (
<li key={item.id} className="index-section__item">
<div className="index-section__item-content">
<div className="index-section__item-info">
{editingId === item.id ? (
<TextField
value={editingName}
onChange={setEditingName}
onKeyDown={(e) => {
if (e.key === "Enter") {
saveEdit();
} else if (e.key === "Escape") {
cancelEdit();
}
}}
autoFocus
/>
) : (
<>
<span
className="index-section__item-name"
onDoubleClick={() => startEditing(item)}
>
{item.name}
</span>
<span className="index-section__item-coords">
({Math.round(item.x)}, {Math.round(item.y)})
</span>
</>
)}
</div>
<div className="index-section__item-actions">
{editingId === item.id ? (
<>
<Button
type="button"
size="small"
onClick={saveEdit}
disabled={!editingName.trim()}
>
</Button>
<Button
type="button"
size="small"
onClick={cancelEdit}
>
</Button>
</>
) : (
<>
<Button
type="button"
size="small"
onClick={() => navigateToItem(item)}
title={t("indexSection.goTo")}
>
<Eye size={14} />
</Button>
<Button
type="button"
size="small"
onClick={() => removeIndexItem(item.id)}
title={t("buttons.delete")}
>
<Trash size={14} />
</Button>
</>
)}
</div>
</div>
</li>
))}
</ul>
)}
</div>
</div>
</div>
);
};

2
packages/excalidraw/components/IndexSection/index.ts

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
export { IndexSection } from "./IndexSection";
export { IndexButton } from "./IndexButton";

4
packages/excalidraw/components/LayerUI.tsx

@ -51,6 +51,7 @@ import { sidebarRightIcon } from "./icons"; @@ -51,6 +51,7 @@ import { sidebarRightIcon } from "./icons";
import { DefaultSidebar } from "./DefaultSidebar";
import { TTDDialog } from "./TTDDialog/TTDDialog";
import { Stats } from "./Stats";
import { IndexButton } from "./IndexSection";
import ElementLinkDialog from "./ElementLinkDialog";
import { ErrorDialog } from "./ErrorDialog";
import { EyeDropper, activeEyeDropperAtom } from "./EyeDropper";
@ -426,6 +427,9 @@ const LayerUI = ({ @@ -426,6 +427,9 @@ const LayerUI = ({
appState.openSidebar?.name !== DEFAULT_SIDEBAR.name) && (
<tunnels.DefaultSidebarTriggerTunnel.Out />
)}
{!appState.viewModeEnabled && (
<IndexButton app={app} appState={appState} />
)}
{shouldShowStats && (
<Stats
app={app}

30
packages/excalidraw/components/icons.tsx

@ -2373,3 +2373,33 @@ export const presentationIcon = createIcon( @@ -2373,3 +2373,33 @@ export const presentationIcon = createIcon(
</g>,
tablerIconProps,
);
export const MapPin = createIcon(
<g strokeWidth="1.5">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 11a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
<path d="M17.657 16.657l-4.243 4.243a2 2 0 0 1 -2.827 0l-4.244 -4.243a8 8 0 1 1 11.314 0z" />
</g>,
tablerIconProps,
);
export const Eye = createIcon(
<g strokeWidth="1.5">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
<path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" />
</g>,
tablerIconProps,
);
export const Trash = createIcon(
<g strokeWidth="1.5">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M4 7l16 0" />
<path d="M10 11l0 6" />
<path d="M14 11l0 6" />
<path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" />
<path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" />
</g>,
tablerIconProps,
);

13
packages/excalidraw/locales/en.json

@ -210,6 +210,9 @@ @@ -210,6 +210,9 @@
"load": "Open",
"getShareableLink": "Get shareable link",
"close": "Close",
"add": "Add",
"cancel": "Cancel",
"delete": "Delete",
"selectLanguage": "Select language",
"scrollBackToContent": "Scroll back to content",
"zoomIn": "Zoom in",
@ -661,5 +664,15 @@ @@ -661,5 +664,15 @@
"spacebar": "Space",
"delete": "Delete",
"mmb": "Scroll wheel"
},
"indexSection": {
"title": "Index",
"addPin": "Add Pin",
"addItem": "Add index item",
"enterName": "Enter pin name...",
"empty": "No pins added yet",
"help": "Select elements or click 'Add Pin' to create navigation points",
"goTo": "Go to location",
"rename": "Double-click to rename"
}
}

10
packages/excalidraw/types.ts

@ -458,8 +458,18 @@ export interface AppState { @@ -458,8 +458,18 @@ export interface AppState {
// and also remove groupId from this map
lockedMultiSelections: { [groupId: string]: true };
bindMode: BindMode;
indexItems: readonly IndexItem[];
}
export type IndexItem = {
id: string;
name: string;
x: number;
y: number;
elementId?: string;
timestamp: number;
};
export type SearchMatch = {
id: string;
focus: boolean;

3927
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save