Skip to content

Commit 7b4cb58

Browse files
authored
[ENG-872] Add ability to change font style and sizing for dn (#551)
* current progress * add font size and styles and recalculate node size * fix type * change updating condition * make sure migration is there when adding new props to shape * add extra bottom spacing * change default to match pr comment * no resize shape
1 parent 593818d commit 7b4cb58

9 files changed

Lines changed: 199 additions & 64 deletions

File tree

apps/obsidian/src/components/canvas/TldrawViewComponent.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { useCallback, useEffect, useRef, useState } from "react";
22
import {
33
defaultShapeUtils,
4-
DefaultStylePanel,
54
DefaultToolbar,
65
DefaultToolbarContent,
76
ErrorBoundary,
@@ -13,6 +12,7 @@ import {
1312
useTools,
1413
defaultBindingUtils,
1514
TLPointerEventInfo,
15+
DefaultSharePanel,
1616
} from "tldraw";
1717
import "tldraw/tldraw.css";
1818
import {
@@ -408,22 +408,25 @@ export const TldrawPreviewComponent = ({
408408
ContextMenu: (props) => (
409409
<CustomContextMenu canvasFile={file} props={props} />
410410
),
411-
412-
StylePanel: () => {
411+
SharePanel: () => {
413412
const tools = useTools();
414-
const isDiscourseNodeSelected = useIsToolSelected(
413+
const isDiscourseNodeToolSelected = useIsToolSelected(
415414
tools["discourse-node"],
416415
);
417-
const isDiscourseRelationSelected = useIsToolSelected(
416+
const isDiscourseRelationToolSelected = useIsToolSelected(
418417
tools["discourse-relation"],
419418
);
420-
421-
if (!isDiscourseNodeSelected && !isDiscourseRelationSelected) {
422-
return <DefaultStylePanel />;
419+
if (
420+
isDiscourseNodeToolSelected ||
421+
isDiscourseRelationToolSelected
422+
) {
423+
return (
424+
<DiscourseToolPanel plugin={plugin} canvasFile={file} />
425+
);
423426
}
424-
425-
return <DiscourseToolPanel plugin={plugin} canvasFile={file} />;
427+
return <DefaultSharePanel />;
426428
},
429+
427430
OnTheCanvas: () => <ToastListener canvasId={file.path} />,
428431
Toolbar: (props) => {
429432
const tools = useTools();

apps/obsidian/src/components/canvas/overlays/RelationPanel.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,8 @@ export const RelationsPanel = ({
249249
src,
250250
title: file.basename,
251251
nodeTypeId: nodeTypeId,
252+
size: "m",
253+
fontFamily: "sans",
252254
},
253255
};
254256

apps/obsidian/src/components/canvas/shapes/DiscourseNodeShape.tsx

Lines changed: 56 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ import {
77
TLResizeInfo,
88
useEditor,
99
useValue,
10+
DefaultSizeStyle,
11+
DefaultFontStyle,
12+
TLDefaultSizeStyle,
13+
TLDefaultFontStyle,
14+
FONT_SIZES,
15+
FONT_FAMILIES,
1016
} from "tldraw";
1117
import { App, TFile } from "obsidian";
1218
import { memo, createElement, useEffect } from "react";
@@ -34,6 +40,8 @@ export type DiscourseNodeShape = TLBaseShape<
3440
title: string;
3541
nodeTypeId: string;
3642
imageSrc?: string;
43+
size: TLDefaultSizeStyle;
44+
fontFamily: TLDefaultFontStyle;
3745
}
3846
>;
3947

@@ -54,6 +62,8 @@ export class DiscourseNodeUtil extends BaseBoxShapeUtil<DiscourseNodeShape> {
5462
title: T.string.optional(),
5563
nodeTypeId: T.string.nullable().optional(),
5664
imageSrc: T.string.optional(),
65+
size: DefaultSizeStyle,
66+
fontFamily: DefaultFontStyle,
5767
};
5868

5969
getDefaultProps(): DiscourseNodeShape["props"] {
@@ -64,6 +74,8 @@ export class DiscourseNodeUtil extends BaseBoxShapeUtil<DiscourseNodeShape> {
6474
title: "",
6575
nodeTypeId: "",
6676
imageSrc: undefined,
77+
size: "s",
78+
fontFamily: "sans",
6779
};
6880
}
6981

@@ -257,13 +269,11 @@ const discourseNodeContent = memo(
257269
});
258270
}
259271

260-
let didImageChange = false;
261272
let currentImageSrc = shape.props.imageSrc;
262273
if (nodeType?.keyImage) {
263274
const imageSrc = await getFirstImageSrcForFile(app, linkedFile);
264275

265276
if (imageSrc && imageSrc !== shape.props.imageSrc) {
266-
didImageChange = true;
267277
currentImageSrc = imageSrc;
268278
editor.updateShape<DiscourseNodeShape>({
269279
id: shape.id,
@@ -275,7 +285,6 @@ const discourseNodeContent = memo(
275285
});
276286
}
277287
} else if (shape.props.imageSrc) {
278-
didImageChange = true;
279288
currentImageSrc = undefined;
280289
editor.updateShape<DiscourseNodeShape>({
281290
id: shape.id,
@@ -287,28 +296,29 @@ const discourseNodeContent = memo(
287296
});
288297
}
289298

290-
if (didImageChange) {
291-
const { w, h } = await calcDiscourseNodeSize({
292-
title: linkedFile.basename,
293-
nodeTypeId: shape.props.nodeTypeId,
294-
imageSrc: currentImageSrc,
295-
plugin,
299+
// Recalculate size when title, image, font size, or font family changes
300+
const { w, h } = await calcDiscourseNodeSize({
301+
title: linkedFile.basename,
302+
nodeTypeId: shape.props.nodeTypeId,
303+
imageSrc: currentImageSrc,
304+
plugin,
305+
size: shape.props.size ?? "s",
306+
fontFamily: shape.props.fontFamily ?? "draw",
307+
});
308+
// Only update dimensions if they differ significantly (>1px)
309+
if (
310+
Math.abs((shape.props.w || 0) - w) > 1 ||
311+
Math.abs((shape.props.h || 0) - h) > 1
312+
) {
313+
editor.updateShape<DiscourseNodeShape>({
314+
id: shape.id,
315+
type: "discourse-node",
316+
props: {
317+
...shape.props,
318+
w,
319+
h,
320+
},
296321
});
297-
// Only update dimensions if they differ significantly (>1px)
298-
if (
299-
Math.abs((shape.props.w || 0) - w) > 1 ||
300-
Math.abs((shape.props.h || 0) - h) > 1
301-
) {
302-
editor.updateShape<DiscourseNodeShape>({
303-
id: shape.id,
304-
type: "discourse-node",
305-
props: {
306-
...shape.props,
307-
w,
308-
h,
309-
},
310-
});
311-
}
312322
}
313323
} catch (error) {
314324
console.error("Error loading node data", error);
@@ -321,7 +331,7 @@ const discourseNodeContent = memo(
321331
return () => {
322332
return;
323333
};
324-
// Only trigger when content changes, not when dimensions change (to avoid fighting manual resizing)
334+
// Trigger when content changes
325335
// eslint-disable-next-line react-hooks/exhaustive-deps
326336
}, [
327337
src,
@@ -372,6 +382,8 @@ const discourseNodeContent = memo(
372382
});
373383
}
374384
};
385+
const fontSize = FONT_SIZES[shape.props.size];
386+
const fontFamily = FONT_FAMILIES[shape.props.fontFamily];
375387

376388
return (
377389
<div
@@ -418,6 +430,7 @@ const discourseNodeContent = memo(
418430
</svg>
419431
</button>
420432
)}
433+
421434
{shape.props.imageSrc ? (
422435
<div className="mt-2 flex min-h-0 w-full flex-1 items-center justify-center overflow-hidden">
423436
<img
@@ -429,8 +442,24 @@ const discourseNodeContent = memo(
429442
/>
430443
</div>
431444
) : null}
432-
<h1 className="m-0 pt-4 text-base">{title || "..."}</h1>
433-
<p className="m-0 text-sm opacity-80">{nodeType?.name || ""}</p>
445+
<h1
446+
className="m-1"
447+
style={{
448+
fontSize: `${fontSize}px`,
449+
fontFamily,
450+
}}
451+
>
452+
{title || "..."}
453+
</h1>
454+
<p
455+
className="m-0 opacity-80"
456+
style={{
457+
fontSize: `${fontSize * 0.75}px`,
458+
fontFamily,
459+
}}
460+
>
461+
{nodeType?.name || ""}
462+
</p>
434463
</div>
435464
);
436465
},
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/* eslint-disable @typescript-eslint/no-unsafe-call */
2+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
3+
/* eslint-disable @typescript-eslint/no-unsafe-return */
4+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
5+
/* eslint-disable @typescript-eslint/no-explicit-any */
6+
import {
7+
createMigrationSequence,
8+
createMigrationIds,
9+
} from "tldraw";
10+
11+
const SEQUENCE_ID_BASE = "com.discourse-graph.obsidian.discourse-node";
12+
13+
const versions = createMigrationIds(`${SEQUENCE_ID_BASE}`, {
14+
addSizeAndFontFamily: 1,
15+
});
16+
17+
export const discourseNodeMigrations = createMigrationSequence({
18+
sequenceId: `${SEQUENCE_ID_BASE}`,
19+
sequence: [
20+
{
21+
id: versions["addSizeAndFontFamily"],
22+
scope: "record",
23+
filter: (r: any) =>
24+
r.typeName === "shape" && r.type === "discourse-node",
25+
up: (shape: any) => {
26+
// Only add defaults if they don't already exist
27+
if (shape.props.size === undefined) {
28+
shape.props.size = "s";
29+
}
30+
if (shape.props.fontFamily === undefined) {
31+
shape.props.fontFamily = "draw";
32+
}
33+
},
34+
},
35+
],
36+
});
37+

apps/obsidian/src/components/canvas/shapes/nodeConstants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ export const FONT_FAMILY =
3737
// Maximum height for key images
3838
export const MAX_IMAGE_HEIGHT = 250;
3939

40+
export const EXTRA_BOTTOM_SPACING = 12;
41+
4042
// Gap between image and text
4143
export const IMAGE_GAP = 4;
4244

apps/obsidian/src/components/canvas/utils/tldraw.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
TldrawFile,
77
TLRecord,
88
TLStore,
9+
loadSnapshot,
10+
TLStoreSnapshot,
911
} from "tldraw";
1012
import {
1113
FRONTMATTER_KEY,
@@ -24,6 +26,7 @@ import {
2426
} from "~/components/canvas/shapes/DiscourseNodeShape";
2527
import { DiscourseRelationUtil } from "~/components/canvas/shapes/DiscourseRelationShape";
2628
import { DiscourseRelationBindingUtil } from "~/components/canvas/shapes/DiscourseRelationBinding";
29+
import { discourseNodeMigrations } from "~/components/canvas/shapes/discourseNodeMigrations";
2730

2831
export type TldrawPluginMetaData = {
2932
/* eslint-disable @typescript-eslint/naming-convention */
@@ -70,20 +73,26 @@ export const processInitialData = (
7073
) as SerializedStore<TLRecord>)
7174
: (data.raw.records as SerializedStore<TLRecord>);
7275

73-
let store: TLStore;
76+
// Create store first (this creates the schema with migrations)
77+
const store = createTLStore({
78+
shapeUtils: customShapeUtils,
79+
bindingUtils: [...defaultBindingUtils, DiscourseRelationBindingUtil],
80+
assets: assetStore,
81+
migrations: [discourseNodeMigrations],
82+
});
83+
7484
if (recordsData) {
75-
store = createTLStore({
76-
shapeUtils: customShapeUtils,
77-
bindingUtils: [...defaultBindingUtils, DiscourseRelationBindingUtil],
78-
initialData: recordsData,
79-
assets: assetStore,
80-
});
81-
} else {
82-
store = createTLStore({
83-
shapeUtils: customShapeUtils,
84-
bindingUtils: [...defaultBindingUtils, DiscourseRelationBindingUtil],
85-
assets: assetStore,
86-
});
85+
// Create a snapshot with the old schema (if available) or use current schema
86+
// The schema from data.raw is typed as any because it's legacy data format
87+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
88+
const oldSchema = data.raw.schema ?? store.schema.serialize();
89+
const snapshot: TLStoreSnapshot = {
90+
store: recordsData,
91+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
92+
schema: oldSchema,
93+
};
94+
95+
loadSnapshot(store, snapshot, { forceOverwriteSessionState: true });
8796
}
8897

8998
return {

apps/obsidian/src/constants.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { TLDefaultSizeStyle } from "tldraw";
12
import { DiscourseNode, DiscourseRelationType, Settings } from "~/types";
23
import generateUid from "~/utils/generateUid";
34

@@ -80,4 +81,33 @@ export const VIEW_TYPE_MARKDOWN = "markdown";
8081
export const VIEW_TYPE_TLDRAW_DG_PREVIEW = "tldraw-dg-preview";
8182

8283
export const TLDRAW_VERSION = "3.14.2";
83-
export const DEFAULT_SAVE_DELAY = 500; // in ms
84+
export const DEFAULT_SAVE_DELAY = 500; // in ms
85+
86+
// TODO REPLACE WITH TLDRAW DEFAULTS
87+
// https://github.com/tldraw/tldraw/pull/1580/files
88+
export const TEXT_PROPS = {
89+
lineHeight: 1.35,
90+
fontWeight: "normal",
91+
fontVariant: "normal",
92+
fontStyle: "normal",
93+
padding: "0px",
94+
maxWidth: "auto",
95+
};
96+
export const FONT_SIZES: Record<TLDefaultSizeStyle, number> = {
97+
m: 25,
98+
l: 38,
99+
xl: 48,
100+
s: 16,
101+
};
102+
// // FONT_FAMILIES.sans or tldraw_sans not working in toSvg()
103+
// // maybe check getSvg()
104+
// // in node_modules\@tldraw\tldraw\node_modules\@tldraw\editor\dist\cjs\lib\app\App.js
105+
// const SVG_FONT_FAMILY = `"Inter", "sans-serif"`;
106+
107+
export const DEFAULT_STYLE_PROPS = {
108+
...TEXT_PROPS,
109+
fontSize: 16,
110+
fontFamily: "'Inter', sans-serif",
111+
width: "fit-content",
112+
padding: "40px",
113+
};

0 commit comments

Comments
 (0)