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
26 changes: 23 additions & 3 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -396,18 +396,18 @@

.app-main {
display: grid;
grid-template-columns: 56px 1fr 300px;
grid-template-columns: var(--palette-width) 1fr 300px;
flex: 1;
min-height: 0;
overflow: hidden;
}

.app-shell[data-layout='desktop'] .app-main {
grid-template-columns: 56px 1fr 300px;
grid-template-columns: var(--palette-width) 1fr 300px;
}

.app-shell[data-layout='compact'] .app-main {
grid-template-columns: 56px 1fr;
grid-template-columns: var(--palette-width) 1fr;
}

.app-shell[data-layout='touch'] .app-main {
Expand Down Expand Up @@ -2601,3 +2601,23 @@ a.statusbar-legal-link {
min-width: 0;
flex: 1;
}

/* The palette keeps a fixed width so the canvas never sits underneath it.
Longer translated labels are truncated with an ellipsis (full text is in the tooltip). */
.tool-palette {
box-sizing: border-box;
flex: 0 0 var(--palette-width);
width: var(--palette-width);
min-width: 0;
max-width: var(--palette-width);
overflow: hidden;
}

.tool-palette .tool-label {
display: block;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
35 changes: 20 additions & 15 deletions src/components/AnnotationsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Tooltip from './Tooltip'
import BottomSheet from './BottomSheet'
import SlideDrawer from './SlideDrawer'
import AnnotationAdvertisement from './AnnotationAdvertisement'
import { useT } from '../i18n'

function AnnotationItem({
annotation,
Expand All @@ -23,6 +24,7 @@ function AnnotationItem({
selected: boolean
layoutMode: LayoutMode
}) {
const t = useT()
const setSelectedId = useAppStore((s) => s.setSelectedId)
const updateAnnotation = useAppStore((s) => s.updateAnnotation)
const removeAnnotation = useAppStore((s) => s.removeAnnotation)
Expand Down Expand Up @@ -60,7 +62,7 @@ function AnnotationItem({
<button
type="button"
className="annotation-delete"
aria-label={`Delete annotation ${markerLabel(index)}`}
aria-label={t('notes.deleteAria', { label: markerLabel(index) })}
onClick={(e) => {
e.stopPropagation()
removeAnnotation(annotation.id)
Expand All @@ -87,7 +89,7 @@ function AnnotationItem({
{layoutMode === 'touch' ? (
markerBadge
) : (
<Tooltip content="Click to center on the map" side="left">
<Tooltip content={t('notes.centerHint')} side="left">
{markerBadge}
</Tooltip>
)}
Expand All @@ -96,7 +98,7 @@ function AnnotationItem({
<textarea
ref={commentInputRef}
className="annotation-comment-input"
placeholder="Add a comment…"
placeholder={t('notes.commentPlaceholder')}
rows={1}
value={annotation.comment}
onClick={(e) => e.stopPropagation()}
Expand All @@ -108,7 +110,7 @@ function AnnotationItem({
{layoutMode === 'touch' ? (
deleteButton
) : (
<Tooltip content="Remove this marker">{deleteButton}</Tooltip>
<Tooltip content={t('notes.removeHint')}>{deleteButton}</Tooltip>
)}
</div>
</div>
Expand All @@ -117,6 +119,7 @@ function AnnotationItem({
}

function MarkerDisplayToggle() {
const t = useT()
const markerDisplayMode = useAppStore((s) => s.markerDisplayMode)
const setMarkerDisplayMode = useAppStore((s) => s.setMarkerDisplayMode)

Expand All @@ -127,7 +130,7 @@ function MarkerDisplayToggle() {
aria-pressed={markerDisplayMode === 'labels'}
onClick={() => setMarkerDisplayMode('labels')}
>
Labels
{t('notes.labels')}
</button>
)

Expand All @@ -138,21 +141,21 @@ function MarkerDisplayToggle() {
aria-pressed={markerDisplayMode === 'comments'}
onClick={() => setMarkerDisplayMode('comments')}
>
Comments
{t('notes.comments')}
</button>
)

return (
<div
className="marker-display-toggle"
role="group"
aria-label="Map marker display"
aria-label={t('notes.displayAria')}
>
<Tooltip content="Show letter labels on map markers" side="bottom">
<Tooltip content={t('notes.labelsHint')} side="bottom">
{labelsButton}
</Tooltip>
<Tooltip
content="Show inline comments on the map. Drag the right edge of a selected comment to change its width."
content={t('notes.commentsHint')}
side="bottom"
>
{commentsButton}
Expand All @@ -166,6 +169,7 @@ function AnnotationsContent({
}: {
layoutMode: LayoutMode
}) {
const t = useT()
const annotations = useAppStore((s) => s.annotations)
const selectedId = useAppStore((s) => s.selectedId)

Expand All @@ -174,14 +178,14 @@ function AnnotationsContent({
<div className="panel-header">
<div className="panel-header-title">
<MessageSquareText size={16} aria-hidden />
<h2>Annotations</h2>
<h2>{t('notes.heading')}</h2>
</div>
<MarkerDisplayToggle />
</div>

{annotations.length === 0 ? (
<p className="panel-empty">
No annotations yet. Use the marker tool to add comments to the map.
{t('notes.empty')}
</p>
) : (
<ul className="annotation-list">
Expand All @@ -207,6 +211,7 @@ export default function AnnotationsPanel({
variant: 'sidebar' | 'drawer' | 'sheet'
layoutMode: LayoutMode
}) {
const t = useT()
const annotations = useAppStore((s) => s.annotations)
const annotationsOpen = useAppStore((s) => s.annotationsOpen)
const toggleAnnotations = useAppStore((s) => s.toggleAnnotations)
Expand All @@ -229,18 +234,18 @@ export default function AnnotationsPanel({
<button
type="button"
className="annotation-toggle"
aria-label="Toggle annotations"
aria-label={t('notes.toggleAria')}
aria-expanded={annotationsOpen}
onClick={toggleAnnotations}
>
<MessageSquareText size={16} aria-hidden />
<span>Notes</span>
<span>{t('notes.toggle')}</span>
{annotations.length > 0 && (
<span className="annotation-toggle-badge">{annotations.length}</span>
)}
</button>
<SlideDrawer
title="Annotations"
title={t('notes.heading')}
open={annotationsOpen}
onClose={closeAnnotations}
>
Expand All @@ -259,7 +264,7 @@ export default function AnnotationsPanel({

return (
<BottomSheet
title="Annotations"
title={t('notes.heading')}
onClose={closeAnnotations}
className="bottom-sheet-full"
>
Expand Down
10 changes: 4 additions & 6 deletions src/components/AppErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Component, type ErrorInfo, type ReactNode } from 'react'
import { t } from '../i18n'

interface Props {
children: ReactNode
Expand Down Expand Up @@ -29,17 +30,14 @@ export default class AppErrorBoundary extends Component<Props, State> {
return (
<div className="app-error-boundary" role="alert">
<div className="app-error-boundary-card">
<h2>Something went wrong</h2>
<p>
O-Analysis ran into an unexpected error. Reloading the page usually
fixes this.
</p>
<h2>{t('error.title')}</h2>
<p>{t('error.body')}</p>
<button
type="button"
className="app-error-boundary-reload"
onClick={this.handleReload}
>
Reload page
{t('error.reload')}
</button>
</div>
</div>
Expand Down
4 changes: 3 additions & 1 deletion src/components/BottomSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useId, useRef } from 'react'
import { X } from 'lucide-react'
import { useEscapeKey } from '../hooks/useEscapeKey'
import { useT } from '../i18n'

export default function BottomSheet({
title,
Expand All @@ -13,6 +14,7 @@ export default function BottomSheet({
children: React.ReactNode
className?: string
}) {
const t = useT()
const titleId = useId()
const sheetRef = useRef<HTMLDivElement>(null)

Expand Down Expand Up @@ -52,7 +54,7 @@ export default function BottomSheet({
<button
type="button"
className="bottom-sheet-close"
aria-label="Close"
aria-label={t('common.close')}
onClick={onClose}
>
<X size={18} aria-hidden />
Expand Down
22 changes: 13 additions & 9 deletions src/components/BottomToolBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { useAppStore } from '../store'
import BottomSheet from './BottomSheet'
import StrokeSettings from './StrokeSettings'
import CanvasRotationControl from './CanvasRotationControl'
import { useT } from '../i18n'
import TextSizeSlider from './TextSizeSlider'
import LanguageSelect from './LanguageSelect'

export default function BottomToolBar() {
const activeTool = useAppStore((s) => s.activeTool)
Expand All @@ -16,57 +18,59 @@ export default function BottomToolBar() {
const toggleAnnotations = useAppStore((s) => s.toggleAnnotations)

const [settingsOpen, setSettingsOpen] = useState(false)
const t = useT()

return (
<>
<nav className="bottom-toolbar" aria-label="Drawing tools">
<nav className="bottom-toolbar" aria-label={t('toolbar.drawingTools')}>
<div className="bottom-toolbar-tools" role="toolbar">
{TOOLS.map(({ id, label, shortLabel, icon: Icon }) => (
<button
key={id}
type="button"
className={`bottom-tool-button${activeTool === id ? ' active' : ''}`}
aria-label={label}
aria-label={t(label)}
aria-pressed={activeTool === id}
title={getToolTip(id, 'touch')}
disabled={id === 'gpx' && !hasTracks}
onClick={() => setActiveTool(id)}
>
<Icon size={20} aria-hidden />
<span className="bottom-tool-label">{shortLabel}</span>
<span className="bottom-tool-label">{t(shortLabel)}</span>
</button>
))}
</div>

<button
type="button"
className="bottom-tool-button bottom-tool-extra"
aria-label="Stroke settings"
aria-label={t('toolbar.strokeSettings')}
onClick={() => setSettingsOpen(true)}
>
<Settings size={20} aria-hidden />
<span className="bottom-tool-label">Style</span>
<span className="bottom-tool-label">{t('toolbar.style')}</span>
</button>

<button
type="button"
className={`bottom-tool-button bottom-tool-extra${annotationsOpen ? ' active' : ''}`}
aria-label="Annotations"
aria-label={t('toolbar.annotations')}
aria-pressed={annotationsOpen}
onClick={toggleAnnotations}
>
<MessageSquareText size={20} aria-hidden />
<span className="bottom-tool-label">Notes</span>
<span className="bottom-tool-label">{t('toolbar.notes')}</span>
{annotations.length > 0 && (
<span className="bottom-tool-badge">{annotations.length}</span>
)}
</button>
</nav>

{settingsOpen && (
<BottomSheet title="View & stroke settings" onClose={() => setSettingsOpen(false)}>
<BottomSheet title={t('toolbar.viewAndStroke')} onClose={() => setSettingsOpen(false)}>
<CanvasRotationControl compact />
<TextSizeSlider label="Comment text size" className="style-sheet-text-size" />
<LanguageSelect className="style-sheet-language" />
<TextSizeSlider label={t('style.commentTextSize')} className="style-sheet-text-size" />
<StrokeSettings layoutMode="touch" />
</BottomSheet>
)}
Expand Down
12 changes: 6 additions & 6 deletions src/components/CanvasArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from '../utils/viewport'
import { getMapPointer } from '../utils/mapPointer'
import type { Point, Viewport } from '../types'
import { useT } from '../i18n'

const ZOOM_FACTOR = 1.06
const WHEEL_COMMIT_MS = 120
Expand All @@ -38,6 +39,7 @@ export default function CanvasArea({
}: {
layoutMode: LayoutMode
}) {
const t = useT()
const activeTool = useAppStore((s) => s.activeTool)
const mapImage = useAppStore((s) => s.mapImage)
const setViewport = useAppStore((s) => s.setViewport)
Expand Down Expand Up @@ -618,11 +620,9 @@ export default function CanvasArea({
<div className="canvas-empty">
<div className="canvas-empty-card">
<ImagePlus size={40} aria-hidden />
<h2>No map loaded</h2>
<h2>{t('canvas.noMap')}</h2>
<p>
{isTouch
? 'Import a map image to get started.'
: 'Import a map image to get started, or drop a file here.'}
{isTouch ? t('canvas.importHintTouch') : t('canvas.importHintDrop')}
</p>
<input
ref={mapInputRef}
Expand All @@ -643,15 +643,15 @@ export default function CanvasArea({
onClick={() => mapInputRef.current?.click()}
>
<ImagePlus size={14} aria-hidden />
Import Map
{t('canvas.importMap')}
</button>
<button
type="button"
className="button button-outline"
disabled={loadingSample}
onClick={() => void handleLoadSample()}
>
{loadingSample ? 'Loading sample…' : 'Open Sample Project'}
{loadingSample ? t('canvas.loadingSample') : t('canvas.openSample')}
</button>
</div>
</div>
Expand Down
Loading
Loading