Skip to content

Commit ed848f3

Browse files
committed
font size slider
1 parent f0a143c commit ed848f3

8 files changed

Lines changed: 193 additions & 52 deletions

File tree

‎src/App.css‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2125,6 +2125,25 @@
21252125
padding-bottom: calc(4px + env(safe-area-inset-bottom, 0px));
21262126
}
21272127

2128+
.statusbar-font {
2129+
display: inline-flex;
2130+
align-items: center;
2131+
gap: 6px;
2132+
min-width: 0;
2133+
color: var(--text);
2134+
}
2135+
2136+
.statusbar-font-slider {
2137+
width: 72px;
2138+
accent-color: var(--accent);
2139+
}
2140+
2141+
.statusbar-font-value {
2142+
min-width: 36px;
2143+
color: var(--text-h);
2144+
text-align: right;
2145+
}
2146+
21282147
.statusbar-tool {
21292148
margin-left: auto;
21302149
color: var(--text-h);

‎src/components/ExportModal.tsx‎

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { useCallback, useEffect, useRef, useState } from 'react'
22
import { Download, Minus, Plus, RotateCcw, X } from 'lucide-react'
33
import { useAppStore } from '../store'
4+
import {
5+
ANNOTATION_TEXT_SCALE_MAX,
6+
ANNOTATION_TEXT_SCALE_MIN,
7+
ANNOTATION_TEXT_SCALE_STEP,
8+
annotationTextScaleMultiplier,
9+
} from '../utils/annotationTextScale'
410
import {
511
defaultIncludeSidebar,
612
downloadExport,
@@ -9,18 +15,10 @@ import {
915
} from '../utils/export'
1016
import type { LayoutMode } from '../hooks/useLayoutMode'
1117

12-
const SCALE_MIN = 50
13-
const SCALE_MAX = 200
14-
const SCALE_STEP = 5
15-
1618
const PREVIEW_ZOOM_MIN = 1
1719
const PREVIEW_ZOOM_MAX = 4
1820
const PREVIEW_ZOOM_STEP = 0.25
1921

20-
function scaleToMultiplier(percent: number): number {
21-
return percent / 100
22-
}
23-
2422
export default function ExportModal({
2523
layoutMode,
2624
onClose,
@@ -31,10 +29,13 @@ export default function ExportModal({
3129
const annotations = useAppStore((s) => s.annotations)
3230
const markerDisplayMode = useAppStore((s) => s.markerDisplayMode)
3331

32+
const textScalePct = useAppStore((s) => s.annotationTextScalePct)
33+
const setAnnotationTextScalePct = useAppStore(
34+
(s) => s.setAnnotationTextScalePct,
35+
)
3436
const [includeSidebar, setIncludeSidebar] = useState(() =>
3537
defaultIncludeSidebar(markerDisplayMode, annotations.length),
3638
)
37-
const [textScalePct, setTextScalePct] = useState(100)
3839
const [previews, setPreviews] = useState<ExportPreviewImages | null>(null)
3940
const [previewLoading, setPreviewLoading] = useState(true)
4041
const [exporting, setExporting] = useState(false)
@@ -51,7 +52,7 @@ export default function ExportModal({
5152
panY: number
5253
} | null>(null)
5354

54-
const textScale = scaleToMultiplier(textScalePct)
55+
const textScale = annotationTextScaleMultiplier(textScalePct)
5556

5657
const refreshPreview = useCallback(async () => {
5758
setPreviewLoading(true)
@@ -359,12 +360,14 @@ export default function ExportModal({
359360
<span className="export-slider-label">Text size</span>
360361
<input
361362
type="range"
362-
min={SCALE_MIN}
363-
max={SCALE_MAX}
364-
step={SCALE_STEP}
363+
min={ANNOTATION_TEXT_SCALE_MIN}
364+
max={ANNOTATION_TEXT_SCALE_MAX}
365+
step={ANNOTATION_TEXT_SCALE_STEP}
365366
value={textScalePct}
366367
aria-label="Text size"
367-
onChange={(e) => setTextScalePct(Number(e.target.value))}
368+
onChange={(e) =>
369+
setAnnotationTextScalePct(Number(e.target.value))
370+
}
368371
/>
369372
<span className="export-slider-value">{textScalePct}%</span>
370373
</label>

‎src/components/StatusBar.tsx‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import { getToolLabel } from '../config/tools'
44
import type { LegalDocumentId } from '../content/legal'
55
import type { LayoutMode } from '../hooks/useLayoutMode'
66
import { useAppStore } from '../store'
7+
import {
8+
ANNOTATION_TEXT_SCALE_MAX,
9+
ANNOTATION_TEXT_SCALE_MIN,
10+
ANNOTATION_TEXT_SCALE_STEP,
11+
} from '../utils/annotationTextScale'
712
import { stageRef } from '../stageRef'
813
import CanvasRotationControl from './CanvasRotationControl'
914
import LegalModal from './LegalModal'
@@ -21,6 +26,10 @@ export default function StatusBar({
2126
const scale = useAppStore((s) => s.viewport.scale)
2227
const setViewport = useAppStore((s) => s.setViewport)
2328
const pointer = useAppStore((s) => s.pointer)
29+
const textScalePct = useAppStore((s) => s.annotationTextScalePct)
30+
const setAnnotationTextScalePct = useAppStore(
31+
(s) => s.setAnnotationTextScalePct,
32+
)
2433
const [legalDoc, setLegalDoc] = useState<LegalDocumentId | null>(null)
2534

2635
const showCoords = layoutMode === 'desktop'
@@ -74,6 +83,24 @@ export default function StatusBar({
7483
</button>
7584
</span>
7685
)}
86+
<label className="statusbar-font">
87+
<span>Font</span>
88+
<input
89+
className="statusbar-font-slider"
90+
type="range"
91+
min={ANNOTATION_TEXT_SCALE_MIN}
92+
max={ANNOTATION_TEXT_SCALE_MAX}
93+
step={ANNOTATION_TEXT_SCALE_STEP}
94+
value={textScalePct}
95+
aria-label="Font size"
96+
aria-valuemin={ANNOTATION_TEXT_SCALE_MIN}
97+
aria-valuemax={ANNOTATION_TEXT_SCALE_MAX}
98+
aria-valuenow={textScalePct}
99+
aria-valuetext={`${textScalePct}%`}
100+
onChange={(e) => setAnnotationTextScalePct(Number(e.target.value))}
101+
/>
102+
<span className="statusbar-font-value">{textScalePct}%</span>
103+
</label>
77104
{showCoords && (
78105
<span>
79106
{pointer

‎src/components/canvas/MarkersLayer.tsx‎

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
44
import { useAppStore } from '../../store'
55
import { areExportMarkersSuppressed } from '../../utils/export'
66
import { stageRef } from '../../stageRef'
7+
import { annotationTextScaleMultiplier } from '../../utils/annotationTextScale'
78
import { markerLabel } from '../../utils/labels'
89
import {
910
COMMENT_FONT_SIZE,
1011
COMMENT_LINE_HEIGHT,
1112
COMMENT_MAX_TEXT_WIDTH,
1213
COMMENT_PADDING,
1314
clampCommentBoxWidth,
15+
commentLabelBadgeLayout,
1416
measureCommentBox,
1517
textWidthFromRightEdge,
1618
} from '../../utils/markerComments'
@@ -47,19 +49,25 @@ function resolveTextWidth(
4749

4850
function measureForAnnotation(
4951
annotation: Annotation,
52+
textScale: number,
5053
previewTextWidth?: number,
5154
) {
5255
const text = commentBodyText(annotation)
5356
const hasExplicitWidth =
5457
previewTextWidth !== undefined || annotation.commentBoxWidth !== undefined
55-
const maxTextWidth = resolveTextWidth(annotation, previewTextWidth)
58+
const storedTextWidth = resolveTextWidth(annotation, previewTextWidth)
59+
const fontSize = COMMENT_FONT_SIZE * textScale
60+
const padding = COMMENT_PADDING * textScale
61+
const maxTextWidth = storedTextWidth * textScale
5662

5763
return {
5864
isPlaceholder: annotation.comment.trim() === '',
59-
...measureCommentBox(text, maxTextWidth, COMMENT_FONT_SIZE, COMMENT_PADDING, {
65+
...measureCommentBox(text, maxTextWidth, fontSize, padding, {
6066
shrinkToContent: !hasExplicitWidth,
6167
}),
6268
textWidth: maxTextWidth,
69+
fontSize,
70+
padding,
6371
}
6472
}
6573

@@ -68,26 +76,27 @@ function CommentLabelBadge({
6876
color,
6977
boxWidth,
7078
boxHeight,
79+
textScale,
7180
}: {
7281
label: string
7382
color: string
7483
boxWidth: number
7584
boxHeight: number
85+
textScale: number
7686
}) {
77-
const badgeRadius = label.length === 1 ? 5 : 5.5
78-
const badgeInset = 2
79-
const x = boxWidth / 2 - badgeRadius - badgeInset
80-
const y = boxHeight / 2 - badgeRadius - badgeInset
87+
const badge = commentLabelBadgeLayout(label, textScale)
88+
const x = -boxWidth / 2 + badge.offsetX
89+
const y = -boxHeight / 2 + badge.offsetY
8190

8291
return (
8392
<>
8493
<Circle
8594
x={x}
8695
y={y}
87-
radius={badgeRadius}
96+
radius={badge.radius}
8897
fill={color}
8998
stroke="#ffffff"
90-
strokeWidth={0.75}
99+
strokeWidth={0.6 * textScale}
91100
listening={false}
92101
/>
93102
<Text
@@ -96,11 +105,11 @@ function CommentLabelBadge({
96105
y={y}
97106
fill="#ffffff"
98107
fontStyle="bold"
99-
fontSize={label.length === 1 ? 6 : 4.5}
100-
width={badgeRadius * 2}
101-
height={badgeRadius * 2}
102-
offsetX={badgeRadius}
103-
offsetY={badgeRadius}
108+
fontSize={badge.fontSize}
109+
width={badge.radius * 2}
110+
height={badge.radius * 2}
111+
offsetX={badge.radius}
112+
offsetY={badge.radius}
104113
align="center"
105114
verticalAlign="middle"
106115
listening={false}
@@ -114,6 +123,7 @@ function CommentBox({
114123
index,
115124
selected,
116125
previewTextWidth,
126+
textScale,
117127
viewportScale,
118128
layoutMode,
119129
onResizePointerDown,
@@ -122,14 +132,16 @@ function CommentBox({
122132
index: number
123133
selected: boolean
124134
previewTextWidth?: number
135+
textScale: number
125136
viewportScale: number
126137
layoutMode: LayoutMode
127138
onResizePointerDown: (e: KonvaEventObject<PointerEvent>) => void
128139
}) {
129-
const { width, height, lineText, textWidth, isPlaceholder } = useMemo(
130-
() => measureForAnnotation(annotation, previewTextWidth),
131-
[annotation, previewTextWidth],
132-
)
140+
const { width, height, lineText, textWidth, isPlaceholder, fontSize, padding } =
141+
useMemo(
142+
() => measureForAnnotation(annotation, textScale, previewTextWidth),
143+
[annotation, previewTextWidth, textScale],
144+
)
133145
const gripWidth = Math.max(10 / viewportScale, 6)
134146
const label = markerLabel(index)
135147
const isTouch = layoutMode === 'touch'
@@ -146,19 +158,19 @@ function CommentBox({
146158
fill="rgba(255, 255, 255, 0.94)"
147159
stroke={selected ? '#08060d' : '#c4c4cc'}
148160
strokeWidth={selected ? 1.5 : 1}
149-
cornerRadius={4}
161+
cornerRadius={4 * textScale}
150162
shadowColor={useShadow ? '#000000' : undefined}
151163
shadowBlur={useShadow ? 4 : 0}
152164
shadowOpacity={useShadow ? 0.22 : 0}
153165
shadowOffsetY={useShadow ? 1 : 0}
154166
hitStrokeWidth={hitPadding}
155167
/>
156168
<Text
157-
x={-width / 2 + COMMENT_PADDING}
158-
y={-height / 2 + COMMENT_PADDING}
169+
x={-width / 2 + padding}
170+
y={-height / 2 + padding}
159171
text={lineText}
160172
width={textWidth}
161-
fontSize={COMMENT_FONT_SIZE}
173+
fontSize={fontSize}
162174
lineHeight={COMMENT_LINE_HEIGHT}
163175
fill={isPlaceholder ? '#71717a' : '#18181b'}
164176
fontStyle={isPlaceholder ? 'italic' : 'normal'}
@@ -169,6 +181,7 @@ function CommentBox({
169181
color={annotation.color}
170182
boxWidth={width}
171183
boxHeight={height}
184+
textScale={textScale}
172185
/>
173186
{selected && (
174187
<>
@@ -211,6 +224,7 @@ const MarkerNode = memo(function MarkerNode({
211224
selectable,
212225
showComments,
213226
previewTextWidth,
227+
textScale,
214228
viewportScale,
215229
layoutMode,
216230
hitMultiplier,
@@ -223,6 +237,7 @@ const MarkerNode = memo(function MarkerNode({
223237
selectable: boolean
224238
showComments: boolean
225239
previewTextWidth?: number
240+
textScale: number
226241
viewportScale: number
227242
layoutMode: LayoutMode
228243
hitMultiplier: number
@@ -262,6 +277,7 @@ const MarkerNode = memo(function MarkerNode({
262277
index={index}
263278
selected={selectable && selected}
264279
previewTextWidth={previewTextWidth}
280+
textScale={textScale}
265281
viewportScale={viewportScale}
266282
layoutMode={layoutMode}
267283
onResizePointerDown={(e) => onResizePointerDown(e, annotation)}
@@ -310,6 +326,9 @@ function MarkersLayer({
310326
const selectedId = useAppStore((s) => s.selectedId)
311327
const markerDisplayMode = useAppStore((s) => s.markerDisplayMode)
312328
const viewportScale = useAppStore((s) => s.viewport.scale)
329+
const textScale = useAppStore((s) =>
330+
annotationTextScaleMultiplier(s.annotationTextScalePct),
331+
)
313332
const updateAnnotation = useAppStore((s) => s.updateAnnotation)
314333

315334
const selectable = activeTool === 'select'
@@ -336,7 +355,7 @@ function MarkersLayer({
336355
if (!pos) return
337356

338357
const localRightEdge = pos.x - annotation.position.x
339-
const textWidth = textWidthFromRightEdge(localRightEdge)
358+
const textWidth = textWidthFromRightEdge(localRightEdge, textScale)
340359
setPreviewWidths((current) => ({ ...current, [resizingId]: textWidth }))
341360
}
342361

@@ -362,7 +381,7 @@ function MarkersLayer({
362381
window.removeEventListener('pointerup', onPointerUp)
363382
window.removeEventListener('pointercancel', onPointerUp)
364383
}
365-
}, [annotations, resizingId, updateAnnotation])
384+
}, [annotations, resizingId, textScale, updateAnnotation])
366385

367386
const beginResize = useCallback(
368387
(e: KonvaEventObject<PointerEvent>, annotation: Annotation) => {
@@ -375,7 +394,10 @@ function MarkersLayer({
375394
stage.setPointersPositions(e.evt)
376395
const pos = getMapPointer(stage)
377396
if (pos) {
378-
startWidth = textWidthFromRightEdge(pos.x - annotation.position.x)
397+
startWidth = textWidthFromRightEdge(
398+
pos.x - annotation.position.x,
399+
textScale,
400+
)
379401
}
380402
}
381403

@@ -385,7 +407,7 @@ function MarkersLayer({
385407
}))
386408
setResizingId(annotation.id)
387409
},
388-
[],
410+
[textScale],
389411
)
390412

391413
if (areExportMarkersSuppressed()) {
@@ -404,6 +426,7 @@ function MarkersLayer({
404426
selectable={selectable}
405427
showComments={showComments}
406428
previewTextWidth={previewWidths[annotation.id]}
429+
textScale={textScale}
407430
viewportScale={viewportScale}
408431
layoutMode={layoutMode}
409432
hitMultiplier={hitMultiplier}

0 commit comments

Comments
 (0)