The bar matched the map's own notes by title and then dropped any that had
no readable `#geolocation`, with one exception for a GPX track. A shape
carries its geometry in `#geoShape` instead, so it was never offered, and a
map holding a dozen of them had no way to reach one by name.
The three ways a note reaches the map are now read in the order DetailPane
reads them: a marker's location, a shape's geometry, a track's file. A shape
entry carries the box it covers and that box's midpoint. The midpoint is what
its distance is measured from, so it is ordered among the markers rather than
sorted behind them and cut by the eight-row cap; the box is what the map is
framed on.
`SearchResult` gained that box, and `frameResult` fits it instead of flying to
a point. The pane fits a shape too, but leaves the camera alone where it is
maximized or on a phone, and there picking a result would otherwise open the
pane over a map that never moved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Code notes render full height, so the scrolling container is laid out as
a flex column with overflow: hidden and never scrolls. The inline title
collapses off an IntersectionObserver watching itself against the
viewport, which then never flips: the sticky title row kept its
hide-title class, and the large title sat permanently above the editor,
taking height that no scroll could reclaim.
Show the inline title on text notes only, and hide the note type
switcher for code notes the way Markdown notes already hide it. Both
gates now read the note type directly, which leaves the SQLite, Markdown
and icon-pack exclusions unreachable: each only ever matched a code or a
file note.
The New Layout page said the inline title affects text and code notes,
and promised a type switcher on new code notes. Both are corrected, in
the User Guide markdown and in the doc notes the app ships.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Regenerated with `pnpm edit-docs:edit-docs` on the patched formatter.
Word boundaries lost after inline tags go from 146 to 9 across the
bundled help; the nine left match their Markdown, which has no space
there either. Every href and src value is kept, and each page's visible
text, whitespace aside, is unchanged — except Custom Widgets, which
gains the "multiple widgets" section its Markdown has carried since
#11120 without the page being regenerated.
Reading the images a page paints only works when the page is one scanned image.
A hotel voucher whose text is drawn as vector outlines -- 2559 constructPath
operations and no text-showing operator at all -- has exactly one image on it,
the 180x91 logo, so OCR returned "SMART TOURS" and nothing else. Neither pdf.js
nor PDFium finds any text in that file; the only way to read it is to rasterize
the page.
Rendering the page covers every case pulling the images out did, and several it
could not: text drawn as outlines, a scan split across image strips, a page
whose /Rotate the images themselves know nothing about, and stamps or annotations
drawn over a scan. It also bounds the work, since a page becomes a fixed number
of pixels no matter what resolution it was scanned at -- the old path would have
allocated a 140 MB RGBA buffer for a 600 dpi scan.
It costs nothing to do so. End to end, PDF bytes to a PNG Tesseract can read, the
two paths measured 175-240 ms and 186-256 ms on the sample scan; PNG encoding
dominates both. Recognition is unchanged: 94 confidence and 718 characters from
the embedded image, 93 and 716 from the render, same text. So the image path goes
away entirely rather than staying on as a fast case, and with it the channel
conversion, its unsupported-channel error path, the minimum image dimension and
the per-image loop.
PDFium ships as WebAssembly, which sidesteps what a native rasterizer would have
cost: @napi-rs/canvas is 30 MB per platform (20 MB of Skia statically linked
against libpng, libjpeg-turbo, libavif, harfbuzz and an SVG renderer, plus 11 MB
of ICU data the desktop build already carries a copy of), and it would need an
esbuild external plus a per-platform binary in copyNodeModules. The wasm is 4 MB,
identical on every target, and bundles normally -- main.mjs carries no reference
to it and it loads from a lazy chunk on the first page that needs rasterizing.
Its loader resolves the wasm against `import.meta.url`, which in the split ESM
bundle is a hash-named file under chunks/, so the bytes are read from
RESOURCE_DIR and handed to init() instead -- the same two paths core_assets.ts
uses for schema.sql, copied beside the server's assets by the build and resolved
through node_modules when running from source.
Pages are rendered in colour rather than PDFium's grayscale. Its conversion
flattens coloured text into its background: on the demo document it swallows the
whole "Organize your thoughts" panel, 60 characters that colour rendering keeps
and that no increase in scale recovers. Scale 2 is the knee of the quality curve
-- confidence 82 at scale 1, 93 at 1.5, 94 at 2, and flat above it while the
pixels to encode and read double again.
The voucher now yields 3284 characters at 0.93 confidence across both pages,
including the guest name, the hotel, the booking reference and the room type.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Custom endpoints section said only that the base URL can be set, leaving
which spellings work to be discovered by trying them. Name the rule: the URL is
used as entered, and only a bare host is completed with `/v1`.
Both copies are edited by hand rather than through `pnpm edit-docs:edit-docs` —
one paragraph inserted in each, matching the wrap and `<code spellcheck="false">`
conventions already in the HTML, so the pair stays in sync and no page is
reflowed. `!!!meta.json` carries note structure and no content-derived field, so
it needs no regeneration; `doc_notes.spec.ts` and `in_app_help.spec.ts` pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Interaction section documented click-based navigation only. Active-heading
highlighting has in fact been shipped and undocumented since 77ad6950e8, which
added it for PDFs and AI chat notes, so the bullet goes in the shared section
alongside the existing ones rather than being written as a text-note feature.
Also covers the collapsed case: since the child row is hidden, the collapsed
heading is highlighted in its place, which would otherwise read as a bug.
Edited by hand rather than through edit-docs, so both representations are
updated together and neither file carries the tool's usual reflow churn.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`getRootCalendarNote()` treats a missing journal as one to build, which is
right for the callers that ask for a day note — the calendar widget, "open
today's note" — and wrong for the ones that only need somewhere to put a
note. Deleting the journal therefore did not stop notes being date-nested:
the next capture recreated the calendar, the year, the month and the day,
which is what made the New button feel like it could not be escaped.
A capture with no `#inbox` now goes to the top level unless a journal
already exists, so a deleted journal stays deleted. `hasCalendarRoot()`
answers that without building anything.
This reaches every surface that goes through `getInboxNote()`: the New
button in the launcher bar, Ctrl+Alt+P, the tray entry, the sender API,
ETAPI's `/inbox/{date}` and the note autocomplete's creation row, whose
label now reads "at the top level" for such a database. Nothing changes
where a journal exists, which is every installation that kept the demo
data — `demo.zip` labels its Journal `#calendarRoot`.
Deliberate day-note navigation is untouched and still builds the calendar
it is asking for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Create note" said nothing about where the note would land, which is
decided by four different rules, so the user had to know their own
configuration to predict it.
`getInboxNote()` cannot answer the question, because it creates the day
note it falls back to as it answers. The decision is now split out into
`resolveInboxTarget()`, which reads becca and creates nothing, and
`getInboxNote()` materialises the day note only when the resolver says
that is the destination. Both go through the same function, so a label and
the capture it describes cannot disagree.
`GET /api/special-notes/inbox-target` exposes the resolved kind, note ID
and title. Hoisting needs no argument: the client already sends
`trilium-hoisted-note-id` on every request, which is what the resolver
reads through `hoistedNoteService`.
The row names the note by title for the three note-backed destinations,
and says "today's day note" for the fourth, which has no title until the
capture creates it. Deleting the journal does not change that answer — the
calendar is rebuilt on demand — so a test pins that rather than leaving it
to be rediscovered.
The lookup is issued before the autocomplete search and awaited after it,
so it costs a request but no extra wait, and is deliberately not cached:
a cache would go stale exactly when someone labels a note `#inbox` or
hoists into a workspace, which is when the label matters most.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The autocomplete offered one creation option and the pages named it by its
label, "Create and link child note". There are two now, so every page that
named the old label describes both and says where each note lands.
Note Inbox enumerates the routes into the inbox, so the new option joins
that list.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Greptile's review of #11355 flagged two issues, both valid.
The User Guide said the device's position "is not sent anywhere," which
is wrong once a tile provider is in the picture: standing the map on the
fix requests tiles for that area from whatever the map's style points at
(VersaTiles by default, or a custom #map:style URL), and that request
carries the viewport's coordinates. Reworded in both the Markdown and its
HTML twin to say Trilium does not send the position on its own, and that
moving the map there does what any other pan or zoom does.
The new comments across MapToolbar.tsx, index.tsx, Pois.tsx,
coordinates.ts, MapToolbar.css and their specs used the personified,
literary register CLAUDE.md's Comments section explicitly asks new code
not to match -- controls that "wear" buttons, positions that "stand,"
events that "say" things. Rewritten in plain, active voice around real
identifiers (GeolocateControl, setState, onLocationClick, isLocationDot),
without changing what any of them explain. Comments that predate this
branch are left as they were, per the same section: they are grandfathered
history, not something this PR is responsible for bringing into line.
No production code changed; the two new spec files still pass (301
tests) and typecheck is clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Clicking the blue dot now stands the map on that position and opens the
searched-place panel for it, so the position can be read and kept as a
marker. That is the same thing typing coordinates into the search bar
does, and it reuses it: the point-as-a-place factory the search bar had
inline moves to coordinates.ts as pointPlace(), keyed and zoomed
identically, and both call sites go through it.
The click is told by its target rather than by hit-testing the map. The
dot is a DOM marker appended to the canvas container, not a layer, so a
click on it bubbles to the map's own click event carrying the dot as the
target -- MapLibre's marker popups work off the same fact. The accuracy
circle is a marker of its own, so it does not match and a click there
stays a click on the map, which is what the user asked for.
Two things had to be kept off the hit box. The pulse is a ::before scaled
from one to three times the dot, and a pseudo-element is hit-tested at any
opacity, so without pointer-events: none the target would have breathed
between 15 and 45 pixels and a click well outside the dot would have
counted. The POI layer's click handler bails on the dot too, since a dot
standing over a shop would otherwise open the shop's panel as well.
The reported position is the last fix rather than where on the dot the
click landed -- the dot is a few pixels wide and unprojecting them would
only add noise -- rounded to the six decimals a place is named and stored
to. In placement mode the click means "put a note here" and the dot adds
nothing to it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds a "Going to your location" section to the Geo Map page, between
repositioning and searching: the prompt on first use, the dot and its
accuracy circle, following and how a drag frees the camera, what a failed
fix and a refused permission look like, and that the position is neither
stored in the note nor sent anywhere while the map's saved view still
moves as after any other pan. The feature list and the bottom-right
controls under "Interaction" name the button, and "System requirements"
says it is only offered over HTTPS, in the desktop application or in the
mobile application, since browsers refuse location on plain HTTP.
Written in both the Markdown source under docs/User Guide and the
export-only HTML twin under apps/server/src/assets/doc_notes, in the
exporter's own style, so the in-app help does not lag the site until the
next edit-docs sync. That sync may rewrap the HTML; nothing else about it
should change.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A word accepted by the fuzzy fallback was bolded exactly like a word the user
typed, so a card built entirely from approximate matches read as though the
query appeared in it verbatim. Searching for "orbiter" in the demo database
highlighted "writer" in "The Last Question" with no indication it was a
stand-in.
Give the fuzzy tier its own marker pair. Braces are already stripped from both
the source text and the tokens before highlighting, so the only ones left are
the pairs inserted here and doubling one is unambiguous: "{{" and "}}" render
as <b class="search-fuzzy-match"> where the single pair still renders as <b>.
renderHighlights replaces the doubled pair first so its braces are consumed
before the single-brace pass, highlightField advances the regex by the actual
marker lengths rather than a hardcoded 2, and the three marker-stripping sites
share one MARKER_CHARS constant. HighlightedTokenInfo gains a "fuzzy" type,
which falls through the existing regex checks into the literal-matching
branches, so nothing else changed.
The colours come from the logo. icon-color.svg carries an orange, a red and a
green family, and the themes already pick one per scheme: light highlights
with #c65050 from the red family and dark with #a4d995, all but the logo's
green. Fuzzy takes the orange from the same source, matched in lightness to
each theme's exact colour so the two read as siblings -- #e47b19 on light
(L 50% against 54%) and #efb075 on dark (L 70% against 72%). A deep orange
would vanish on dark and a light one would wash out on white, which is why
they differ. The new variables mirror the existing two-level indirection, so a
theme author overrides one value and all three surfaces follow: the result
cards, the quick-search dropdown and the jump-to-note autocomplete.
Not bold and dotted-underlined, to carry the distinction where colour alone
would not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review feedback on the search overhaul. The branch carried comment prose that
narrated the investigation rather than the constraint that still binds, plus
helpers extracted only so a unit test could reach them.
Comments: cut the block on autocomplete.js's silent `val` setter down to the
two lines a reader needs to not delete the `trigger("input")` calls, dropped
the redundant notes on the content snippet and its CSS rule, and collapsed the
repeated three-line jump-to-match note at each type widget's content-ready
point to one line. The detail behind it now lives in `consumeSearchTerms`'s own
documentation. Trimmed the JSDoc on the text_utils tokenizer helpers, the
preprocessor's link extraction and search.ts's highlight helpers.
Helpers: inlined `deriveSearchViewScope` and `deriveSeededFind` at their single
call sites and deleted the two specs that existed only to reach them.
`resultViewScope()` had two call sites, so quick_search stores the derived view
scope where it used to store the token list, which removes the method without
duplicating the ternary. In search.ts, `tokenInfoMatches` and `wrapText` are
inlined; `toTokenInfos` and `renderHighlights` stay, because three call sites
each would duplicate a `map()` and a `replace()` chain rather than inline them.
`useSearchTermsConsumer` stays a hook: four call sites across the code and text
widgets, read-only and editable variants.
Search.md: rewrote the ten em-dash asides as plain sentences.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A map with no saved view opened on the stock centre and zoom, and its
notes were found by panning until they turned up. A map filled in by a
script or through ETAPI asks that of every reader who opens it, since
nobody has ever positioned it by hand — a map whose markers were placed
by hand already has a view, the placing having moved the camera there.
Framed once, and only where nothing has been saved: a saved view is
where the reader put the map. The framing moves the camera like anything
else, so the `moveend` behind it saves the view, and the next open reads
it back rather than working it out again. Not animated — this is where
the map opens, not somewhere it is taken.
`maxZoom` is the detail pane's own, so a single note stands on its street
rather than at whatever zoom solves a box with no width.
The box itself moves to coordinates.ts as `boundsOf`, which is where the
seam at ±180° was already dealt with for GPX tracks: markers either side
of it would otherwise be framed the long way round the world. DetailPane
now yields its points into it rather than carrying its own copy, and a
long ride's thousands of points are still never gathered into an array.
DEFAULT_COORDINATES was [3.878638227135724, 446.6630455551659], read as
longitude 446.66 — a turn and 86.66 — and latitude 3.88: open ocean west
of Sumatra, with nothing to say for itself. Now the prime meridian a
little north of the equator, which at DEFAULT_ZOOM is most of the
inhabited world. Only a map holding nothing with a place opens there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A manifest built by copying a font's own stylesheet carries its glyphs as CSS
escapes -- Font Awesome's `fa-0` is `"\\30 "`, four characters, not the digit.
Interpolated raw, the browser resolved the escape and the icon rendered.
Escaping the glyph for the shared-page stylesheet turned the leading backslash
into `\5c `, so `content: "\5c 30 "` renders the text `30 ` instead of the
glyph. Every icon in such a pack became its own hex code as visible text.
Decode the escapes to characters before escaping. Both manifest forms then
normalise to one, and the output cannot widen: the decoded text still goes
through escapeCssString(), so a glyph spelling `\3c /style\3e ` decodes to
`</style>` and escapes straight back, and a lone `\5c ` round-trips rather than
emitting a backslash that would end the glyph's own CSS string.
generateTaskStateCss() reads glyphs from the same manifests and had the same
bug, so it decodes too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The charset check rejected every code point outside [a-zA-Z0-9-_], but a CSS
identifier takes letters, digits, `-`, `_` and any code point at or above U+0080
unescaped. A key such as `un-café` produced a valid selector before the check
existed and vanished after it, with only a log line to say why.
Widen the pattern to the set CSS actually accepts. The characters the check
exists for -- `<`, `"`, `\`, controls and whitespace -- are all ASCII, so the
`</style>` break-out it closes stays closed.
The pattern is shared with the prefix check, which this widens the same way:
both values are interpolated into the same selector, so a separate rule for
each would be arbitrary, and the change only ever accepts more.
The regex carries no `u` flag, so the range matches UTF-16 code units and an
emoji's surrogate pair passes as two in-range units. A test covers an emoji
glyph and an emoji key, since a pack keyed on the characters themselves is the
case most likely to regress if the pattern is later rewritten with `u`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bumps electron 43.4.1 -> 44.0.0 and repairs the three `pnpm typecheck`
errors the bump surfaces in apps/desktop.
Login items: Electron 44 removed `openAsHidden` from
`app.setLoginItemSettings()` and `wasOpenedAsHidden` from
`app.getLoginItemSettings()`, since both only worked on macOS 12 and
below. `wasLaunchedHidden()` now combines the remaining
`wasOpenedAtLogin` with the stored `hideOnAutoStart` option, so macOS
keeps the feature: the app opens normally and hides its own window,
matching what Windows and Linux already do through --start-hidden.
Clipboard: the module is now modelled on the W3C Clipboard API, so
`writeImage()` is gone and `write()` takes MIME-typed payloads and
returns a promise. The copy-image-to-clipboard handler re-encodes the
decoded image to PNG and writes it as a `ClipboardItem`. `toPNG()`
returns `Buffer<ArrayBufferLike>`, which is not a valid `BlobPart`,
hence the `Uint8Array` view.
Electron 44 also drops macOS 12, so the User Guide system requirements
and the website download blurb move from Monterey to Ventura.
Verified with electron 44.0.0 installed: `pnpm typecheck` is clean and
`pnpm --filter desktop test` passes 492 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WkCLU8T5ENHbhMaL2zdEuW
api.cheerio was deprecated in v0.103.0 in favor of api.htmlParser. The
scripting API was its only consumer: core parses HTML with
node-html-parser in about twenty places, from the enex, notion, keep and
markdown importers through similarity, page_description and link_embed.
backend_script_api.ts imported cheerio statically and the core barrel
re-exports BackendScriptApi, so it sat on the startup path of the server,
desktop and standalone builds alike. Measured with esbuild against the
installed versions:
node-html-parser alone (node) 247 kB
node-html-parser + cheerio (node) 1.6 MB
cheerio, browser build 373 kB
The two share no dependencies, so the marginal cost was roughly 1.35 MB
minified on server and desktop (parse5, htmlparser2, undici,
encoding-sniffer, cheerio-select) and 373 kB inside standalone's
abstract_provider chunk, which becca_loader pulls at worker startup.
api.cheerio now resolves to a throwing Proxy carrying migration
instructions, the same shape api.axios took when it was removed, so an
existing script fails with a hint instead of a TypeError on undefined.
cheerio stays in the lockfile as a transitive devDependency of
webdriverio, which the ckeditor5 browser tests drive. Nothing ships it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A supermarket found by searching, or clicked on the base map, was shown
under a cart in the tooltip and in the panel's heading, and then became a
marker wearing the same generic pin as every other. The icon was worked
out and drawn, but thrown away at the moment the note was created:
PlaceToKeep did not carry it, and createNoteAt wrote CHILD_NOTE_ICON as
the iconClass label unconditionally. Markers draw from note.getIcon(), so
the pin was what appeared.
Both entry points already had the icon in hand -- poiFromFeature reads it
off the tile's OSM tags and nominatim off the geocoder's category -- so
widening PlaceToKeep is enough for it to reach the note; index.tsx passes
a GeoSearchResult through as it stands. A place whose kind neither
reports still falls back to the pin, as a note dropped by clicking the
map does.
The user guide already promised this of a clicked place ("the icon that
matches the kind of place it is"); the section on keeping a searched one
now says it too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The guide is read the world over, and it is the text the other locales
are translated from. It was written in a register that suits neither: an
em dash carrying an aside inside a sentence that already had two clauses,
inverted openings such as "Where the service reports a boundary", and the
passive throughout, so that the reader was rarely the subject of the
sentence describing what they do.
Every passage of the search, point and place sections is rewritten. The
asides are sentences of their own, the dashes are gone, the reader is
addressed directly -- "these are the places you can click" rather than
"those are the ones that can be clicked" -- and the lists separate their
term from their description with a colon.
Hyphens inside compound words are spelling rather than punctuation, and
are kept.
Both the Markdown and the HTML the app ships are written, since
edit-docs, which otherwise keeps the two in step, wants a desktop session.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The map named its own new notes, reaching into the relation map's
catalogue for the word it used. That was a borrowed string in two senses:
the key belongs to another feature, so a translator working on it has no
way of knowing the geo map says it too, and the map was answering a
question the server had already answered better.
A note created with no title is named by the server, which is what the
tree's + button relies on and what a note gets everywhere else in the
app. So the map stops sending one. Three things follow. The name is "New
note" rather than the relation map's lowercase "new note", which is what
the rest of the app calls a note nobody has named yet, and which no
longer reads oddly beside a marker called after a real place. A map
carrying `#titleTemplate` names its markers by that template, a feature
the geo map simply did not take part in before. And the borrowed key is
left to the relation map, without a new one being minted for anybody to
translate.
A place that arrives with a name of its own -- searched for, or clicked
on the base map -- is still kept under it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A place kept as a marker takes the name the place carries, which is what
a reader wanted of a cafe found by searching. A point read out of the
search bar carries only its own coordinates, and a note called "45.9432,
24.9668" is named after the one thing about it nobody needs saying twice:
the marker already stands there.
It takes the name a marker placed by clicking takes instead, and the pane
opens with that name picked out to be typed over -- the two ways of
putting a note somewhere the map has nothing yet now behave alike.
Said by the place rather than read off its id: a place knows whether it
has a name of its own, and a point is the only one that does not.
The api spec could not have caught a title going wrong: i18next is not
initialized under test, so `t()` answers with nothing, and the test that
speaks of the stock name checked every part of the call except the name.
It mocks `t` as the search bar's spec does, and says what it means.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Coordinates typed into the bar named nothing: they matched no note and no
place, so the only row left was the geocoder's, which would not have
found them either. The guide's way to reach a point was to make a note by
hand and paste the pair into its `#geolocation` attribute.
They are now read where they are typed and offered as a row above
everything else, since a reader who typed a point named where they were
going rather than looked for it. Taking it flies there and pins it, and
the pin is kept as a marker exactly as a searched place is -- the point
is carried as a place, which is what it is taken as, in a row of its own
so that what it says is what taking it does.
Read in the forms a reader arrives with: the bare pair both Google Maps
and OpenStreetMap hand over and `#geolocation` holds, the `geo:` URI the
map offers a place under, and the URLs of those two sites, which is what
is copied by someone who never went looking for coordinates at all. A
pair standing off the Earth is a pair of numbers being searched for
rather than somewhere to be flown to, and is passed over.
`parseLocation` is not reused for this. It reads an attribute the map
wrote, so it is lenient where a search bar cannot be: it would have read
"45 Main Street" as a point 45 degrees north.
A place may now ask for the zoom it is shown at, the level guessed for a
place of unsaid extent being a town-wide view -- which is not what
somebody who typed a spot to six decimals meant.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A track carries its route in the file rather than on a #geolocation
label, so matchMarkers passed over it for want of a point to fly to. It
is drawn on the map like anything else, though, and the pane already
fits a selected track's whole route off the map's own source.
A track is now offered by title and reported with no centre, which
frameResult leaves alone: the route is a shape, not a spot, and the pane
frames it. Carrying no centre, a track also carries no distance, so it
sorts after the markers that have one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two things stood between a note on the map and the reader looking for it.
Its title was matched by holding the query against it whole and as typed,
so "zurich" passed over "Zürich Hauptbahnhof" and "hotel paris" passed
over "Paris Hotel". The terms the app filters its own lists by are used
instead -- the ones the settings search runs on -- which fold accents on
both sides and ask for each word on its own, in whatever order it was
typed.
And the eight the list has room for were taken while the notes were being
gathered, in the order the map happens to hold them, and only ordered by
distance afterwards. A map of more than eight cafes therefore offered an
arbitrary eight of them, the one down the road as likely to be left out
as any other. They are gathered whole now and capped once sorted, so what
is offered is the nearest of what matches.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The colour alone was read at the fraction of solid the styles hold their
places at, which is what keeps them a background: an orange at that
strength is a warmer grey rather than a foreground, and the places that
can be kept still had to be picked out of the ones that cannot.
They are now drawn at nearly solid while the rest stay as faint as the
style asked, so the two read as foreground and background rather than as
two greys.
The styles paint this with the old function syntax -- a ramp climbing
from nothing at zoom 16 to a fraction of solid by 17 -- which is an
object rather than an expression and so cannot be composed with the way
the colour is. Rather than writing out what the styles say today, the
ramp is read and rebuilt as the expression saying the same thing, with
the raise standing only at the zooms a place can be picked from: a style
is fetched now rather than shipped, and one that lifts its places out of
the background on its own is not to be argued with about how far. A ramp
that finishes climbing before a place can be picked has the raise added
as a stop of its own; a flat number is stepped in where the colour is;
and anything that is not a ramp of numbers is left alone, its colour
still composed, the two properties being taken and put back apart.
Between 16 and 17 a place that answers a click now climbs faster than one
that does not, MapLibre taking one zoom curve at the top of an expression
and no more. It reads as the places growing into the foreground they turn
orange on reaching.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Which places could be clicked was there to be found by hovering each in
turn: the styles draw them all in one grey, and nothing told a cafe that
can be kept from a bench that cannot.
They are now drawn in the colour a picked place is pinned in, which gives
the map one rule -- orange is a place you can pin -- whether the place
came from the search or was already on the map. The colour is composed
over what the style painted rather than replacing it, so a place that
answers no click keeps the grey and stands as the background it is, and a
style painting its places some way that cannot be composed with is left
alone entirely.
What the colour asks is what the click asks: a name to keep the place
under, and a zoom close enough in to pick it from. The zoom is asked at
the top of the expression, MapLibre taking `["zoom"]` only as the input
of an outermost `step` or `interpolate` -- nested any deeper the whole
value is refused, and a refused value is a map that looks exactly as it
did before, which is the only account it gives of itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The styles draw a place as a bare icon: they carry its name but put no
label on the map for it, so the only way to learn what one was called was
to click it and read the panel. Turning the styles' own labels on was the
other way round, and would have set them against the titles of the map's
own markers, which are optional and give way to whatever is placed first
-- the map's own notes losing their names to a street of shops.
A name on hover costs the map nothing until it is asked for. It carries
the icon the place would wear as a marker, waits out a rest shorter than
the one a marker's preview sits out -- that one reads a note from the
server, this is already in hand -- and goes as the pointer leaves.
The pointer and the name are now both driven by the place under it rather
than by the layer it is drawn on, which fixes what the cursor claimed: it
turned into a pointer over every place, including the unnamed ones that
answer no click. Both now appear for exactly what can be clicked. A
marker standing over a place keeps the pointer it set for itself, and a
click takes the name down and holds it down until the pointer has left
and come back, the panel it opened saying the name at length already.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The vector styles already draw the shops, cafes and museums around the
area being viewed, but they were decoration: nothing hit-tested them, so
the only way to pin a place you were looking at was to click bare ground
and type its name yourself.
A click on one now stands the map on it, under the same pin and panel a
searched place gets, and Add as marker keeps it as a note named the way
the place is named and wearing the icon for its kind. The name and the
OSM tags come out of the tile the map has already downloaded, so this
costs no request to anyone -- osm_icons already speaks that vocabulary,
having been written for the geocoder that reports the same tags.
The styles put their places on at zoom 16 but fade them in over the zoom
that follows, so at 16 they are drawn at no opacity at all; picking is
held back until 17, a place that cannot be seen not being one anybody
means to click. A marker, a cluster or a track answers a click first,
and a place with no name is passed over rather than kept as "Bench".
The raster layer bakes its places into the image and Neutrino draws
none, so on both there is nothing to hit-test and a click means what it
meant before.
A tile carries no address, so the panel now leaves out the line that
would have repeated the heading.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The panel took the detail pane's corner and width, but the detail pane
never renders there on a phone — it swaps to MarkerSheet. At 380px capped
to the map's width, the card spanned the whole screen from the top down,
covering the search bar and the result navigator under it.
On mobile it stands at the foot instead, which leaves those reachable, so
results can still be stepped through with a place open. The clearance the
foot bars need is now --geo-map-foot in index.css, read by both panels.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>