4652 Commits
Author SHA1 Message Date
Elian Doran 034fa134a9 fix(deps): update codemirror (#12013) 2026-10-11 14:03:48 +03:00
Claude fcd6d86d12 test(codemirror): fire selectionchange after the selection moves, as browsers do
@codemirror/view 6.43.14 makes DOMObserver.flush() call view.update([])
whenever it saw a selection change. happy-dom dispatches selectionchange
synchronously from inside Selection.collapse(), which CodeMirror calls
while writing its selection during an update, so all ten
hosted_completion specs threw "Calls to EditorView.update are not
allowed while an update is in progress".

The package had no test setup, so it did not get the deferral added to
apps/client/src/test/setup.ts. Add the same setup here, kept out of the
library build and the coverage count.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UQQd11a32NYctkABZQY3dM
2026-10-11 10:49:03 +00:00
Elian DoranandClaude Opus 5.5 eec52f8c84 fix(clipper): keep embedded YouTube videos as link previews (closes #1449)
A clipped page's YouTube players reached the server as <iframe>s, which
the sanitizer strips because `iframe` is not an allowed tag, so the
videos vanished from the note without a trace. (Before iframes were
stripped, they were stored as live players and all started playing when
the note opened, which is what the issue first reported.)

`processContent()` now replaces each iframe whose `src` is a YouTube
video with the `section.link-embed` markup the text editor stores for a
YouTube link preview: a thumbnail that loads the player only on click.
Its title, channel, thumbnail and favicon come from the same oEmbed
lookup the editor uses, exported from core as `resolveYouTubePreview()`,
and the pictures are stored as attachments of the clipped note. When the
lookup fails, the preview keeps only the URL rather than dropping the
video. Other iframes are still removed by the sanitizer.

The conversion runs on the server rather than in the extension because
the server knows it can render link previews (added in v0.104.0): an
older server keeps stripping the iframe, and an older extension gets
the fix without an update.

`YOUTUBE_REGEX` also matches `youtube-nocookie.com/embed/`, the domain
of YouTube's privacy-enhanced embeds.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-11 10:59:14 +03:00
renovate[bot] 3e150eb777 fix(deps): update codemirror 2026-10-11 06:26:00 +00:00
renovate[bot] d4ee123ecd fix(deps): update ai sdk 2026-10-11 04:22:16 +00:00
Elian Doran 65baf16a8c feat(share): show shared notes with the app's own views (#12001) 2026-10-10 20:15:15 +03:00
Elian DoranandClaude Opus 5.5 8e063854d4 test(share-theme): cover the app view host and its views
The share-theme gate requires 100% and stood at 82.6% of lines:
`app_host.tsx`, `app_view.ts`, `collection_view.tsx` and
`app_globals.ts` had no spec, and `note_view.tsx` covered only the
widget fallback.

`app_host.spec.tsx` mocks the client modules the host wires together
(froca, options, `appContext`, the link menu, the image URL resolver)
and drives a shared page, a static export page, a note missing from
froca and clicks on links into the tree. `app_view.spec.ts` imports
`app_globals.ts` for real, so the jQuery globals are checked rather than
mocked. The note view spec now mounts each view the share theme draws
its own way, and unmounts each container after a test: emptying it left
the Mermaid view's theme observer alive, which drew again in the next
test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 19:03:36 +03:00
Elian DoranandClaude Opus 5.5 6d7719cc85 fix(share): keep notes hidden from the tree out of note maps and name completion
`isReadable()` checks that a note is shared, unprotected and open to the
caller's credentials, but not that the share tree lists it. The tree
note map and attribute-name completion relied on it alone, so
`/share/api/note-map/<root>/tree` returned the ID and title of every
`#shareHiddenFromTree` note below the map root (and of its descendants,
which the tree cannot reach either), and `/share/api/attribute-names`
offered the names of their attributes. The share's search already
leaves such notes out through `isVisibleInShareTree()`.

`isListedInShareTree()` walks up from a note to the share root through
non-hidden branches, mirroring `SNote.getVisibleChildBranches()`. The
link map, relation map, script bundle and `loadTree()` keep reading any
readable note: they reach only the notes a shared note names itself,
and a hidden note stays reachable by its URL.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 18:38:25 +03:00
Elian DoranandClaude Opus 5.5 82866ee20c test(export): compare the export spec's binary content as byte arrays
The two tests that check a binary note or attachment survives the export
built their bytes with `Buffer.from()`, compared them with
`Buffer.compare()` and read pages with `.toString("utf-8")`. They now use
`Uint8Array` fixtures, compare the bytes as arrays and read the pages
with `decodeUtf8()` from `services/utils/binary.ts`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 18:27:22 +03:00
Elian DoranandClaude Opus 5.5 5e495e6817 fix(share): load the renderer each exported dashboard tile draws with
The static export picked the share theme's groups for the notes a
dashboard or a presentation draws by note type, as `content:<type>`,
but the client names those groups after what it renders: a Markdown
note is a `code` note drawn by `content:markdown`, a PDF, audio, video
or canvas drawing file is drawn by `content:file`, an icon pack by
`content:iconPack`. The export asked for `content:code`, `content:file`
for every file, `content:text` and `content:book`, three of which do not
exist, so a dashboard tile showed a rendering error and a presentation
never finished loading. The spec asserted those wrong names.

The decision now lives in commons and both sides read it:

- `getNoteContentType()` and `getFileContentType()` move out of the
  client's renderer; `isMarkdownMime()` is the one list of Markdown media
  types, which `FNote.isMarkdown()` and `BNote.isMarkdown()` use too.
- `CONTENT_RENDERER_GROUPS` maps each content type to the group of the
  renderer the app loads on demand for it; text, code and office
  documents load none. `getContentRendererGroup()` looks it up with
  `hasOwn`, as the other tables of `notes.ts` are read.
- `CONTENT_RENDERERS` is exported, and a client spec fails on a group of
  the commons table that it has no entry for, so the two cannot drift.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 18:05:33 +03:00
Elian DoranandClaude Opus 5.5 90c0c4ea33 test(share): encode the export spec's SVG with the binary helper
The share data spec turned its SVG text into bytes with a hand-written
`TextEncoder`; it now goes through `encodeUtf8()` from
`services/utils/binary.ts`, as every other binary conversion in core does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 18:00:07 +03:00
Elian DoranandClaude Opus 5.5 4765e7e033 fix(share): count only readable notes in a shared note map
A shared note map left out the notes a visitor cannot read, but the
descendant count it kept for each note it drew still came from the whole
database: a shared parent reported its password-protected and unshared
children among its descendants.

`buildDescendantCountMap()` now takes an `isCounted` test and skips a
note it refuses together with everything under it, so a locked note
reveals nothing below it either. `buildTreeMap()` and `buildLinkMap()`
pass it through, accepting every note by default, which leaves the app's
own note map as it was; `buildVisibleNoteMap()` gives it the share's
`isVisible`, for the live share and the static export alike.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 17:54:22 +03:00
Elian DoranandClaude Opus 5.5 d15ffdbb14 fix(share): write image and file notes of a static export beside their pages
The share export wrote an image note's page HTML under the image's own
name ("Photo.png"), since mapExtension() kept image extensions for the
sake of attachments, and never wrote the image's bytes at all; its <img>
then pointed back at that HTML. A PDF's frame (api/notes/<id>/view) and
a file note's download button (location.href='api/notes/<id>/download')
were never rewritten, so both reached for an API a static site lacks.

Image and file notes now get a page with the .html extension and, next
to it, a file holding their content: the title as it is, or with an
extension derived from the MIME type when it has none ("Manual" ->
"Manual.pdf"). Embedded images, the PDF frame and the download button
point at that file; links and the tree keep pointing at the page. The
image and .zip exceptions in mapExtension() now apply to attachments
only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 17:34:23 +03:00
Elian DoranandClaude Opus 5.5 f5312a99c8 fix(share): open any note an app view names on its shared page
A live shared page knew the URL of a note only when the note came with
the page's payload or with a later `api/tree` response. A view that
learns of notes another way had nowhere to send the visitor: a shared
note map lists its nodes from `api/note-map`, so a click on any node
beyond the map note's own children did nothing, and its context menu
offered no place to open it.

The URL is now computed from the note's ID instead of shipped beside it.
Both froca sources answer `getNoteLink()`, which `openNote`, `hasLink`
and the link context menu's resolver read:

- On a live share, a note without a known link links to `./<noteId>`.
  `/share/<noteId>` already serves the note, and now sends a 302 to its
  own link when that differs: its `#shareAlias` or its
  `#shareExternalLink`. The redirect runs after the access check, so it
  reveals no alias to a visitor without credentials, and `?raw` is left
  alone.
- The static export answers `null` for a note it does not hold. Its
  links are complete already: `rows.json` carries one for every exported
  note, and the host reloads froca from it before any view mounts.

A reference link to an unshared note on a live page now leads to the
share's 404 page rather than doing nothing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 17:16:59 +03:00
Elian DoranandClaude Opus 5.5 abdd7f76b1 test(share): bring the share code back to full coverage and fix CI-only failures
The standalone suite fell to 96% lines on `trilium-core/src/share/**`, below its
100% threshold: the routes the app views read (`/share/api/tree`, the attachment
and blob routes, the note map, the relation map and the script bundle) had no
core spec, and only the Node-gated zip export reached `froca_payload.ts`. The
handler specs now drive each route and check that a note behind credentials is
left out, and `froca_payload`/`SNote` specs cover the built-in templates read
from becca. Two fallbacks in `content_renderer.ts` could never run and are
removed: both callers of `hostInAppView` pass string content, and
`buildExportRows` asks for a link only for the notes `canAccess` admitted.

The server suite failed two share-export tests in `zip.spec.ts`, which read
`share_theme.json` from a client build CI does not have; the spec now builds a
share export over empty theme assets, since the tests check naming and link
rewriting only.

The client suite failed to load `PromotedAttributes.spec.tsx`: its `ws` mock
called `importOriginal`, which loaded `tree.ts` against the still-pending mock.
The spec now sets `logError` on the global websocket mock instead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 17:00:40 +03:00
Elian DoranandClaude Opus 5.5 dfe7bac7ff refactor(share): draw the zoom viewer's controls with the app's ZoomControls
The share theme's zoom viewer drew its own floating buttons, where the
app puts every control over a note's content on an
`OverlayControlGroup`. `ZoomViewer` now holds `ZoomControls` in a group
at the bottom end, labelled from the app's catalogue, and takes only the
viewport's `label`.

`ZoomControls` and the group's tooltip reach `react/hooks.tsx`, which
loads `app_context`, froca and Bootstrap and expects the jQuery global
and stylesheets that only `setupAppView()` sets up. On a text page that
setup never runs, so the viewer `mermaid.ts` mounted over a Mermaid note
embedded in a text note would fail to load. That path goes:
`setupMermaid()` draws an embedded Mermaid note in place, as it draws a
`mermaid` code block, and core's `renderMermaid()` no longer writes the
viewer's `data-*` labels; their four `share_theme.*` keys, added on this
branch, go with them. Giving embedded notes their app view is left to a
separate change.

Every remaining viewer takes the page, so the in-flow mode (wheel zoom
only with focus, panning only once zoomed in) and its `fillsPage` prop
go too. The Sharing page describes the page-filling viewer, and that the
diagram's source shows only without JavaScript, as the hosted view
leaves it out.

Raised by Greptile on #12001.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 16:41:49 +03:00
Elian DoranandClaude Opus 5.5 304211fe4c fix(share): keep a shared note's content when its view fails to load
`mountNoteView()` emptied the container before the view's files had
loaded, and the effects that loaded them had no rejection handler. If a
chunk failed to load (a code, spreadsheet, relation map, note map or
Mermaid note's), the visitor got a blank page instead of the content
core rendered for visitors without scripts.

`loadView()` now loads the view first, from the note's type in the
payload, and the container is emptied only once it has. A failed load
rejects `mountNoteView()`, which leaves the rendered content in place.
`setupAppView()` awaits the mount, so the spinner stays up while the
view loads, and its `finally` still shows the content if loading fails;
before, the container showed empty for the length of the load.

Raised by Greptile on #12001.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 16:24:17 +03:00
Elian DoranandClaude Opus 5.5 45e627398e style(share): draw a list collection's subpages as the app's list card
The rows of a shared list were transparent until hovered. They now stand
on the card background as the sections of the app's list card do: 3px
apart, rounded at the two ends of the list, with the app's padding and
hover color, and titles at normal weight.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 16:14:34 +03:00
Elian DoranandClaude Opus 5.5 25b6e61a02 feat(share): build the views' image URLs through one helper
The views build their image URLs as `api/images/<noteId>/<title>` and
`api/attachments/<id>/image/<title>`, relative to the page. On a shared
page that is the share's API, but a page of the static export sits in a
folder of its own, so the URLs reached no file; a `MutationObserver`
rewrote them after the fact, so every image first failed once, and the
geo map's image overlay, which MapLibre loads outside the DOM, never
showed.

`image_urls.ts` builds both URLs, unless a resolver set with
`setImageUrlResolver()` knows another, and `resolveContentImageUrls()`
does the same for the `src`, `data-image` and `data-favicon` of a
note's HTML, before it is parsed, as the browser loads an image as soon
as it parses its element. The read-only builders go through it:
`createImageSrcUrl()`, `link.ts`'s image conversion, the content
renderer, the attachment view, the geo map's overlay, the text renderer
and the share theme's image view. The editor's paths keep their own.

On a page of the export, the host sets the static source's
`getImageUrl()`, which answers from the `rows.json` it read before the
views mounted, and the observer goes. On a shared page no resolver is
set, so the URLs stay as they were.

Crawling the 60 pages of an export that host an app view, none
requests the API any more, and the image map draws its image.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 16:14:18 +03:00
Elian DoranandClaude Opus 5.5 90509d2aff feat(share): read the static export's app view data from its files
An export page hosting an app view read its notes from the share's
API, which a static export has none of, so every view stopped at its
first request. Core now marks an export page's payload with
`exportBasePath`, and the share theme then gives froca
`createStaticFrocaSource()`, which reads what the export computed under
`data/`:

- notes, branches, attributes, attachments and links from `rows.json`,
  fetched once, the links made relative to the page;
- blobs, relation maps and script bundles from their own files, and a
  note map by its request's key, else another map of its root and kind;
- the search `getChildNoteIdsWithArchiveFiltering()` lists a note's
  children with, from the rows; any other search finds nothing.

The views build their image URLs in a dozen places, so a
`MutationObserver` over `#content` points each `api/images/…` and
`api/attachments/…/image/…` URL at the export's file instead.

A collection a dashboard draws never mounted, here or on a shared page:
it takes its path from `getBestNotePathString()`, and no note above
the page's notes is in froca. The host places each note without a
parent in froca below a stand-in `root`, in the payload and in every
answer of the source, and on a page of the export first loads every
note of the export, so such a collection also has its children.

Exporting a subtree holding every hosted type and serving it as plain
files, all 60 pages hosting an app view mount and draw, with no failed
request for an asset or a data file.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 15:54:59 +03:00
Elian DoranandClaude Opus 5.5 f3fa99a551 feat(share): key the static export's note maps by their request
The export wrote each note map as `data/note-map/<rootId>-<type>.json`,
so two note map notes on the same root with other relation filters
wrote the same file, the last one winning.

`getNoteMapDataKey()` in commons keys a request by its root, kind and
relation filters, sorted, so the same filters in any order give the
same key. The export writes each request's map once, as
`data/note-map/<n>.json`, and indexes it by its key in `rows.json`
under `noteMaps`; a static froca source builds the key from
`getNoteMap()`'s arguments to find it. `rows.json` is now written last,
once the maps are known. The key avoids `toSorted()`, as the share
theme targets Chrome 96.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 15:50:23 +03:00
Elian DoranandClaude Opus 5.5 ca7cf406c9 feat(share): compute the data of the static export's app views
The app views an export page hosts read their notes from the share's
API, which a static export has none of. The export now computes those
answers into `data/`, for a static froca source to read later:

- `rows.json`: the rows of every exported note with its attachments,
  and the paths, relative to the export root, of each note's page,
  each attachment's file and each note's image;
- `blobs/{notes,attachments}/<id>.json`: the content of the hosted
  notes, of their children and grandchildren and of their text
  attachments;
- `images/<noteId>.<ext>`: the bytes of an image note, which the export
  held nowhere, as the note's own file is its page; a canvas, Mermaid,
  mind map or spreadsheet note points at its existing export attachment;
- `note-map/<rootId>-{tree,link}.json`, both kinds, as a reader can
  switch between them; `relation-map/<noteId>.json`; and
  `script/<noteId>.json` for each render note's script.

Protected notes and notes outside the export are left out, as a shared
page leaves out what a visitor cannot read. The note map, relation map
and script bundle rules move from the share handlers into
`buildVisibleNoteMap()`, `buildVisibleRelationMap()` and
`buildVisibleScriptBundle()`, which both call with their own visibility
check, so the export answers what the share answers. `getBlobRow()`
decodes byte content instead of `String()`ing it.

An export of a subtree holding every hosted type writes 5.7 MB under
`data/`, 4.2 MB of it blobs; every path in `rows.json` exists in the
archive.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 15:22:13 +03:00
Elian DoranandClaude Opus 5.5 e19e4942b9 feat(share): host app views on the pages of the static export
The static export rendered a hosted collection or note type with the
share's static fallback only. It now hosts the app's view as the live
share does, and ships the parts of the app bundle its notes use.

- `renderNoteForExport()` takes a `getLink` for the export's pages, and
  `getContent()` hosts a becca note when given one. The payload comes
  from `buildFrocaRows()`, which now also reads becca notes, and holds
  only the notes the export holds, each linked to its page relative to
  the page; the root links to the export's index. `assetPath` is the
  export's `assets/`.
- `getAppViewGroups()` picks the manifest groups from the exported
  subtree: the view and type groups, the content and view groups of the
  notes a dashboard or presentation draws, `app` beside any of them and
  `scripting` for render notes. `getShareThemeGroupFiles()` resolves
  them with the groups they require.
- The export copies the English catalogues and those of the display
  language to `assets/translations/`, skipping one a locale lacks.
- The server and standalone loaders write a manifest file listed as
  `../assets/<file>` under its bare name, so the geo map's worker no
  longer fails the export with ENOENT.

An export of a subtree holding every hosted type is 30.8 MB, with 28 MB
of assets; a calendar alone ships 2.1 MB, against 37.7 MB for the old
single group. All 60 hosted pages of that export mount; the views'
`api/` requests still fail, so their data is a separate step.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 15:05:06 +03:00
Elian DoranandClaude Opus 5.5 4d73698164 build(share): split the share theme's app bundle by view and note type
The share theme's manifest listed the app's views as one `app` group of
37.7 MB, so a page hosting a calendar listed the geo map, the
spreadsheet and the text editor with it. The client build now derives
the groups from the type tables that load the views, so nobody keeps a
list of chunks by hand:

- `view:<viewType>` from `ViewComponents` in `NoteList.tsx`;
- `type:<noteType>` from `TYPE_MAPPINGS`, overridden by the share
  theme's own `SHARE_NOTE_VIEWS`;
- `content:<noteType>` from the new `CONTENT_RENDERERS` table in the
  client's `content_renderer.ts`, which the dashboard and the
  presentation draw their notes with.

Each group holds the files its entry reaches; a dynamic import stops at
another group's entry or at a module a table gates, a static import is
always followed. A group lists the groups it reaches in the new
`requires`, and leaves out their files: `app` is now 1.5 MB, the base
every hosted view requires. `mermaid` and `scripting` (the frontend
script API, which pulled CKEditor into every group through the render
note's bundle loader) are chosen by content, so a group requires them
only when they are its own entry.

The hosted view and note types move to commons as
`SHARE_HOSTED_VIEW_TYPES` and `SHARE_HOSTED_NOTE_TYPES`; core gates
hosting by them, and the build fails when one has no group. Files the
build writes outside `src/`, such as the geo map's MapLibre worker in
`assets/`, are listed as `../assets/<file>`.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 15:05:05 +03:00
Elian DoranandClaude Opus 5.5 c063a4cd8f fix(share): keep locked notes and share credentials out of a hosted view's notes
A hosted view's page embeds the notes the view reads, which
`hostInAppView()` collected with `buildFrocaPayload()` without the
page's access check. A child note locked by `#shareCredentials` of its
own went into the page whether or not the visitor had presented them,
and every `#shareCredentials` label went in as it was, so the page's
source held the password of the note and of any locked child.

`hostNoteView()` and `renderCollectionView()` pass the page's
`canAccessEmbed` on to `buildFrocaPayload()`, so the payload holds the
notes the visitor can read, as the embeds and the API route already do.
`buildFrocaRows()` leaves out `#shareCredentials` altogether, for the
page and for the API route.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 12:53:09 +03:00
Elian DoranandClaude Opus 5.5 1ddb2117ae fix(share): lay out a list collection's subpages as a list
A list collection takes `#viewType=list` from its built-in template,
`_template_list_view`, which lives in the hidden subtree and so is never
in shaca. `getChildLinksLayout()` read only the note's own label, so a
shared list collection showed its subpages as a grid of cards.

The page model cannot reach the template, so `getChildLinksLayout()`
takes the view type from its caller, and core passes `getViewType()`,
which falls back on the built-in template as it does for the hosted
views. `getViewType()` now also takes the becca note the static export
renders, which reads its templates itself.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 12:49:20 +03:00
Elian DoranandClaude Opus 5.5 b5610e79fd feat(share): show shared presentations with the app's presentation view
A presentation collection is hosted like the other collections:
`"presentation"` joins `HOSTED_VIEW_TYPES`. The slides load their content
and their vertical slides through the share's froca source, as dashboard
widgets do, and the theme, the overview and the full-screen button work
as in the app.

The "Edit slide" button opens the current slide's note to change it, so
a read-only presentation, as a shared one always is, leaves it out.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 12:47:01 +03:00
Elian DoranandClaude Opus 5.5 8e9e53256c feat(share): show shared dashboards with the app's dashboard, read-only
A dashboard collection is hosted like the other collections:
`"dashboard"` joins `HOSTED_VIEW_TYPES`.

A read-only dashboard, as a shared one always is, now leaves its widgets
where they are: the grid is static, so they can be neither dragged nor
resized, a note dragged from the tree is refused with the usual locked
message instead of being cloned in, and a widget's menu has no "Remove".
"Refresh" stays for render notes and web views. The menu adds each
separator with the group after it, so the shorter menu does not end on
one.

The grid now also takes `staticGrid` when it is built. The effect calling
`setStatic()` runs only when the state changes, so a grid rebuilt for
another note lost it, which a collapsed single-column dashboard hit as
well.

The Collections row of the sharing page still called the calendar, table
and board unsupported; it now lists the view types shown as in the app.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 12:40:58 +03:00
Elian DoranandClaude Opus 5.5 19d422f68a feat(share): complete attribute names from the notes a visitor can read
The search editor completes attribute names with `fetchAttributeNames()`,
which called the app's `attribute-names` route: a 404 on a shared page,
and a route that lists the names of every attribute in the database,
which are none of a visitor's business.

- `FrocaSource` gains `getAttributeNames()`, which `fetchAttributeNames()`
  calls; the app's source sends the request it sent before.
- `/share/api/attribute-names` collects the names from the attributes of
  shared notes only, leaving out protected notes and notes behind
  credentials the caller did not give, then adds the built-in names.
- `getAttributeNames()` keeps reading the database for the app and hands
  the rest to `completeAttributeNames()`, which adds the built-in names,
  drops the links Trilium keeps itself and lists prefix matches first,
  for both routes alike.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 12:36:05 +03:00
Elian DoranandClaude Opus 5.5 79f0dd907e fix(share): give the app views Boxicons' utility classes
`boxicons-compat.css` keeps Boxicons' utility classes for the icon packs:
the animations, rotation, flipping, sizes, borders and `.bx-empty`. Only
the app's `style.css` loaded it, so on a shared page a spinner stood
still, a rotated arrow pointed the wrong way, and the unchecked entries
of a table's sort submenu, whose `bx-empty` icon had no width, sat out of
line with the checked one. `app_globals.ts` now loads it with the app
views, beside the app's menu and button styles.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 12:32:21 +03:00
Elian DoranandClaude Opus 5.5 a687524a12 feat(share): filter a shared collection through the share
A collection's filter searched with `server.get("search?…")` and the
search editor checked a query with `server.post("search/lint")`, which a
shared page has no API for: typing in a shared board's filter answered
404s and error toasts.

- `FrocaSource` gains `searchInSubtree()` and `lintSearch()`, which froca
  passes through and `services/search.ts` and the search editor call. The
  app's source sends the same requests as before.
- `/share/api/search` runs the subtree search of the app's route, with the
  same search options, below a note the caller may access, and answers
  only the matches a visitor can read: shared, not protected, behind
  credentials the caller gave and visible in the share tree, with the
  tokens to highlight and the query's error.
- `/share/api/search/lint` reads a search string without running it.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 12:32:16 +03:00
Elian DoranandClaude Opus 5.5 e5c83394f5 feat(share): show shared table collections with the app's table view
A table collection is hosted like the other collections: `"table"` joins
`HOSTED_VIEW_TYPES`.

The table draws a row's title and a relation's target as
`.reference-link` spans with a `data-href` into the note tree, which the
app's global link handler opens; that handler stays off on a shared page,
so a click on them did nothing. `ShareAppHost` now catches a click on any
link into the note tree a hosted view renders, a `data-href` or an `<a>`
whose `href` starts with `#`, and opens the note's shared page when the
share holds it, as `hasLink()` on `HostedApp` tells. A link to a note the
share does not hold, or a plain fragment such as a footnote, is left
alone.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 12:20:20 +03:00
Elian DoranandClaude Opus 5.5 022e1f8997 style(share): pad shared collections and draw one border under the title
- The app's collection views pad themselves by `--content-margin-inline`,
  which the app's `.scrolling-container` sets and a shared page did not,
  so a shared board ran to the edges. The app view's container now sets
  it, 24px and 8px on a narrow screen, as the app does.
- The title row and the collection bar below it each drew a bottom
  border. A page holding a collection bar leaves the title row's out.
- A shared page shows the collection bar only for a view with controls
  of its own in it, so a geo map no longer gets an empty bar.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 12:15:03 +03:00
Elian DoranandClaude Opus 5.5 865cb18a50 feat(share): show shared boards with the app's board
A board joins the collections a shared page shows with the app's own
view, its view type read from its built-in template where it has none of
its own.

The board's API takes `onOpenNote`, set on every render from the view's
props as `noteContext` is: `openNote()`, which a click on a card, a link
in a card and quick edit go through, opens the note on its shared page in
place of the quick edit popup, and `openCard()` opens the target of a
redirecting card the same way. The app passes none, so its board is
unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 12:12:42 +03:00
Elian DoranandClaude Opus 5.5 f83318234b style(share): draw the app views' buttons as the app does
A shared calendar's toolbar showed browser buttons: the look of
`ActionButton`, `Button` and their groups comes from Bootstrap, from
`style.css` and from the Next theme's `forms.css`, none of which a shared
page loads. The collection properties bar itself already brought its own
stylesheet.

The button rules move, unchanged, into stylesheets of their own the
share can load, as the menus did:

- `stylesheets/buttons.css` holds `style.css`'s `.icon-action` rules,
  imported after `menus.css`. They stand earlier in `style.css` than
  before, after only single-class rules on tables, forms, modals and
  badges, none of which reaches a button.
- `theme-next/buttons.css` holds the Next theme's command buttons,
  low-profile buttons, icon buttons and button groups, the head of
  `forms.css`, which imports it in their place.

The rules stay out of `ActionButton`'s own stylesheet: `.icon-action` is
worn by elements of sixteen other modules, and a component's stylesheet
is injected after the linked `style.css`, which would reorder it against
Bootstrap's `.btn` on a framed `ActionButton`, which wears both.

`app_globals.ts` imports both after the app's `menus.css`, in the order
the app loads them, and the share theme carries the Next theme's button
tokens in both modes.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 12:05:12 +03:00
Elian DoranandClaude Opus 5.5 b8547a33c4 fix(share): draw the separators of a shared page's menus
Bootstrap draws a menu's separator with `border-top: 1px solid
var(--bs-dropdown-divider-bg)`, a variable built from
`--bs-border-color-translucent`, which only Bootstrap's `root` partial
defines. The share theme's subset of Bootstrap left that partial out, so
the declaration was invalid at computed-value time and the separator had
no border; with no height either, its top and bottom margins collapsed
into one. Measured on a relation map's menu, items stood 8px apart across
a separator on the share against 17px in the app.

The subset now imports `bootstrap/scss/root`, as the app's Bootstrap does,
which defines the other `--bs-*` variables the menus and tooltips read as
well. It holds only variables but for a `color-scheme` scoped to
`[data-bs-theme="dark"]`, which a shared page never sets.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 12:04:08 +03:00
Elian DoranandClaude Opus 5.5 1aef217845 style(share): align the context menu's icons with their text
The app lifts the icons of its context menu (`#context-menu-container`)
by 1px, in `theme-next/shell.css`, which a shared page does not load, so
the share's context menus took the 2px of the dropdown menus. Measured on
the same relation map, an icon's middle stood 1.7px above its text's on
the share against 0.7px in the app; with the share giving the context
menu the app's offset, both stand at 0.7px.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 12:01:30 +03:00
Elian DoranandClaude Opus 5.5 4570bafbde fix(share): keep a shared view's stored config on the page
`ViewModeStorage` read a view's config through froca, which a shared page
points at the share, but wrote it with `server.post()`, which a shared
page has no API for: moving a shared map posted to
`share/undefinednotes/…/attachments` and raised an error toast.

`FrocaSource` gains `saveAttachment()` and `removeAttachment()`, which
froca passes through, and `ViewModeStorage` saves and cleans up through
them. The app's source sends the same requests as before; the share's
does nothing, as a visitor cannot change notes, so a moved map or a
switched calendar view holds while the page is open.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 11:55:43 +03:00
Elian DoranandClaude Opus 5.5 cd4c82d68d feat(share): run a shared render note's script as the app does
A shared render note mounts the app's render widget as an app view that
takes the page, which runs its target's bundle: the HTML it holds, or the
JSX component it exports.

- The widget reads the bundle through `froca.getScriptBundle()`, a method
  of `FrocaSource` beside `getNoteMap()` and `getRelationMap()`. The app's
  source posts to `script/bundle/<noteId>` as before; the share's calls
  `GET /share/api/script/bundle/:noteId`.
- The share route checks access to the script note and builds its bundle
  with `getScriptBundleForFrontend()`, answering it only when every note
  of `allNoteIds` is shared, not protected and readable with the
  visitor's credentials, as the bundle carries the source of each. It
  refuses a backend script, and a module it cannot read, before building
  anything: `getScriptBundleForFrontend()` would otherwise toast every
  connected client about a backend script a visitor asked for.
- `render()` and `renderIfJsx()` take the component to mount a JSX
  render under, which the widget hands down from its `ParentComponent`
  context, with the closest `.component` element's as the fallback.
  `glob.getComponentByEl()` exists only in the app, so a JSX render note
  could not mount on a shared page.

Core hosts a render note with no content of its own, so the page shows
the spinner until the script has run.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 11:52:52 +03:00
Elian DoranandClaude Opus 5.5 51bfa43e49 fix(share): show a collection with its template's icon
A collection takes its icon from its built-in template, such as
`_template_board`, which lives in the hidden subtree and so is never in
shaca. `SNote.getIcon()` read only the labels shaca could resolve, so a
shared board, calendar or map wore the generic book icon in the tree, in
subpage lists and in its page's title row.

`SNote.getBuiltInTemplateLabelValue()` reads a label from the built-in
templates the note names with `~template`, through becca. `getIcon()`
falls back on it when the note has no `#iconClass` of its own, keeping
the icon pack filter, and `getViewType()` and `isSubtreeHidden()` use it
in place of their own walks over the template relations.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 11:50:44 +03:00
Elian DoranandClaude Opus 5.5 6c1218ca8b test(share-theme): give the search and speculation specs a complete glob
`types.d.ts` requires `isShare` on `window.glob`, which the boot script
always sets, but four specs assigned it without, failing `pnpm
typecheck` once the share theme's project was checked again.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 11:49:49 +03:00
Elian DoranandClaude Opus 5.5 10f49d0726 feat(share): open a link's note in a browser tab or window from its menu
A link's context menu on a shared page offered the app's ways of opening
a note: a new tab, split or window, and the quick edit popup. The page
has no tab bar, no splits and no popup, so the tab opened in the host's
`TabManager` where nothing shows it, and the others did nothing.

On a shared page, `getItems()` now offers only a new tab and a new
window, and `handleLinkContextMenuItem()` opens the note's shared page
in a browser tab, or a browser window (`popup`), instead of running the
app's commands; a note with no shared page opens nowhere. The menu
learns the shared page of a note from `setShareLinkResolver()`, which
the share host sets to the links froca's share source collects, so every
link menu a hosted view shows follows it.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 11:49:49 +03:00
Elian DoranandClaude Opus 5.5 d7cdee8b4b feat(share): hide the children of a #subtreeHidden note from the shared tree
The share's navigation tree listed every child of a note that keeps them
out of the app's tree, so a shared board repeated its cards below it.

- A note with `#subtreeHidden` lists none of its children in the tree; a
  badge counts them, after the app's tree badge. The page of one of them
  lists only that child below its parent, so the page keeps its place.
- The previous and next links skip those children, and a page below such
  a note has neither, as the order no longer reaches it.
- `getNavigationTree()` takes an `isSubtreeHidden` option and gives each
  entry a `hiddenChildCount`; `getPrevNextLinks()` takes the same check.
  The page model stays free of core: core passes the check in.
- A board has the label from its built-in template, which shaca does not
  hold, so `isSubtreeHidden()` reads the template's label through becca
  when the note has none of its own, as `getViewType()` does; the static
  export's `BNote` resolves templates itself.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 11:46:34 +03:00
Elian DoranandClaude Opus 5.5 5401080cd8 style(share): draw the app views' context menus as the app does
A context menu opened on a shared page, such as a map marker's, came up
as a bare list: its look comes from Bootstrap's dropdown, from
`style.css` and from the Next theme's `base.css`, none of which a shared
page loads.

The menu rules move, unchanged, into stylesheets of their own the share
can load:

- `stylesheets/menus.css` holds `style.css`'s two menu blocks, the
  dropdown surfaces and rows (with the dropdown portal and the Tabulator
  and Excalidraw popups sharing their selectors) and the context menu's
  cover, container, mobile sheet and submenus. `style.css` imports it
  after its other imports; every other rule of `style.css` on those
  elements follows both blocks, as before.
- `theme-next/menus.css` holds the Next theme's MENUS section, imported
  after the theme's other parts, which it followed before; what precedes
  it in `base.css` is fonts, tokens and `body` rules.

`app_globals.ts`, which an app view loads first, imports Bootstrap's
dropdown beside its tooltip (`tooltip.scss` becomes `bootstrap.scss`),
then the app's `menus.css`, then the theme's, in the order the app loads
them. The share theme carries the Next theme's menu tokens in both
modes, and `setupAppView()` marks the body `desktop`, which many of the
theme's menu rules are scoped by and which matches `isMobile()` being
false on a shared page.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 11:45:06 +03:00
Elian DoranandClaude Opus 5.5 0feed43a07 style(client): draw tooltips from one component stylesheet, on shared pages too
The app view of a shared page opens Bootstrap tooltips (the overlay
buttons' titles), but a shared page loads neither Bootstrap's stylesheet
nor the app's, so they showed as bare text at the foot of the page.

The bubble's rules come together in `widgets/react/tooltip.css`, which
`hooks.tsx` imports beside `useStaticTooltip()`: the stacking layer, the
full opacity and the bubble from `style.css`, and the Next theme's look
from `theme-next/shell.css`, now read from tokens (`--tooltip-padding`,
`--tooltip-border`, `--tooltip-border-radius`, `--tooltip-box-shadow`,
`--tooltip-backdrop-filter`, `--tooltip-font-size`,
`--tooltip-foreground-color`, `--tooltip-arrow-display`) whose fallbacks
are what the legacy themes drew. The Next theme sets only tokens, adding
`--tooltip-border` and `--tooltip-arrow-display`; its rule block goes.
The rules keep `!important` on the properties they held it on, so the
help tooltip, the about dialog, the charts, the color scheme switcher and
the note tooltips still win where they did.

A shared page builds Bootstrap's tooltip from its own partial
(`share-theme/src/content/tooltip.scss`, about 3.4 kB), which only
`@import` loads, so the client's Sass options silence that deprecation
and quiet the dependencies'. `app_globals.ts`, which an app view loads
first, imports it, so it precedes the views' `tooltip.css`. The share
theme carries the Next theme's tooltip tokens and shadow colors.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 11:38:16 +03:00
Elian DoranandClaude Opus 5.5 f85e189d72 feat(share): draw a shared relation map note with the app's relation map
A shared relation map note mounts the app's `RelationMap` as an app view
that takes the page, where it showed "This note cannot be displayed".

- The relations come through `froca.getRelationMap()`, a method of
  `FrocaSource` beside `getNoteMap()`. The app's source posts to
  `relation-map` as before; the share's calls
  `GET /share/api/relation-map/:noteId`, which reads the notes placed on
  the map from the map note's own content, so a visitor cannot ask about
  other notes, keeps those a visitor can read and answers
  `buildRelationMap()`, which `relation-map.ts` now keeps apart from its
  app route. A placed note the visitor cannot read is left off the map
  with its relations.
- `RelationMap` takes `onOpenNote`, with which a click on a note opens it
  on its shared page in place of the note pane, and a middle or modified
  click in place of a tab.
- `useEditorSpacedUpdate()` saves nothing on a shared page: the relation
  map saves its pan and zoom into the note, which a visitor cannot write.

Core hosts the note with no content of its own, as a note map note.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 11:37:12 +03:00
Elian DoranandClaude Opus 5.5 33257d40b4 style(share): draw overlay controls on the app's gray frosted surface
`OverlayControlGroup` draws its surface from `--overlay-button-background`,
which only the Next theme names, so on a shared page it fell back to the
translucent white of the menus, without frosting, and hovered in the
menus' color. The share theme now carries the Next theme's overlay
surface, hover and edge colors for light and dark, its frosting and its
hover ink, so the geo map's and the note map's controls look as they do
in the app.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 11:29:55 +03:00
Elian DoranandClaude Opus 5.5 7bfb2d635f refactor(client): give the slider its own stylesheet
The pill track and round thumb of `Slider` lived in `theme-next/forms.css`,
so the slider looked like a browser's own in the other themes and on a
shared page, which loads no app theme; on a shared note map, the map's own
rules set `appearance: none` without restyling the track, leaving a black
pill. `Slider.css` now carries them, imported by the component, with the
Next theme's colors first and names every theme and the share theme define
after them. The note map keeps only the slider's width.

The share theme's `.slider`, `.switch` and toggle icon rules styled the
on/off theme toggle that the three-way chooser replaced; they pulled the
note map's slider out of its group and over the lock button, and go.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 11:28:34 +03:00
Elian DoranandClaude Opus 5.5 ed2f97631e refactor(note_map): put the map's controls on overlay control groups
The note map was the last view floating its controls with the legacy
`content-floating-buttons`, styled from the app's global `style.css`,
Bootstrap and `theme-next/pages.css`. A shared page loads none of them,
so a shared note map showed browser buttons and a slider in an
unplaced black panel. `OverlayControlGroup` brings its own stylesheet,
as the geo map's controls do on a shared page.

- The choice of link or tree map stands at the top start, built from
  `MapTypeOverlayButtons`; `MapTypeSwitcher` stays the pane header's
  `ActionButton` group, both drawn from one `MAP_TYPES` list, and loses
  the `frame` and `className` props only the map passed.
- Fixing the nodes and the link distance stand at the bottom start, the
  lock an `OverlayControlButton` held down while the nodes are fixed.
  Both groups pass `overCanvas`, so a press on them never starts a drag.
- The slider's rules move to `.note-map-link-distance`; the theme's two
  rules for `.tn-tool-button` inside `.fixnodes-type-switcher`, which
  nothing renders any more, go.
- `OverlayControlGroup` gives a child that is not a button the chip's
  surface, ink, frosting and height, and the rounded end it stands at,
  so the slider is part of the chip rather than a hole in it.

The share theme now names `--main-text-color`, which the slider's thumb
reads, and the app's tooltip colors, which the tooltip over a hovered
note reads.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 11:23:58 +03:00
Elian DoranandClaude Opus 5.5 e25cda78cd feat(share): draw a shared note map note with the app's note map
A shared note map note mounts the app's `NoteMap` as an app view that
takes the page.

- The map reads its notes through `froca.getNoteMap()`, a method of
  `FrocaSource` beside `searchNoteIds()`. The app's source posts to
  `note-map/…` as before; the share's calls
  `GET /share/api/note-map/:noteId/:mapType`, with the relation filters as
  repeated `excludeRelation` and `includeRelation` parameters.
- The share route checks access to the map's root and builds the map with
  core's own code, `buildTreeMap()` and `buildLinkMap()`, which
  `note_map.ts` now keeps apart from the request handling of its app
  routes. It answers only the notes that are shared, not protected and
  readable with the visitor's credentials, and the links between them.
- A note map note without `#mapRootNoteId` is drawn from its parent: the
  payload names the shared note's parent, which `NoteMap` takes as
  `defaultRootNoteId` in place of the active note context's, which a
  shared page does not have.
- `NoteMap` takes `onOpenNote`, with which a click opens the note on its
  shared page and a right click opens no menu.

Core hosts a note map note with no content of its own, so the page shows
the spinner until the map mounts.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 11:17:42 +03:00