Compare commits

..

512 Commits

Author SHA1 Message Date
Philip Okugbe 2de00568d3 New translations translation.json (Portuguese, Brazilian)
[ci skip]
2026-05-20 17:56:41 +01:00
Philip Okugbe 6c647865ef New translations translation.json (Chinese Simplified)
[ci skip]
2026-05-20 17:56:40 +01:00
Philip Okugbe 576c769093 New translations translation.json (Ukrainian)
[ci skip]
2026-05-20 17:56:38 +01:00
Philip Okugbe 3331cd12cc New translations translation.json (Russian)
[ci skip]
2026-05-20 17:56:37 +01:00
Philip Okugbe 6410b67bdd New translations translation.json (Dutch)
[ci skip]
2026-05-20 17:56:35 +01:00
Philip Okugbe dfa38c704e New translations translation.json (Korean)
[ci skip]
2026-05-20 17:56:34 +01:00
Philip Okugbe 6f89789ac6 New translations translation.json (Japanese)
[ci skip]
2026-05-20 17:56:32 +01:00
Philip Okugbe f5907d5d34 New translations translation.json (Italian)
[ci skip]
2026-05-20 17:56:30 +01:00
Philip Okugbe 63c87e52fc New translations translation.json (Spanish)
[ci skip]
2026-05-20 17:56:29 +01:00
Philip Okugbe 2d78fa297a New translations translation.json (French)
[ci skip]
2026-05-20 17:56:27 +01:00
Philip Okugbe 29187f66fb New translations translation.json (German)
[ci skip]
2026-05-20 17:56:25 +01:00
Philipinho e02f0acc65 fix: add i18next_json type to crowdin 2026-05-20 17:34:34 +01:00
Philipinho adb1f27767 v0.90.0 2026-05-20 16:55:23 +01:00
Philip Okugbe 92c0e36e46 fix(a11y): WCAG 2.1 AA fixes (#2219) 2026-05-20 16:47:25 +01:00
Olivier Lambert 1c166c4736 feat(editor): add alt text support for images (#2097)
* feat(editor): add alt text support for images
* feat:  extend alt text support to videos and diagrams

---------
Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2026-05-20 16:45:59 +01:00
Philip Okugbe 66a754c9eb Revert "fix: prevent browser tab fallback in editor (#2123)" (#2216)
This reverts commit 1d2486455f.
2026-05-19 14:07:07 +01:00
Philip Okugbe 6cf8101ab3 feat(ee): templates (#2215)
* feat(ee): templates
* fix tree
* fix
2026-05-19 02:41:52 +01:00
Philipinho 0d6538ab1a feat: iframe configuration 2026-05-18 22:02:31 +01:00
Philip Okugbe b7b99cb3b2 fix: code splitting and editor fixes (#2211)
* fix table

* fix code splitting

* fix: editor ready check

* fix codeblock/mermaid gap cursor

* fix callout
2026-05-15 02:46:54 +01:00
Philipinho 03c1e8c4ed fix collab module 2026-05-14 15:06:51 +01:00
Philipinho e41518a93d fix type 2026-05-14 14:49:02 +01:00
Peter Tripp 932c1ad5b7 Better trash (#2190)
* Better trash

I recently lost a bunch of time editing and searching for pages that were actually in the Trash. Docmost intentionally tries to not link to Trashed pages, but the url of that Trashed page and any inbound links still work.  This makes it clearer when a page you are interacting with is in the Trash.

- /trash
  - Refactored banner into `trash-banner.tsx`
  - Refactored "Restore" modal into `use-restore-page-modal.tsx`
- Page (when isDeleted)
  - Add: `trash-banner.tsx`
  - Add breadcrumbs: `Parent / Child / Page (Deleted)`
  - Change: Deleted Pages are read-only
  - Replace "Move to Trash" with "Restore" in page menu (invokes `use-restore-page-modal`)

I tried very hard to keep this simple and re-use existing translation strings wherever possible.

* cleanup

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2026-05-14 14:41:10 +01:00
Julien Fontanet 82d065669d fix: page mode toggle no longer overwrites default preference (#1996)
The header edit/read toggle now controls only the current session's mode
without saving it as the user's preference. The saved preference (set in
profile settings) is applied once on initial load and sticks across page
navigations within the session, so navigating to a new page no longer
resets the mode mid-session.

Fixes #1693
2026-05-14 13:15:03 +01:00
Philip Okugbe f758091b2a perf(permissions): cache space role and page edit lookups (#2208) 2026-05-14 13:11:28 +01:00
Philip Okugbe f4af4c3fc0 feat(editor): add page break node (#2202) 2026-05-14 03:48:13 +01:00
Philipinho 3b983a27f6 sync 2026-05-14 03:01:55 +01:00
Philip Okugbe 299a9ca3c8 fix: bug fixes (#2201)
* fix(editor): hide transclusion borders and reset spacing in read-only mode

* feat(share): add full width toggle for shared pages

* feat(share): support resizing sidebar on shared pages

* fix: auto redirect if there is only one SSO provider.
- fix tighten sso redirect
- fix share tree margin

* sync

* package overrides
2026-05-14 02:54:00 +01:00
Philip Okugbe cea9be7926 feat: table enhancement (#2191) 2026-05-14 00:37:44 +01:00
Philip Okugbe 31ed0df3f7 feat(tree): replace sidebar tree (react-aborist) with custom tree implementation (#2199)
* feat(tree): replace react-arborist with custom tree implementation

* feat(tree): keyboard arrow navigation between rows

* feat(emoji-picker): focus search input on open

* refactor(emoji): switch to @slidoapp/emoji-mart fork for accessibility

* feat(tree): Home/End and typeahead keyboard navigation

* feat(tree): roving tabindex and * to expand sibling subtrees

* feat(tree): Space activation and ARIA refinements

* fix(tree): move treeitem role to focusable row + aria-current
2026-05-13 23:01:04 +01:00
Philip Okugbe a689cca7a0 feat: page labels/tags (#2188)
* feat: labels (WIP)
* full implementation
2026-05-10 18:14:15 +01:00
Philip Okugbe 537e45bc11 feat: page details section and backlinks (#2186)
* feat: page details section and backlinks
2026-05-09 17:03:08 +01:00
Philip Okugbe bdc369fce0 feat(editor): fixed toolbar preference (#2185)
* feat(editor): fixed toolbar preference

* remove key

* cleanup translation strings

* update axios
2026-05-09 13:27:03 +01:00
Philip Okugbe 2d8b470495 feat(editor): indentation (#2174)
* switch to default codeblock tab handling

* feat(editor): indentation
2026-05-08 21:40:37 +01:00
David Gallardo c66c08fa78 fix: ignore emoji when deriving avatar initials (#2167) 2026-05-08 21:36:10 +01:00
David Gallardo 6046d04375 feat(editor): replace emoji picker with browse + search (#2171)
* feat(editor): show emoji name in suggestion list

Replace the fixed-column emoji grid with a vertical list that displays
each emoji alongside its :shortcode: name. This makes the picker more
discoverable—users can see and learn shortcodes without prior knowledge.

Changes:
- EmojiList: switch from SimpleGrid/ActionIcon to UnstyledButton list
  rows showing emoji glyph + monospace 🆔 label
- Navigation simplified to ArrowUp/ArrowDown (list has no columns)
- Results capped at 8 items for a focused, scannable dropdown
- CSS module: rename menuBtn -> menuItem, tighten padding

* feat(editor): replace SearchIndex with name/id includes search

Port the exact search algorithm from the original extension:
- Build a flat index from @emoji-mart/data: { id, name (lowercase), native }
- Filter with name.includes(q) || id.includes(q) — predictable, no
  keyword indirection
- Results capped at 5 (same as extension)
- Frequently-used emojis (sorted by usage) shown when query is empty
- Remove emoji-mart init() / SearchIndex / getEmojiDataFromNative
  dependencies; index is built lazily and cached in memory
- Remove unused GRID_COLUMNS constant

* feat(editor): emoji picker with browse and search modes

When the query is empty the picker shows a category bar with 8 tabs
(people, nature, food…) and a scrollable emoji grid. Typing after ':'
switches to a compact list that shows the glyph and :shortcode: side by
side, making it easy to discover emoji names while you type.

- Category data is loaded lazily from @emoji-mart/data and cached, so
  opening the picker more than once has no overhead
- Grid keyboard nav: arrow keys move by cell/row, Enter picks
- List keyboard nav: up/down through results, Enter picks
- Mouse hover syncs the keyboard selection index in both modes
- incrementEmojiUsage tracks picks so frequently used ones bubble up
  in future sessions

* fix(editor): polish emoji picker copy and loading

* feat: add emoji to slash command

* Add keyboard support to emoji group navigation

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2026-05-08 21:33:43 +01:00
David Gallardo 5d8c11e741 fix: sync html lang with current user locale (#2165) 2026-05-08 21:15:04 +01:00
Philip Okugbe de60aa7e61 feat: synced blocks (transclusion) (#2163)
* feat: synced blocks (transclusion)

* fix:remove name

* make placeholders smaller

* feat: enforce strict transclusion schema

* fix: scope synced blocks to workspace, gate unsync on edit permission

* fix collab module error
2026-05-08 13:23:16 +01:00
Peter Tripp c9fa6e20b3 Add alias: /toc and /ol (#2161) 2026-05-08 01:20:27 +01:00
Philipinho ec51ca7815 fix request ip 2026-05-07 22:09:32 +01:00
Philipinho 2b63137217 mail 2026-05-07 18:13:24 +01:00
Philipinho 3227bc6059 fix: a11y 2026-05-04 23:04:26 +01:00
Philip Okugbe 73dc62bca3 update react-email (#2149) 2026-05-04 22:26:53 +01:00
Philipinho 3c74bb3dee update package 2026-05-04 22:09:19 +01:00
Philip Okugbe dbe6c2d6ba feat: A11y fixes (#2148) 2026-05-04 21:21:37 +01:00
Sarthak Chaturvedi fe18f22dc6 fix: prevent code block deletion when adding inline comments in read mode (#2146) 2026-05-04 21:14:21 +01:00
Philipinho fcef0c6b96 fix: S3 2026-05-04 20:57:35 +01:00
Philipinho 17f3158a3b update aws packages 2026-05-01 20:00:20 +01:00
Philipinho b74ca00bfd sync 2026-05-01 14:57:32 +01:00
Philip Okugbe c247d4c1e3 feat(ee): PDF import (#2142)
* feat: replace pdfjs-dist with firecrawl-pdf-inspector

* use modified firecrawl-pdf-inspector

* feat: pdf import

* increase single file upload size limit

* use npm package

* sync

* update package
2026-05-01 14:56:39 +01:00
Philip Okugbe 641ce142df feat(ee): SCIM (#1347)
* SCIM - init (EE)

* accept db transaction

* sync

* Content parser support for scim+json

* patch scimmy

* sync

* return early if userIds is empty

* sync

* SCIM db table

* fixes

* scim tokens

* backfill

* feat(audit): add scim token events

* rename scim migration

* fix

* fix translation

* cleanup
2026-05-01 14:53:30 +01:00
Sarthak Chaturvedi 1d2486455f fix: prevent browser tab fallback in editor (#2123) 2026-05-01 13:58:51 +01:00
Philipinho a0aea43e25 feat(saml): allow disabling RequestedAuthnContext via env var
Adds SAML_DISABLE_REQUESTED_AUTHN_CONTEXT env var, passed through
    to the SAML strategy's disableRequestedAuthnContext option.
    Defaults to existing behavior (element sent). Set to true to omit
    the element when the IdP authenticates the user with a method that
    does not match (e.g. MFA, FIDO, passwordless), which would
    otherwise cause AADSTS75011 with Microsoft Entra ID.
2026-05-01 11:47:03 +01:00
Philip Okugbe 09c69d7a0f feat: properly preserve table width (#2143) 2026-05-01 00:49:31 +01:00
Sarthak Chaturvedi 9943e104a5 fix(i18n): Correct German column count label rendering (#2131) 2026-05-01 00:37:59 +01:00
Peter Tripp b16f1e5a55 fix: ctrl-k behavior on macOS (#2052)
* Improve cmd-k / ctrl-k behavior

Use cmd-k on macOS/iOS for search and keep ctrl-k everywhere else.

Fixes a bug where ctrl-k on macOS, which cuts to the end of the line,
was also triggering the search prompt.

* comment submit: cmd-enter (mac) / ctrl-enter (win/linux)
2026-05-01 00:36:40 +01:00
Philip Okugbe 24be90b95f fix: duplicate PDF uploads (#2139) 2026-04-29 10:01:47 +01:00
Olivier Lambert 3ecf27c6b0 fix(page-permission): make people-with-access list scroll past 4 entries (#2137)
The "People with access" list in the page share modal used
<ScrollArea mah={250}>, which caps the container height but does not
make the inner viewport scroll (no fixed height is given to the
viewport). Items beyond ~4 entries were rendered correctly but clipped
out of view.

Switches to <ScrollArea.Autosize mah={400}>, which is Mantine's
dedicated primitive for "grow with content up to a max, then scroll".

Closes #2135
2026-04-29 09:36:38 +01:00
Philipinho 980521f957 v0.80.1 2026-04-27 16:06:32 +01:00
Philipinho fe44dc92a9 sync 2026-04-27 15:51:23 +01:00
Philip Okugbe fad410ef23 chore: add undici for oidc proxy support (#2132) 2026-04-27 15:50:42 +01:00
Philipinho 15b8908b1a update postcss 2026-04-27 15:23:47 +01:00
Philipinho 8e15b22d8c package updates 2026-04-27 15:22:02 +01:00
Philipinho ec83fc82d5 fix: refactor sanitize 2026-04-27 15:16:26 +01:00
Philipinho a573acedd0 fix: local storage, and package overrides 2026-04-22 14:13:25 +01:00
Philipinho dba8e315ab override 2026-04-14 17:59:59 +01:00
Philipinho 81ae7a17a6 confirm dialog 2026-04-14 17:56:36 +01:00
Philipinho 271f855761 v0.80.0 2026-04-14 17:08:44 +01:00
Philipinho 3e6d915227 sync 2026-04-14 16:34:44 +01:00
Philip Okugbe a6a7e4370a feat(ee): PDF export api (#2112)
* feat(ee): server side PDF export

* feat: pdf export queue

* sync

* sync
2026-04-14 16:26:54 +01:00
Philip Okugbe cc00e77dfb fix: space overview favorites (#2110) 2026-04-14 02:58:24 +01:00
Philipinho 66c70c0e76 fix print 2026-04-14 00:40:17 +01:00
Philip Okugbe 0e8b3bbfb3 New Crowdin updates (#2109)
* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)
2026-04-14 00:05:51 +01:00
Philip Okugbe a3a9f35005 fix home flickers (#2108) 2026-04-13 23:54:03 +01:00
Philip Okugbe 4056bd0104 feat: enhancements (#2107)
* refactor
* fix
* update packages
2026-04-13 23:34:40 +01:00
Philip Okugbe bd68e47e03 feat(ee): page verification workflow (#2102)
* feat: page verification workflow

* feat: refactor page-verification

* sync

* fix type

* fix

* fix

* notification icon

* use full word

* accept .license file

* - update templates
- update migration and notification

* fix copy

* update audit labels

* sync

* add space name
2026-04-13 20:20:34 +01:00
Philip Okugbe d6068310b4 Merge commit from fork
Refactor link.ts to simplify HTML parsing and rendering logic.
2026-04-13 01:09:36 +01:00
Philipinho e02661974e sync 2026-04-13 00:13:18 +01:00
Philip Okugbe 1113f17a43 New Crowdin updates (#2104) 2026-04-12 22:46:39 +01:00
Philip Okugbe d42091ccb1 feat: favorites (#2103)
* feat: favorites and templates(ee)

* rename migrations

* fix sidebar

* cleanup tabs

* fix

* turn off templates

* cleanup

* uuid validation
2026-04-12 22:06:25 +01:00
Philip Okugbe 57efb91bd3 feat(ee): ai chat (#2098)
* feat: ai chat

* feat: ai chat

* sync

* cleanup

* view space button
2026-04-10 19:23:47 +01:00
Philip Okugbe da9b43681e feat: watch space (#2096) 2026-04-09 00:37:51 +01:00
Philipinho 4966f9b152 fix(deps): package updates 2026-04-07 10:24:46 +01:00
Philipinho e1bbceb9a6 fix: logs 2026-04-07 10:10:41 +01:00
Philip Okugbe 895c1817ae feat: bug fixes (#2084)
* handle enter in inline code

* fix: duplicate comment cache

* track link nodes (backlinks)

* fix en-US translation

* fix internal a-links

* overrides

* 0.71.1
2026-04-05 13:45:36 +01:00
Philip Okugbe 642024ba9d New Crowdin updates (#2078) 2026-03-31 21:14:41 +01:00
Philipinho 147d028036 v0.71.0 2026-03-31 20:42:37 +01:00
Philipinho 992691e6e0 fix module import 2026-03-31 20:41:09 +01:00
Philip Okugbe 9aaa6c731c feat: add AI_EMBEDDING_SUPPORTS_MRL env var to decouple pgvector dimensions from model API (#2079)
Some embedding models don't accept a `dimensions` parameter. This adds
an optional env var that controls whether the dimension is sent to the
model API, while always using it for pgvector indexing. Preset models
have this handled automatically; the env var allows explicit override
for custom models.
2026-03-31 19:39:49 +01:00
Philipinho fd91b11c6c pin version 2026-03-31 16:06:44 +01:00
Philipinho af8b0ddf3a sync 2026-03-31 16:05:09 +01:00
Philip Okugbe 879aa2c3d8 feat: page update notifications (#2074)
* feat: watchers notification and email preferences

* fix: email copy

* digests

* clean up

* fix

* clean up

* move backlinks queue-up to history processor

* fix

* fix keys

* feat: group notifications

* filter

* adjust email digest window
2026-03-31 16:03:59 +01:00
Philip Okugbe c180d0e487 feat: ratelimits (#2073)
* feat: rate limits

* ip
2026-03-30 15:38:44 +01:00
Philip Okugbe a062f7a165 fix: enhance confluence importer (#2072)
* fix placeholder

* min resize dimensions

* fix media links

* fix
2026-03-30 13:16:40 +01:00
Philip Okugbe cbd0dd4a0b feat: indexes (#2071) 2026-03-29 20:29:12 +01:00
Philip Okugbe 2d6d829581 New translations translation.json (English) (#2066) 2026-03-29 16:25:45 +01:00
Philipinho 5cea30cc5c fix markdown paste 2026-03-29 16:11:21 +01:00
Philipinho bca85a49d6 pin marked version 2026-03-29 03:03:35 +01:00
Philipinho c9cdfa0f17 fix 2026-03-29 02:20:56 +01:00
Philip Okugbe 412962204c fix: editor fixes (#2067)
* autojoiner

* fix marked

* return clipboardTextSerializer as markdown

* fix clipboardTextSerializer for single lines

* cleanup two preceeding spaces in ordered lists item

* fix extra paragraph in task list

* don't zip sinple page exports
2026-03-29 02:19:09 +01:00
Olivier Lambert a42ac3d450 fix: strip trailing whitespace-only paragraphs from pasted content (#2050) 2026-03-28 22:26:47 +00:00
Philipinho 642c92f779 fix select 2026-03-28 20:34:44 +00:00
Philipinho ccb35517bb sync 2026-03-28 20:29:31 +00:00
Philip Okugbe cbdb37ed0a New Crowdin updates (#2061) 2026-03-28 20:29:06 +00:00
Julien Fontanet aa27d57624 fix: notification items are now real links (#2039)
Replace UnstyledButton with UnstyledButton component={Link} so each
notification renders as a real anchor element. Regular left-clicks use
SPA navigation and close the popover; Ctrl/Cmd/middle-click open the
page in a new tab. All click types mark the notification as read.
2026-03-28 20:23:21 +00:00
Philip Okugbe 3829b6cbef feat(ee): viewer comments (#2060) 2026-03-28 19:32:52 +00:00
Philipinho 17da762984 overrides 2026-03-28 19:28:22 +00:00
Philipinho 859f16740b tooltip portal 2026-03-28 19:19:00 +00:00
Philip Okugbe 7981ef462e feat(editor): audio and PDF nodes (#2064)
* use local resizable

* feat: aduio

* support audio imports

* feat: use confluence real file names

* cleanup

* error handling

* hide notice

* add audio

* fix pulse

* Fix import and export

* unify pulse

* hide in readonly mode

* keywords

* keyword

* translations

* better sort

* feat: PDF embed

* cleanup

* remove audio menu

* open active

* hide focus on readonly mode

* increase iframe default dimension
2026-03-28 17:33:29 +00:00
Philip Okugbe 2d835da0e3 New Crowdin updates (#2059) 2026-03-27 22:11:19 +00:00
Philipinho a3559b7c33 sync 2026-03-26 20:01:02 +00:00
Philip Okugbe 803f1f0b81 feat: user session management (#2056)
* user session management

* WIP

* cleanup

* license

* cleanup

* don't cache index

* rename current device property

* fix
2026-03-26 20:00:04 +00:00
Philipinho 4e8f533b91 override 2026-03-26 16:48:33 +00:00
Philipinho 7b0d8fe140 override 2026-03-26 16:46:40 +00:00
Philipinho 2f92278a9d sync 2026-03-26 16:35:05 +00:00
Philipinho 53608eae35 clean up ws 2026-03-26 13:59:17 +00:00
Philipinho 0e4a1e7419 enum validation 2026-03-26 00:41:38 +00:00
Philipinho 9125996e97 sync 2026-03-25 10:08:36 +00:00
Philip Okugbe fa4872e89e fix(deps): package updates (#2041)
* update
* overrides
* override
* fix page update mutation
* fix
* cleanup
* loader
* fix excalidraw package
* override
* fix regex
2026-03-25 10:07:01 +00:00
Philipinho 6d6f3a8a8e merge commit 2026-03-24 10:52:09 +00:00
Philip Okugbe 975b4dcaab feat: auth pages layout (#2042)
* auth pages layout
* exclude home route from redirect
* fix margin
2026-03-22 16:40:50 +00:00
Philip Okugbe 6683c515cf fix: make codeblock language detection performant (#2032)
* fix: make codeblock language detection performant
* lint
2026-03-17 20:40:22 +00:00
Philipinho cc5c800238 0.70.3 2026-03-17 14:29:09 +00:00
Philipinho cfaee93af9 fix 2026-03-17 14:28:22 +00:00
Philipinho 74eddb0638 v0.70.2 2026-03-16 13:49:50 +00:00
Philipinho 7c83a9d4f0 update dompurify 2026-03-16 13:49:20 +00:00
Philipinho 2678c4e279 fix 2026-03-16 00:32:30 +00:00
Philipinho b0bde4b375 feat: replace link popover with dedicated bubble menu 2026-03-16 00:26:03 +00:00
Philipinho 724e37d5b7 revert 2026-03-15 23:03:32 +00:00
Philipinho 33184e9d8d sync 2026-03-15 22:07:26 +00:00
Philip Okugbe 7520c329d0 fix notion importer (#2027)
* fix notion importer

* fix link selector on mobile
2026-03-15 22:06:40 +00:00
Philip Okugbe d7a5fda53c feat: better feature flags (#2026)
* feat: feature flag upgrade

* fix translations

* refactor

* fix

* fix
2026-03-15 22:05:32 +00:00
Philipinho 236a63dadc sync 2026-03-15 17:09:29 +00:00
Philip Okugbe 89b94e5d19 feat: refactor link menu (#2025)
* link markview - WIP

* WIP

* feat: refactor links

* cleanup
2026-03-15 17:08:59 +00:00
Philip Okugbe 97c459be67 feat(cloud): add find-workspace and email verification endpoints (#2020)
* feat: add find-workspace and email verification endpoints
* sync
2026-03-14 13:36:30 +00:00
Philip Okugbe d0ed6865cb fix page level comment on mobile (#2018)
* add icon next to comment box
2026-03-14 01:01:24 +00:00
Philip Okugbe 65b89a1b24 fix email button (#2017) 2026-03-14 00:40:32 +00:00
Philip Okugbe 1fdee33206 feat(editor): add auto-save and unsaved changes protection for diagrams (#2011)
* feat(editor): add auto-save and unsaved changes protection for diagrams
* 30 seconds
2026-03-13 17:58:29 +00:00
Philip Okugbe 7b69727a30 fix shared page mention view for non-logged in users (#2008) 2026-03-11 19:25:27 +00:00
Philip Okugbe 66c26af34b noop audit module (#1994) 2026-03-05 09:29:39 +00:00
Philip Okugbe b4f009513e fix: resize handle clipping (#1990) 2026-03-04 12:24:46 +00:00
Philipinho fcffa3dfa0 fix media 2026-03-04 12:08:08 +00:00
Philipinho 1980b94825 0.70.1 2026-03-04 11:57:31 +00:00
Philip Okugbe bea1637519 fix: image fallback regression (#1989)
* fix: image fallback regression

* fix image preview on upload

* fix image loading
2026-03-04 11:51:43 +00:00
Philip Okugbe 37355452e1 update release workflow 2026-03-03 20:25:39 +00:00
Philipinho 057360c6be fix: validate import size 2026-03-03 20:00:05 +00:00
Philipinho f12bfc1ff7 fix menu positioning 2026-03-03 18:28:55 +00:00
Olivier Lambert f5d794220e fix: resolve keystroke input being swallowed after link in Firefox (#1922)
* fix: resolve keystroke input being swallowed after link in Firefox

In Firefox, when the cursor is at the right boundary of a link mark,
contenteditable inserts new text inside the <a> element. ProseMirror
then rejects the DOM mutation because the link mark has inclusive: false,
causing keystrokes to be silently swallowed. Unlike Chrome, Firefox also
does not fire ProseMirror's handleTextInput callback in this state.

This adds a ProseMirror plugin that intercepts printable character
keydowns at link mark boundaries and programmatically inserts the text
without the link mark, bypassing Firefox's native contenteditable
behavior entirely.

Fixes #1773

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve keystroke input being swallowed before a link in Firefox

Extend the linkBoundaryInput plugin to also handle the left boundary
of links, where the cursor is just before a link (e.g. at the start
of a line). Firefox inserts text inside the <a> element in this case
too, causing ProseMirror to reject the mutation.

Fixes #1748
2026-03-03 17:19:03 +00:00
faruk-agentiqus a3c1c6cccd fix(editor): disable slash and emoji menus inside code blocks (#1897)
The slash command menu (/) and emoji menu (:) were incorrectly
triggering when typing inside code blocks, breaking keyboard
navigation and confusing users who type paths like /work or
symbols like := in their code.

Added an `allow` function to both SlashCommand and EmojiCommand
extensions that checks if the cursor is inside a code block and
disables the menu accordingly.

Closes #1730
2026-03-03 16:51:00 +00:00
MATHEUS LUIS LORSCHEITER 4b105586a9 fix(client): ensure sidebar remains visible on shared subpages (#1887)
* fix(client): ensure sidebar remains visible on shared subpages

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-03 16:48:53 +00:00
Philip Okugbe d2641db895 New Crowdin updates (#1984)
* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)
2026-03-03 16:44:49 +00:00
Philipinho 1111df65cd fix type 2026-03-03 16:22:00 +00:00
Philipinho e455154b7d fix 2026-03-03 16:14:35 +00:00
Philipinho ef24b3c07d feat: API key restriction 2026-03-03 16:07:08 +00:00
Philipinho 2352f3c5d9 sync 2026-03-03 14:44:16 +00:00
Philipinho 568dd4c321 fix headings 2026-03-03 14:17:51 +00:00
Philipinho b6478fee84 fix imports 2026-03-03 13:57:10 +00:00
Philip Okugbe 5d2aad3668 New Crowdin updates (#1978)
* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)
2026-03-03 00:22:51 +00:00
Philipinho 9331ac2df8 v0.70.0 2026-03-03 00:13:01 +00:00
Philipinho 9f4728e279 fix 2026-03-03 00:08:20 +00:00
Philipinho 628b08339a UI tweaks 2026-03-02 22:56:05 +00:00
Philipinho 68842dbea2 comments view 2026-03-02 22:18:57 +00:00
Philipinho b1510cd6d7 fix 2026-03-02 22:09:57 +00:00
Philipinho af92224e10 github actions 2026-03-02 21:52:58 +00:00
Philipinho c24ff44e09 fix(deps): update dependencies 2026-03-02 21:44:24 +00:00
Philipinho 90c190df78 fix: space members view enhancement 2026-03-02 21:33:15 +00:00
Philipinho 17ec2f4ac5 lists sorting 2026-03-02 21:07:47 +00:00
Philipinho 9881c53f00 feat: spaces and groups search 2026-03-02 20:40:38 +00:00
Philipinho 721651e2e2 feat: user deactivation 2026-03-02 19:05:10 +00:00
Philip Okugbe a3fd79dae8 fix: spreadsheets paste (#1982) 2026-03-02 17:37:56 +00:00
Philipinho 616d9297eb sync 2026-03-02 04:08:59 +00:00
Philipinho ee6b98edaa * enhance ai menu
* remove api prefix from mcp
2026-03-02 03:31:52 +00:00
Philipinho cf43e2b4fe feat: enhance embed resizer 2026-03-02 02:45:13 +00:00
Philipinho 614baf153b fix: show resize handle if node is selected 2026-03-02 01:57:06 +00:00
Philip Okugbe 4f3577f009 feat: enhance comments (#1980)
* feat: non-inline comments support

* enhance comments

* fix types
2026-03-02 01:42:25 +00:00
Philipinho d5e4b8bb59 fix ui 2026-03-01 20:58:04 +00:00
Philipinho 1a897faaa2 exclude events 2026-03-01 19:13:56 +00:00
Philipinho 6f1a91cc05 sync 2026-03-01 18:38:43 +00:00
Philip Okugbe 60848ea903 feat(ee): mcp (#1976)
* feat: MCP
* sync
* sync
2026-03-01 18:37:39 +00:00
Philip Okugbe 2309d1434b feat: support cross-space page mentions (#1979) 2026-03-01 17:14:10 +00:00
Philipinho dcc2bacb22 sync 2026-03-01 01:31:10 +00:00
Philip Okugbe 69d7532c6c feat(ee): audit logs (#1977)
feat: clickhouse driver
* sync
* updates
2026-03-01 01:29:03 +00:00
Philip Okugbe 85ce0d32bf New Crowdin updates (#1960)
* New translations translation.json (Russian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (Dutch)

* New translations translation.json (Dutch)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)
2026-02-28 01:24:57 +00:00
Philip Okugbe fc0997fd90 feat: editor attachment paste handling (#1975)
* reupload attachments if uploaded to a different page
* use image dimensions on paste/DnD

* tooltips withinPortal:false

* isolating attribute
2026-02-28 01:24:19 +00:00
Philipinho df64de5306 fix focus 2026-02-27 01:38:43 +00:00
Philip Okugbe ea44468fad feat: editor inline status node (#1973)
* inline status node

* fix alignment

* fix

* typed storage

* fix math block popup on select all
2026-02-27 01:34:03 +00:00
Philip Okugbe 59e945562d feat(ee): page-level access/permissions (#1971)
* Add page_hierarchy table

* feat(ee): page-level permissions

* pagination

* rename migration
fixes

* fix

* tabs

* fix theme

* cleanup

* sync

* page permissions notification
* other fixes

* sharing disbled

* fix column nodes

* toggle error handling
2026-02-26 19:49:10 +00:00
Philipinho 22f33bab7c cleanups 2026-02-25 22:41:54 +00:00
Philipinho e0a8521566 enhance columns 2026-02-25 22:31:01 +00:00
Philip Okugbe b5803f42da xwiki html import cleanup (#1969) 2026-02-24 15:53:38 +00:00
Olivier Lambert 5de1c8e3ed fix: inline code input rule deletes character before opening backtick (#1923)
The upstream TipTap Code extension input rule regex /(^|[^`])`([^`]+)`(?!`)$/
uses a capture group (^|[^`]) that includes the character preceding the
opening backtick in the full match. When markInputRule processes this,
it deletes everything from the match start to the code content, which
removes that preceding character along with the backtick delimiters.

For example, typing foo(`bar` would result in foo`bar` (formatted)
instead of the expected foo(`bar` (formatted) — the ( is lost.

Fix: disable the built-in Code extension from StarterKit and register it
separately with a corrected regex that uses a lookbehind assertion
(?:^|(?<=[^`])) instead of a capture group. The lookbehind asserts the
preceding character without including it in the match, so markInputRule
only deletes the backtick delimiters.

Functionally tested on Firefox and Chrome.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 15:51:24 +00:00
Philip Okugbe ef87210b3d feat: editor UI refresh and enhancements (#1968)
* feat: new image menu
* switch to resizable side handles
* use pixels

* refactor excalidraw and drawio menu

* support image resize undo

* video resize

* callout menu refresh

* refresh table menus

* fix color scheme

* fix: patch @tiptap/core ResizableNodeView to prevent resize sticking after mouseup

* feat: columns

* notes callout

* focus on first column

* capture tab key in column

* fix print

* hide columns menu when some nodes are focused

* fix print

* fix columns

* selective placeholder

* fix blockquote

* quote

* fix callout in columns
2026-02-24 15:22:37 +00:00
Philipinho c172d3bd5e fix 2026-02-21 00:43:49 +00:00
Philip Okugbe 53132acb0a fix: redirect to original page after re-authentication (#1959)
* fix: redirect to original page after re-authentication

When a session expires, the current URL is now preserved as a query
parameter on the login page. After successful login (including MFA
flows), the user is redirected back to their original page instead of
always landing on /home.

* secure

---------

Co-authored-by: Julien Fontanet <julien.fontanet@isonoe.net>
2026-02-21 00:02:23 +00:00
b4sh2 d6472f0876 Merge commit from fork
Co-authored-by: b4sh2 <b4sh2@users.noreply.github.com>
2026-02-20 16:59:44 +00:00
Philipinho 873c963043 fix db types duplication 2026-02-19 22:34:07 +00:00
Julien Fontanet 03a70d768a fix: allow deleting last character in headings (#1954)
The copy-link decoration widget (contentEditable="false") injected
inside headings prevented browsers from deleting the last remaining
character via Backspace or Delete keys. Only show the widget when the
heading has more than one character of content.
2026-02-18 13:48:15 +00:00
Philip Okugbe 0aeaa43112 feat: replace sharp with client-side icon resize (#1951) 2026-02-16 19:48:19 +00:00
Philip Okugbe 92d5d0b237 New Crowdin updates (#1950)
* New translations
2026-02-16 04:22:40 +00:00
Philipinho 0ce74d34de env validation 2026-02-16 04:11:19 +00:00
Philipinho 00b5328676 fix page error boundary 2026-02-16 04:06:41 +00:00
Philipinho 2ebdc2baea empty states 2026-02-16 00:33:16 +00:00
Philip Okugbe 621ef4f0cf New Crowdin updates (#1948)
* New translations
2026-02-15 23:10:32 +00:00
Philipinho 26b9338da5 sync 2026-02-15 23:04:18 +00:00
Philipinho 618f56577d turn into callout option 2026-02-15 22:51:23 +00:00
Philipinho 0a05ce6133 enhance editor bubble menu 2026-02-15 22:39:42 +00:00
Philipinho cb9d6be3b9 sync 2026-02-15 17:07:27 +00:00
Arek Nawo b76f5adaad feat(ee): AI menu (#1912)
* feat(ee): AI menu

* - Add insert below and copy option

* prebuild @editor-ext

* sanitize output

* clear existing output

* switch to menu component

* refactor directory

* separator

* refactor directory

* support more languages

* pass markdown to model

* fix: close AI menu on page change

* enhance text input and preview styling

* fix: Use absolute positioning for the AI menu

* make preview scrollable

* activation controls

* enhance bubble menu

* sync

* set width

* fix line break

* switch terminologies

* cloud

* buffer

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2026-02-14 20:58:08 -08:00
Philipinho 41fa77b29d sync 2026-02-14 20:03:35 -08:00
Philip Okugbe 05b3c65b0f feat: notifications (#1947)
* feat: notifications
* feat: watchers

* improvements

* handle page move for watchers

* make watchers non-blocking

* more
2026-02-14 20:00:38 -08:00
Philipinho e0ab9d9b5e override package 2026-02-14 10:37:11 -08:00
Philipinho 55280db672 dark color theme tweaks 2026-02-14 10:35:03 -08:00
Philipinho 32bbc6911f override qs 2026-02-12 11:46:28 -08:00
Philipinho 5814542128 update lock file 2026-02-12 11:41:24 -08:00
Philip Okugbe 18b5781522 feat(API): page content update and retrieval (#1937)
* feat: page content update and retrieval output

* import module

* refactor naming
* support prepend

* rename contentOperation -> operation

* dry

* add yjs utils
2026-02-12 11:13:47 -08:00
Philipinho 49ab9875ba fix tiptap version conflicts 2026-02-11 22:47:25 -08:00
Philipinho 25f4b8c2b4 fix 2026-02-11 17:47:30 -08:00
Philipinho 4d43f86c51 update deps 2026-02-11 17:43:13 -08:00
Philip Okugbe f170ede8da fix(deps): override packages (#1936)
* override packages
2026-02-11 16:48:26 -08:00
Philipinho 7861b5b186 fix: add RedisModule to CollabAppModule 2026-02-09 18:50:31 -08:00
Philipinho 3a9bdfbb06 fix(deps): update vite and nx 2026-02-09 18:32:09 -08:00
Philipinho ab7999a946 v0.25.3 2026-02-09 18:27:55 -08:00
Philip Okugbe 0f02261ee6 feat: page version history improvements (#1925)
* Refactor: use queue for page history

* feat: save multiple version contributors

* display contributor avatars in history list

* fix interval
2026-02-09 18:25:35 -08:00
Philip Okugbe aff8dba2cb fix: diagrams SVG content length (#1928) 2026-02-09 18:20:09 -08:00
Olivier Lambert f6a8247c48 fix: cursor jumps to end of text when editing a comment (#1924)
* fix: cursor jumps to end of text when editing a comment

When editing a comment mid-text, the cursor would jump to the end after
every keystroke, making it impossible to insert text at any position
other than the end.

Root cause: on each keystroke, the comment editor's onUpdate callback
updated parent state (setContent), which changed the defaultContent prop
passed back to CommentEditor. A useEffect watching defaultContent then
called commentEditor.commands.setContent(), which reset the entire
editor content and moved the cursor to the end.

Fix:
- Store in-progress edits in a ref instead of state to avoid triggering
  React re-renders and the prop->effect->setContent cascade
- Read from the ref when saving the comment
- Sync the ref back into state after a successful save so the read-only
  view updates immediately
- Guard the setContent useEffect to only run for read-only editors, so
  websocket-driven updates from other browsers still work

Fixes #1791

Functionally tested on Firefox and Chrome: mid-text editing, saving,
cross-browser live updates via websocket.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix stale content on edit cancel

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2026-02-09 15:16:40 -08:00
Philip Okugbe 7879e1f600 fix: add execCommand fallback for clipboard (#1927)
* fix: add execCommand fallback for clipboard
2026-02-09 14:44:27 -08:00
Philip Okugbe 3cb70f0696 New translations translation.json (German) (#1915) 2026-02-06 11:37:33 -08:00
Philipinho fbb44df548 v0.25.2 2026-02-06 11:32:00 -08:00
Philip Okugbe bc3ce893c4 New Crowdin updates (#1914)
* New translations translation.json (Japanese)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (Japanese)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)
2026-02-06 11:31:12 -08:00
Philipinho ae96352189 sync 2026-02-06 10:37:51 -08:00
Philip Okugbe 1ad53c2581 feat(ee): public sharing controls (#1910)
* feat(ee): public sharing controls
* lint
2026-02-06 10:35:36 -08:00
Philip Okugbe 2f97a3debc feat: DOCX import (#1913) 2026-02-06 10:34:51 -08:00
Philipinho 40b5346f9e cleanup redundant param 2026-02-06 10:28:52 -08:00
Philipinho d6b4573b79 update compose services versions 2026-02-06 10:27:34 -08:00
Philip Okugbe 4878850b25 fix: attachment bugs in safari(#1908)
* use widely available arrayBuffer
* fix stream fails in safari
* fix hasFocus bug
* fix safari upload bug
* feat: add HTTP range request support for file serving
2026-02-05 07:47:03 -08:00
Philip Okugbe 5c3942c159 fix safari print (#1907) 2026-02-04 08:26:03 -08:00
Philipinho e0809e7104 v0.25.1 2026-02-04 07:10:13 -08:00
Philipinho da6793ac87 downgrade tiptap version (fix menu) 2026-02-04 07:09:48 -08:00
Philip Okugbe 08e94eb3c1 update dependencies (#1902) 2026-02-03 15:15:23 -08:00
Philipinho 5a14186f1c fix global diff css 2026-02-03 13:47:56 -08:00
Philipinho 6a0bb8d4cb v0.25.0 2026-02-03 13:18:03 -08:00
Philip Okugbe fba9f4cb2b New Crowdin updates (#1896)
* New translations translation.json (Japanese)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (Japanese)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (Japanese)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (Japanese)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)
2026-02-03 13:16:27 -08:00
Philipinho d8f7c4a822 cleanup 2026-02-03 13:12:39 -08:00
Philipinho 202685b39f fix translation 2026-02-03 13:09:56 -08:00
Philip Okugbe fc4a428208 fix(deps): update dependencies (#1898) 2026-02-03 13:04:00 -08:00
Philip Okugbe 5506eb194b feat: page history diff (#1891)
* Show actual history changes
* V2 - WIP
* feat: page history diff
* fix: exclude content from history listing

---------

Co-authored-by: Jason Norwood-Young <jason@10layer.com>
2026-02-03 11:55:20 -08:00
Philipinho f32bb298e0 v0.25.0-beta.1 2026-01-30 23:09:01 +00:00
Pleasure1234 3178cad796 fix: handle empty replace term in search and replace functionality (#1562)
- Fix 'Empty text nodes are not allowed' error when replace field is empty
- Update both replace() and replaceAll() functions to check for empty replaceTerm
2026-01-30 22:37:22 +00:00
Philipinho 9d7f8c62c5 sync 2026-01-30 22:31:49 +00:00
Philip Okugbe 78b1c1a453 feat: switch to cursor pagination (#1884)
* add cursor pagination function

* support custom order modifier
* refactor returned object

* feat(db): migrate paginated endpoints to cursor-based pagination

* sync

* support hasPrevPage boolean

* feat(client): migrate pagination from offset to cursor-based

* support beforeCursor/prevCursor

* wrap search results in items array for API consistency
2026-01-30 19:28:54 +00:00
Philip Okugbe 96ed98619f feat: add IPv6 support via configurable HOST binding (#1885) 2026-01-30 00:33:10 +00:00
Philip Okugbe 60501de992 fix: missing logs on OnApplicationBootstrap hook (#1882)
* - fix: set default Nest logger and bufferLogs to false for pino compatibility
- handle redis error event

* fix collab server logging too
2026-01-29 09:25:23 +00:00
Philip Okugbe 74e915546b feat: collab redis extension with server affinity (#1873)
* feat(collab): better redis extension
* move types to own file
* debug logging
* fix: graceful collab shutdown
* rename default prefix
* pass wsAdapter to gateway
* expose event handler
* unique collab serverId generation
* uninstall @hocuspocus/extension-redis package
* expose more functions
* sync with latest
* cleanup
* fastify router options
* cleanup type
2026-01-27 17:05:05 +00:00
Philipinho 3523600f40 add timestamps 2026-01-27 16:49:22 +00:00
Philip Okugbe 6ccb2bb872 feat(export): add metadata file to preserve page icons and ordering on import (#1877)
* feat(export): add metadata file to preserve page icons and ordering on import
- Export includes `docmost-metadata.json`
- Import reads metadata to restore icons and sort siblings by original position

* cleanup

* bonus fixes

* handle unknown prosemirror nodes

* add docmost app  version
2026-01-27 16:39:39 +00:00
Philipinho 0245a183e1 sync 2026-01-26 02:08:54 +00:00
Philip Okugbe de5f71894a New Crowdin updates (#1869)
* New translations translation.json (Japanese)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)
2026-01-25 12:39:19 +00:00
Philip Okugbe 351b075ebb fix(tree): update sidebar-pages cache directly instead of refetching on page move (#1870) 2026-01-25 12:38:44 +00:00
Philipinho 1ca7d42203 fix switch space toggle 2026-01-25 02:49:25 +00:00
Philipinho 1e441560f6 fix production logs filter 2026-01-25 02:15:10 +00:00
Philip Okugbe 54775f537d fix: handle malformed URLs gracefully during import/export (#1868)
* Handling malformed URLs gracefully

* Allow import of invalid URLs, but adding logging.

---------

Co-authored-by: gpapp <gergely.papp@itworks.hu>
2026-01-25 00:48:43 +00:00
Philipinho 5dbf0027bd Add isomorphic basename utility 2026-01-25 00:08:02 +00:00
Philip Okugbe 5588ec34fb New Crowdin updates (#1866)
* New translations translation.json (Japanese)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (Japanese)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)
2026-01-25 00:04:50 +00:00
Philipinho 55b8128829 Fix Google sheets regex 2026-01-24 23:35:04 +00:00
Philip Okugbe aa6a046aa6 feat(export): add export loading state and copy as markdown (#1867)
* feat: add loading state to export

* feat: copy as markdown

* preserve taskList comment
2026-01-24 23:30:17 +00:00
Philip Okugbe 657fdf8cb7 feat: Tiptap V3 migration (#1854)
* Tiptap3 migration - WIP

* fix collaboration

* remove unused code

* fix flicker

* disable duplicate extensions

* update tiptap version

* Switch to useEditorState
- Set shouldRerenderOnTransaction to false

* fix editable state

* add tippyoptions for reference

* merge main

* tiptap 3.6.1

* fix bubble menu

* fix converter

* fix menus

* fix collaboration caret css

* fix: Set `isInitialized` to force immediate react node view rendering

* feat: Migrate tippy.js menus to Floating UI

* feat: Update collaboration connection for HocusPocus v3

* fix: Connect/disconnect websocketProvider

* cleanup

* cleanup

* feat: Improved placeholder and upload handling for images

* feat: Improved placeholder and upload handling for videos

* refactor: Image node and view clean-up

* feat: Improved placeholder and upload handling for attachments

* fix: Video view styles

* fix: Transaction handling on asset upload

* fix: Use imageDimensionsFromStream

* feat: Multiple file upload, improved placeholders, local previews

* fix: Drag & drop, paste upload

* fix: Allow media as attachment

* * add skeleton pulse animation
* add translation strings
* fix attachment view responsiveness

* fix collab connection status display

* Tiptap v3.17.0

* fix suggestion menu exit bug

* fix search shortcut

* fix history editor css

* tiptap 3.17.1

---------

Co-authored-by: Arek Nawo <areknawo@areknawo.com>
2026-01-24 20:41:08 +00:00
Philip Okugbe 98f71c95fe feat: stream file serving (#1865) 2026-01-24 17:54:56 +00:00
Philip Okugbe efb0a9317b feat: allow upload of large files (#1862)
* Allow upload of large files

* feat: createByteCountingStream utility function.

---------

Co-authored-by: gpapp <gergely.papp@itworks.hu>
2026-01-22 20:00:58 +00:00
Philipinho 063ea99b66 sync 2026-01-21 18:17:48 +00:00
Philip Okugbe aa143ad79c refactor(db): migrate from node-postgres to postgres.js (#1846)
* refactor(db): migrate from node-postgres to postgres.js
* ignore schema param
2026-01-21 18:12:16 +00:00
Philip Okugbe 918f4508d2 feat: switch to pino for logs (#1855)
- switch to json logs in production
- add option to support http logging
2026-01-21 01:23:50 +00:00
Philipinho 5cd0ba6902 fix script 2026-01-20 22:36:19 +00:00
Philipinho a1260188ae fix: UI improvements 2026-01-19 21:05:34 +00:00
Philipinho bdf02f593d Merge branch 'feat/auto-tooltip' 2026-01-19 19:43:58 +00:00
Philipinho e24bf5ed57 feat: auto-tooltip component 2026-01-19 19:40:06 +00:00
Philip Okugbe f3f74c591f fix(share): escape page title in SEO meta tags (#1850) 2026-01-19 19:31:28 +00:00
Philipinho 5f966a2d89 chore: add clean up command 2026-01-18 16:50:51 +00:00
Philipinho bcb004af21 update lockfile 2026-01-16 13:22:41 +00:00
Philipinho ac675e7d74 update dockerfile 2026-01-16 13:21:42 +00:00
Philipinho bf89eff5e7 sync 2026-01-16 13:20:31 +00:00
Philip Okugbe 183787fa0c fix: update dependencies (#1843) 2026-01-14 16:36:47 +00:00
Philipinho 15aa04a5f7 sync 2026-01-14 11:49:39 +00:00
Philipinho 79343a5d52 fix: prevent text overflow in group and space list tables 2026-01-13 16:25:42 +00:00
Philipinho 61e252918e fix length 2026-01-13 16:13:52 +00:00
Philipinho e98fa7f69a sync
* fix form length
2026-01-13 16:13:04 +00:00
Philip Okugbe 6d148a35eb New Crowdin updates (#1830)
* New translations translation.json (Japanese)

* New translations translation.json (Japanese)
2026-01-13 16:01:08 +00:00
Philip Okugbe 0bbc1c35de fix: public sharing performance improvements (#1841) 2026-01-13 16:00:22 +00:00
Philip Okugbe 47097969a0 fix: use subquery (#1833)
- enhance file tasks list endpoint
2026-01-13 15:58:26 +00:00
Philip Okugbe 13f529e064 fix anchor scroll in same page (#1834) 2026-01-13 15:35:53 +00:00
Philip Okugbe 8fc8422fbc fix: increase max length for groups and spaces (#1840) 2026-01-13 15:31:03 +00:00
Philipinho 732951a322 v0.24.1 2025-12-14 13:24:09 +00:00
Philipinho 2544775266 fix: switch to node slim image 2025-12-14 13:16:40 +00:00
Philipinho d59539f197 fix ai streaming 2025-12-13 14:15:41 +00:00
Philipinho b061df7f7d Use new fastify router options 2025-12-13 14:15:06 +00:00
Philipinho 0fe1459864 fix: override jsonwebtoken version 2025-12-12 17:25:27 +00:00
Philipinho 6af7956889 v0.24.0 2025-12-12 17:15:59 +00:00
Philip Okugbe 3dbb957bd7 New Crowdin updates (#1541)
* New translations translation.json (Dutch)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (Russian)

* New translations translation.json (German)

* New translations translation.json (German)

* New translations translation.json (German)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Russian)

* New translations translation.json (Spanish)

* New translations translation.json (Korean)

* New translations translation.json (Korean)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)
2025-12-12 17:15:19 +00:00
Philipinho f39a4cf2d5 fix space modal spacing 2025-12-12 14:08:30 +00:00
Philipinho 724e01bd55 fix default page share state (API) 2025-12-11 20:43:26 +00:00
Philip Okugbe 6e350f6746 fix nodeview dragging (#1775) 2025-12-11 19:32:18 +00:00
Philip Okugbe cb9f27da9a fix mermaid security (#1774) 2025-12-11 16:44:52 +00:00
Philip Okugbe d2629afff2 feat: anchor links (#1765)
* feat: add heading extension with unique ID support and scroll functionality
* Added unique id for heading
* remove baseUrl heading storage
* move heading to extensions package
* WIP
* support anchors in mentions
* enhance scrolling functionality
* nodeId function
* fix nanoid import
* Bring unique-id extension local
* fixes
* fix internal link scroll in public pages
* add unique id server side
* rename mention anchor to anchorId
* capture first anchorId on paste

---------

Co-authored-by: Romik <40670677+RomikMakavana@users.noreply.github.com>
2025-12-06 14:46:54 +00:00
Philip Okugbe 9139d393ef fix: update tiptap packages (#1755)
* update tiptap version

* create empty paragraph on enter

* feat: split title text into page content on Enter

* update hocuspocus
2025-12-02 13:15:19 +00:00
Philipinho ab96672ecd fix 2025-12-02 13:14:03 +00:00
Philipinho 2ea3c2da58 sync 2025-12-01 14:05:59 +00:00
Philip Okugbe 9fb16bc842 feat(EE): AI vector search (#1691)
* WIP

* AI module - init

* WIP

* sync

* WIP

* refactor naming

* new columns

* sync

* sync

* fix search bug

* stream response

* WIP

* feat embeddings sync

* refine

* Add workspaceId to page events

* refine

* WIP

* add translation string

* sync

* reset ai answer on query change

* hide AI search in cloud

* capture streaming error

* sync
2025-12-01 11:50:25 +00:00
Philip Okugbe c3b350d943 fix: zip extraction validation (#1753)
* fix: zip extraction validation

* fix
2025-12-01 11:37:59 +00:00
Philip Okugbe 8014ba3ab7 feat: Text background highlight (#1754)
* #1196/feat: add text background highlight

* unify text color

* dark mode support
* unify text color and highlight

* dark mode support for color selector trigger

* fix see through in color selector dark mode

* fix selection highlight in dark mode

* brown color

* clean up

---------

Co-authored-by: sanua356 <sanek.pankratov356@gmail.com>
2025-12-01 11:34:35 +00:00
Philipinho ec3a04f7c7 fix 2025-11-29 12:37:35 +00:00
Philip Okugbe 04a17c9b92 package security updates (#1744)
* package security updates

* package updates
2025-11-29 11:50:20 +00:00
Philip Okugbe 520c07a0bc fix: generic page import hierarchy (#1747)
* fix page hierarchy

* fix
2025-11-29 11:50:02 +00:00
Philipinho 60a8ed6826 sync 2025-10-25 02:08:29 +01:00
Philip Okugbe f5684b792e fix duplicated page parenting (#1692) 2025-10-23 15:00:11 +01:00
Philipinho 042836cb6d sync 2025-10-07 21:09:55 +01:00
Philipinho 4f1f0ba513 fix 2025-10-07 21:06:59 +01:00
Philip Okugbe 3164b6981c feat: api keys management (EE) (#1665)
* feat: api keys (EE)

* improvements

* fix table

* fix route

* remove token suffix

* api settings

* Fix

* fix

* fix

* fix
2025-10-07 21:05:13 +01:00
Philipinho 16c1e864af fix comment space 2025-10-07 18:44:37 +01:00
Philipinho c9b1cad982 sync 2025-10-07 18:39:30 +01:00
Philip Okugbe bf8cf6254f feat: Typesense search driver (EE) (#1664)
* feat: typesense driver (EE) - WIP

* feat: typesense driver (EE) - WIP

* feat: typesense

* sync

* fix
2025-10-07 17:34:32 +01:00
Philip Okugbe 3135030376 fix editor converter (#1647) 2025-09-30 16:07:19 +01:00
Philip Okugbe 3fae41a5ca fix: editor performance improvements (#1648)
* Switch to useEditorState
* change shouldRerenderOnTransaction to false
2025-09-30 14:04:01 +01:00
Philipinho b50e25600a sync 2025-09-28 16:44:33 +01:00
Philipinho 1f3b0c7276 cloud fix 2025-09-24 21:25:39 +01:00
Philipinho 3c4cab0d2a v0.23.2 2025-09-18 18:00:28 +01:00
Philipinho 4de25a8b94 invalidate queries on space deletion 2025-09-18 15:52:53 +01:00
Philipinho cf5bbb10df fix import html processing 2025-09-18 15:34:13 +01:00
Philipinho ac17521717 sync 2025-09-18 13:24:16 +01:00
Philip Okugbe 9ac180f719 fix: enhance page import (#1570)
* change import process

* fix processor

* fix page name in notion import

* preserve confluence table bg color

* sync
2025-09-17 23:50:27 +01:00
Philipinho 46669fea56 (cloud) disable page sharing in trial mode 2025-09-17 23:36:13 +01:00
Pleasure1234 fe6ecdf1f1 fix: update combobox props in SpaceSelect component (#1564)
Added 'keepMounted: false' and 'dropdownPadding: 0' to comboboxProps for improved dropdown behavior and appearance in the SpaceSelect sidebar component.
2025-09-17 13:36:12 +01:00
Philipinho 04ae1d7270 Allow lastColumnResizable in table 2025-09-15 22:34:29 +01:00
Philip Okugbe 1280f96f37 feat: implement space and workspace icons (#1558)
* feat: implement space and workspace icons
- Create reusable AvatarUploader component supporting avatars, space icons, and workspace icons
- Add Sharp package for server-side image resizing and optimization
- Create reusable AvatarUploader component supporting avatars, space icons, and workspace icons
- Support removing icons

* add workspace logo support
- add upload loader
- add white background to transparent image
- other fixes and enhancements

* dark mode

* fixes

* cleanup
2025-09-15 21:11:37 +01:00
Philipinho 61d1cf88a7 fix: reset file inputs after import 2025-09-15 12:52:31 +01:00
Philipinho f413720e15 - sync
- reinstantiate S3 client to fix file upload errors during import
- delete import zip file after use
2025-09-14 03:00:23 +01:00
Philipinho 8e16ad952a v0.23.1 2025-09-13 03:15:53 +01:00
Philip Okugbe 7ada3cb1f9 fix: page import task (#1551)
* fix import

* - fix notion importer
- support notion page icon import
- fix horizontal rule css
- rename service file

* sync

* 3 mins delay
2025-09-13 03:14:59 +01:00
Philipinho 47c54174b3 sync 2025-09-11 00:50:15 +01:00
Philipinho dc0650289d sync 2025-09-04 15:07:01 -07:00
Philipinho 091e790b83 fix attachment search in cloud 2025-09-04 14:22:40 -07:00
Philipinho ae24ea29ba v0.23.0 2025-09-04 13:42:59 -07:00
Philipinho 9df6061e1a lock file 2025-09-04 13:42:33 -07:00
Philipinho 31053e2b20 update mermaid 2025-09-04 13:41:55 -07:00
Philipinho eb8e8507ea use debug 2025-09-04 13:27:15 -07:00
Philipinho c99bfb8ef1 make print better 2025-09-04 13:22:43 -07:00
Philipinho 26ea04e2a3 sync 2025-09-04 12:25:53 -07:00
Philipinho 6cc58c57f5 sync 2025-09-04 12:16:30 -07:00
Philipinho 7d2ff346fa UI fixes 2025-09-04 11:35:04 -07:00
Philipinho b08d37fbf0 fix 2025-09-04 10:57:17 -07:00
Philipinho d43ee77617 remove debug log 2025-09-04 09:40:17 -07:00
Philipinho 5d91eb4f5f feat: queue imported attachments for indexing 2025-09-04 09:38:30 -07:00
Quinten Van Damme 3e9f6b11cc Remove version from docker-compose.yml [deprecated] (#1011) 2025-09-04 03:55:32 +01:00
Hoie Kim db55de9406 feat: progressive web app (#614)
* feat: progressive web app

* replace icons

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-09-04 01:33:52 +01:00
Philip Okugbe 1919eba340 New Crowdin updates (#1522)
* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)
2025-09-03 13:17:08 -07:00
Philip Okugbe 7951b2e0c6 New Crowdin updates (#1509)
* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)
2025-09-03 18:28:30 +01:00
Philipinho 73b78f625d more translations 2025-09-03 10:11:19 -07:00
Philipinho cf7534de3d fix version display 2025-09-03 09:37:29 -07:00
Philipinho adec36d544 fix: adjust margins
- use default browser highlight background
2025-09-02 21:45:38 -07:00
Philipinho f9e10805f0 sync 2025-09-02 21:38:14 -07:00
Eshwar Tangirala 00e499b3e5 Fixing extra page bug on print (#1478) 2025-09-03 05:25:48 +01:00
Sarthak Mittal 5ee6e46535 checkbox aligned to text (#1486) 2025-09-03 05:23:28 +01:00
Philip Okugbe 1f797c3d27 fix: confluence drawio import (#1518)
* POC

* WIP - working

* WIP

* WIP

* sync

* fix drawio preview image
2025-09-03 05:19:09 +01:00
Philip Okugbe f12866cf42 feat(EE): full-text search in attachments (#1502)
* feat(EE): fulltext search in attachments

* feat: global search
- search filters
- attachments search ui
- and more

* fix import

* fix import

* rename migration

* add GIN index

* fix table name

* sanitize
2025-09-02 05:27:01 +01:00
Philip Okugbe dcbb65d799 feat(EE): LDAP integration (#1515)
* LDAP - WIP

* WIP

* add hasGeneratedPassword

* fix jotai atom

* - don't require password confirmation for MFA is user has auto generated password (LDAP)
- cleanups

* fix

* reorder

* update migration

* update default

* fix type error
2025-09-02 04:59:01 +01:00
Finn Dittmar 5968764508 feat: emoji callout icon (#1323) 2025-08-31 21:16:52 +01:00
Alexander Schaber 242fb6bb57 fix: set mermaid theme based on computed color scheme (#1438) 2025-08-31 20:48:59 +01:00
Philip Okugbe 74cd890bdd feat(EE): implement SSO group sync for SAML and OIDC (#1452)
* feat: implement SSO group synchronization for SAML and OIDC

- Add group_sync column to auth_providers table
- Extract groups from SAML attributes (memberOf, groups, roles)
- Extract groups from OIDC claims (groups, roles)
- Implement case-insensitive group matching with auto-creation
- Sync user groups on each SSO login
- Ensure only one provider can have group sync enabled at a time
- Add group sync toggle to SAML and OIDC configuration forms

* rename column
2025-08-31 20:33:37 +01:00
Philipinho 509622af54 ignore type error 2025-08-31 12:20:40 -07:00
Philipinho 937386e42b fix: hide table handles in readonly mode 2025-08-31 12:08:02 -07:00
Philipinho 60a373f488 fix: readonly editor table responsiveness 2025-08-31 12:04:27 -07:00
Philip Okugbe 73ee6ee8c3 feat: subpages (child pages) list node (#1462)
* feat: subpages list node

* disable user-select

* support subpages node list in public pages
2025-08-31 18:54:52 +01:00
Mirone 7d1e5bce0d feat: table row/column drag and drop (#1467)
* chore: add dev container

* feat: add drag handle when hovering cell

* feat: add column drag and drop

* feat: add support for row drag and drop

* refactor: extract preview controllers

* fix: hover issue

* refactor: add handle controller

* chore: f

* chore: remove log

* chore: remove dev files

* feat: hide other drop indicators when table dnd working

* feat: add auto scroll and bug fix

* chore: f

* fix: firefox
2025-08-31 18:53:27 +01:00
Philip Okugbe aa58e272d6 fix: exclude deleted pages (#1494) 2025-08-31 09:11:33 +01:00
Philipinho 08135a2fba sync 2025-08-12 11:09:26 -07:00
Philipinho d92a94244f sync 2025-08-12 10:21:17 -07:00
Philipinho 5012a68d85 sync 2025-08-06 10:19:35 -07:00
Philip Okugbe 5a3377790e feat: debug mode env variable (#1450) 2025-08-06 18:16:30 +01:00
Philip Okugbe 3b85f4b616 fix: enforce C collation for page position ordering to ensure consistent behavior in Postgres 17+ (#1446)
- Add explicit C collation to position ordering queries to fix incorrect page placement in PostgreSQL 17+
- Ensures consistent ASCII-based ordering regardless of database locale settings
- Fixes issue where new pages were incorrectly placed at random positions instead of bottom
2025-08-04 09:49:29 +01:00
Philipinho cb2a0398c7 fix: invalidate trashed page from tree state 2025-08-04 00:42:13 -07:00
Philip Okugbe 95b7be61df fix: hide trash from can view permission (#1445) 2025-08-04 08:35:28 +01:00
Philip Okugbe b0c557272d fix nested taskList in markdown export (#1443) 2025-08-04 08:01:18 +01:00
Philip Okugbe dddfd48934 feat: add attachments support for single page exports (#1440)
* feat: add attachments support for single page exports
- Add includeAttachments option to page export modal and API
- Fix internal page url in single page exports in cloud

* remove redundant line

* preserve export state
2025-08-04 08:01:11 +01:00
Philipinho aa6eec754e fix: exclude trashed pages from position generation 2025-08-04 00:00:06 -07:00
Philip Okugbe 97a7701f5d fix local storage copy function (#1442) 2025-08-04 03:20:18 +01:00
Philipinho b97eb85d05 sync 2025-08-03 03:59:08 -07:00
Philipinho 1615e0f4ad v0.22.2 2025-08-01 16:15:02 -07:00
Philip Okugbe 1cb2535de3 fix trash in search (#1439)
- delete share if page is trashed
2025-08-02 00:14:00 +01:00
Philipinho 83bc273cb0 cleanup 2025-08-01 07:05:25 -07:00
Philipinho c7beaa3742 v0.22.1 2025-08-01 06:54:28 -07:00
Philipinho 4a228e5a51 fix comment replies 2025-08-01 06:51:56 -07:00
Philipinho edff375476 sync 2025-08-01 02:54:11 -07:00
Philipinho 95016b2bfc sync 2025-08-01 02:51:55 -07:00
Philipinho ca83712364 cleanup 2025-08-01 02:26:14 -07:00
Philip Okugbe 39550fe906 fix: duplicate page position bug (#1431) 2025-07-30 18:07:06 +01:00
Philipinho e74ecb2604 v0.22.0 2025-07-29 15:22:46 -07:00
Philipinho 992fb23160 update lock file 2025-07-29 15:04:38 -07:00
Philipinho d58a3bba9b update linkify 2025-07-29 14:59:50 -07:00
Philipinho 6ef47fc432 show button only if necessary 2025-07-29 14:59:23 -07:00
Philipinho 9e6765d83c fix 2025-07-29 14:51:55 -07:00
Philipinho ec0ed5c630 fix import 2025-07-29 14:50:59 -07:00
Philipinho 77b334ea37 reorder migration 2025-07-29 14:49:19 -07:00
Philip Okugbe 5da92a538a feat: add unaccent support for accent-insensitive search (#1402)
- Add PostgreSQL unaccent and pg_trgm extensions
- Create immutable f_unaccent wrapper function for performance
- Update all search queries to use f_unaccent for accent-insensitive matching
- Add 1MB limit to tsvector content to prevent errors on large documents
- Update full-text search trigger to use f_unaccent
- Fix MultiSelect client-side filtering to show server results properly
2025-07-29 22:47:13 +01:00
Philipinho f90c5a636b cleanup comment 2025-07-29 14:30:45 -07:00
Philipinho 6db93ef0c7 upsell 2025-07-29 14:28:40 -07:00
Philip Okugbe a3d058042f New Crowdin updates (#1342)
* New translations translation.json (German)

* New translations translation.json (Spanish)

* New translations translation.json (Russian)

* New translations translation.json (Spanish)

* New translations translation.json (Russian)

* New translations translation.json (French)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (Spanish)

* New translations translation.json (Russian)

* New translations translation.json (French)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Ukrainian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)
2025-07-29 21:53:16 +01:00
Philipinho 4ab9261cf5 sync 2025-07-29 13:41:07 -07:00
Philip Okugbe ca9558b246 feat(EE): resolve comments (#1420)
* feat: resolve comment (EE)

* Add resolve to comment mark in editor (EE)

* comment ui permissions

* sticky comment state tabs (EE)

* cleanup

* feat: add space_id to comments and allow space admins to delete any comment

- Add space_id column to comments table with data migration from pages
- Add last_edited_by_id, resolved_by_id, and updated_at columns to comments
- Update comment deletion permissions to allow space admins to delete any comment
- Backfill space_id on old comments

* fix foreign keys
2025-07-29 21:36:48 +01:00
Eddy Oyieko ec12e80423 feat: trash for deleted pages in space (#325)
* initial commit

* added recycle bin modal, updated api routes

* updated page service & controller, recycle bin modal

* updated page-query.ts, use-tree-mutation.ts, recycled-pages.ts

* removed quotes from openRestorePageModal prompt

* Updated page.repo.ts

* move button to space menu

* fix react issues

* opted to reload to enact changes in the client

* lint

* hide deleted pages in recents, handle restore child page

* fix null check

* WIP

* WIP

* feat: implement dedicated trash page
- Replace modal-based trash view with dedicated route `/s/:spaceSlug/trash`
- Add pagination support for deleted pages
- Other improvements

* fix translation

* trash cleanup cron

* cleanup

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-07-29 21:20:49 +01:00
Philip Okugbe 28fcb11cb4 update passport-saml (#1418) 2025-07-29 19:30:53 +01:00
Philip Okugbe 6b627d289c fix xss in generic iframe embed (#1419) 2025-07-29 19:28:48 +01:00
Philip Okugbe 78bce0e29d fix: validate public avatar path (#1416) 2025-07-28 18:17:06 +01:00
Philip Okugbe 0bd7ecb9b0 feat: enhance table cells with rich content support (#1409)
- Support multiple content types in table cells and headers: paragraphs, headings, lists (bullet/ordered/task), blockquotes, callouts, images, videos, attachments, math blocks, toggles, and code blocks
- Add custom table extension with smart Tab key handling for list indentation within tables
- Preserve default table navigation when not in lists
2025-07-28 08:22:22 +01:00
Philip Okugbe 1f815880a4 Revert "feat: set mermaid theme based on computed color scheme (#1397)" (#1412)
This reverts commit 32c7ecd9cf.
2025-07-26 01:34:15 +01:00
Philipinho 37b9056070 sync 2025-07-24 16:38:32 -07:00
Philip Okugbe ad5cf1e18b feat: add resizable embed component │ (#1401)
- Created reusable ResizableWrapper component
- Added drag-to-resize functionality for embeds
2025-07-25 00:23:14 +01:00
Alexander Schaber 32c7ecd9cf feat: set mermaid theme based on computed color scheme (#1397)
Use Mantine's `useComputedColorScheme` hook to dynamically configure mermaid's theme.
- When the computed color scheme is "light", the theme is set to "default".
- Otherwise, it is set to "dark".
2025-07-25 00:22:27 +01:00
Philip Okugbe b30bf61dc4 feat: home space list (#1400) 2025-07-25 00:21:40 +01:00
Philip Okugbe 662460252f feat(EE): MFA implementation (#1381)
* feat(EE): MFA implementation for enterprise edition
- Add TOTP-based two-factor authentication
- Add backup codes support
- Add MFA enforcement at workspace level
- Add MFA setup and challenge UI pages
- Support MFA for login and password reset flows
- Add MFA validation for secure pages
* fix types
* remove unused object
* sync
* remove unused type
* sync
* refactor: rename MFA enabled field to is_enabled
* sync
2025-07-25 00:18:53 +01:00
Philip Okugbe 8522844673 feat: duplicate page in same space (#1394)
* fix internal links in copies pages

* feat: duplicate page in same space

* fix children
2025-07-21 21:39:57 +01:00
Philip Okugbe f8dc9845a7 fix page tree api atom (#1391)
- The tree api atom state is not always set, which makes it impossble to create new pages since the buttons rely on it.
- this should fix it.
2025-07-21 05:02:40 +01:00
Philip Okugbe 4dfed2b2af queue import attachments upload (#1353) 2025-07-19 18:00:06 +01:00
Philip Okugbe 44e592763d feat: quick theme toggle and Mantine 8 upgrade (#1369)
* upgrade to mantine v8

* feat: quick theme toggle
2025-07-15 06:28:27 +01:00
Philip Okugbe 90488a95b1 feat: table background color, cell header and align (#1352)
* feat: add toggle header cell button to table cell menu

Added ability to toggle header cells directly from the table cell menu. This enhancement includes:
- New toggle header cell button with IconTableRow icon
- Consistent UI/UX with existing table menu patterns
- Proper internationalization support

* fix: typo in aria-label for toggle header cell button

* feat: add table cell background color picker

- Extended TableCell and TableHeader to support backgroundColor attribute
- Created TableBackgroundColor component with 21 color options
- Integrated color picker into table cell menu using Mantine UI
- Added support for both regular cells and header cells
- Updated imports to use custom TableHeader from @docmost/editor-ext

* feat: add text alignment to table cell menu

- Created TableTextAlignment component with left, center, and right alignment options
- Integrated alignment selector into table cell menu
- Shows current alignment icon in the button
- Displays checkmark next to active alignment in dropdown

* background colors

* table background color in dark mode

* add bg color name

* rename color attribute

* increase minimum table width
2025-07-15 06:27:48 +01:00
Philip Okugbe 9f39987404 fix: nested ordered-list style (#1351)
* feat: dynamic ordered-list style
* fix nested task list import
2025-07-15 02:43:59 +01:00
Philipinho 16ec218ba7 fix: deactivated user check 2025-07-14 10:28:42 -07:00
Philipinho 608783b5cf (cloud) billing copy 2025-07-14 03:56:26 -07:00
Philipinho 5f5f1484db throw early 2025-07-14 03:53:07 -07:00
Philip Okugbe f4082171ec feat: display user email below name in multi-member-select dropdown (#1355)
- Added email field to user items mapping
- Updated renderMultiSelectOption to show email in smaller, dimmed text
- Email only displays for user type options, not groups
2025-07-14 10:37:13 +01:00
fuscodev 6792a191b1 feat: Ctrl/Cmd+S: prevent 'Save As' dialog (#1272)
* init

* remove: force save

* switch from event.key to event.code by sanua356
2025-07-14 10:36:24 +01:00
Philip Okugbe e51a93221c more checks for collab auth token (#1345) 2025-07-14 10:35:03 +01:00
Philip Okugbe e856c8eb69 (cloud) fix: updates to billing (#1367)
* billing updates (cloud)

* old billing grace period
2025-07-14 10:34:18 +01:00
Philip Okugbe c2c165528b fix: seamlessly update editor collab token on expiration (#1366) 2025-07-14 07:19:06 +01:00
Philipinho 9fa2b9636c make sure editor is ready for editor search 2025-07-13 15:38:29 -07:00
fuscodev 29388636bf feat: find and replace in editor (#689)
* feat: page find and replace

* * Refactor search and replace directory

* bugfix scroll

* Fix search and replace functionality for macOS and improve UX

- Fixed cmd+f shortcut to work on macOS (using 'Mod' key instead of 'Control')
- Added search functionality to title editor
- Fixed "Not found" message showing when search term is empty
- Fixed tooltip error when clicking replace button
- Changed replace button from icon to text for consistency
- Reduced width of search input fields for better UI
- Fixed result index after replace operation to prevent out-of-bounds error
- Added missing translation strings for search and replace dialog
- Updated tooltip to show platform-specific shortcuts (⌘F on Mac, Ctrl-F on others)

* Hide replace functionality for users with view-only permissions

- Added editable prop to SearchAndReplaceDialog component
- Pass editable state from PageEditor to SearchAndReplaceDialog
- Conditionally render replace button based on edit permissions
- Hide replace input section for view-only users
- Disable Alt+R shortcut when user lacks edit permissions

* Fix search dialog not closing properly when navigating away

- Clear all state (search text, replace text) when closing dialog
- Reset replace button visibility state on close
- Clear editor search term to remove highlights
- Ensure dialog closes properly when route changes

* fix: preserve text marks (comments, etc.) when replacing text in search and replace

- Collect all marks that span the text being replaced using nodesBetween
- Apply collected marks to the replacement text to maintain formatting
- Fixes issue where comment marks were being removed during text replacement

* ignore type error

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-07-10 04:40:07 +01:00
Philipinho f80004817c sync 2025-07-08 16:05:34 -07:00
Finn Dittmar ac79a185de fix ctrl-a for codeblocks (#1336) 2025-07-08 22:13:21 +01:00
Philipinho 27a9c0ebe4 sync 2025-07-07 14:55:09 -07:00
Philipinho 81ffa6f459 sync 2025-07-03 04:12:24 -07:00
Whai 5364702b69 fix: comments block on edge and older browser (#1310)
* fix: overflow on edge and older browser
2025-07-01 05:14:08 +01:00
Philipinho 232cea8cc9 sync 2025-06-27 03:20:01 -07:00
Philipinho b9643d3584 sync 2025-06-27 03:07:51 -07:00
Philip Okugbe 9f144d35fb posthog integration (cloud) (#1304) 2025-06-27 10:58:36 +01:00
Philip Okugbe e44c170873 fix editor flickers on collab reconnection (#1295)
* fix editor flickers on reconnection

* cleanup

* adjust copy
2025-06-27 10:58:18 +01:00
Philipinho 1be39d4353 sync 2025-06-27 02:22:11 -07:00
Philipinho 36d028ef4d sync 2025-06-24 05:53:59 -07:00
Philip Okugbe f5a36c60e8 feat: tiered billing (cloud) (#1294)
* feat: tiered billing (cloud)

* custom tier
2025-06-24 13:22:38 +01:00
Finn Dittmar d5b84ae0b8 Only allow changing the email if the correct password is provided (#1288)
* fix

* fix overwriting password

* finalize

* BadRequestException

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-06-24 09:02:55 +01:00
Philip Okugbe e775e4dd8c fix(editor): prevent text color removal from other list items when setting color in lists (#1289)
Only unset color when 'Default' is selected. This ensures setting color on one list item does not remove it from others.
2025-06-23 19:31:30 +01:00
Philipinho 65b01038d7 v0.21.0 2025-06-18 14:28:14 -07:00
Philipinho e07cb57b01 sync 2025-06-18 14:25:40 -07:00
Philipinho 2b53e0a455 fix: add import size limit to static window config 2025-06-18 13:58:41 -07:00
Auxa b9b3406b28 Fix: Prevent premature focus change in TitleEditor when pressing Enter during IME composition (#730)
* fix: Prevents key events during text composition

Stops handling title key events when composing text,
ensuring proper input behavior during IME use.

* Refines IME composition event checks

Separates IME composition control from shift key logic and adds a Safari-specific keyCode check to prevent premature focus shifts during IME input.
2025-06-18 21:33:35 +01:00
Philip Okugbe 728cac0a34 fix word counter (#1269) 2025-06-18 21:32:11 +01:00
Philipinho d35e16010b handle empty invitation 2025-06-18 13:10:32 -07:00
Philipinho 15791d4e59 sync 2025-06-18 12:50:43 -07:00
Philip Okugbe 3318e13225 fix: use JWT expiry time for cookie duration (#1268)
* Set default jwt expiry to 90 days.
2025-06-18 20:50:11 +01:00
Philipinho 080900610d cleanup 2025-06-17 16:14:06 -07:00
fuscodev d1dc6977ab feat: edit mode preference (#666)
* lock/unlock pages

* remove using isLocked column - add default page edit state preference

* * Move state management to editors (avoids flickers on edit mode switch)
* Rename variables
* Add strings to translation file
* Memoize components in page component
* Fix title editor sending update request on editable state change

* fixed errors merging main

* Fix embed view in read-only mode

* remove unused line

* sync

* fix responsiveness on mobile

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-06-18 00:11:47 +01:00
Philip Okugbe 5f62448894 less create workspace form fields in cloud (#1265)
* sync

* less signup form fields in cloud

* min length
2025-06-17 23:56:07 +01:00
Philip Okugbe 44445fbf46 fix: enforce SSO in invitation signups (#1258) 2025-06-15 20:25:15 +01:00
Philip Okugbe 1c674efddd fix: revert tiptap version (#1255) 2025-06-13 21:38:49 +01:00
Philip Okugbe ccf7e34e99 feat: ukrainian language support (#1250) 2025-06-11 23:31:45 +01:00
Philip Okugbe f39d48d6ee New Crowdin updates (#1063)
* New translations translation.json
2025-06-11 23:21:01 +01:00
Philip Okugbe f584ea84b0 chore: upgrade packages (#1242)
* upgrade tiptap editor extensions

* upgrade packages

* fix type issue
2025-06-11 23:18:39 +01:00
Chai bc0c4d6258 fix: make link popup work on safari (#1243)
* fix: make link popup work on safari

* fix: second iteration

* chore: cleanup

* chore: format

* chore: undo unused stuff
2025-06-11 23:09:59 +01:00
Philip Okugbe d8da307a61 feat: enhance excalidraw (#1240)
* WIP

* use next excalidraw version

* support local persistence for excalidraw library.

Co-authored-by: Drauggy <n.fomenko@safe-tech.ru>

---------

Co-authored-by: Drauggy <n.fomenko@safe-tech.ru>
2025-06-09 23:25:36 +01:00
Philip Okugbe 50b3f9ddd9 generic iframe embed (#1234) 2025-06-09 22:32:23 +01:00
Philip Okugbe 0029f84d50 feat: toggle table header row and column (#1203)
* feat: toggle table header row and column
* switch position
2025-06-09 05:39:43 +01:00
Philip Okugbe 6d024fc3de feat: bulk page imports (#1219)
* refactor imports - WIP

* Add readstream

* WIP

* fix attachmentId render

* fix attachmentId render

* turndown video tag

* feat: add stream upload support and improve file handling

- Add stream upload functionality to storage drivers\n- Improve ZIP file extraction with better encoding handling\n- Fix attachment ID rendering issues\n- Add AWS S3 upload stream support\n- Update dependencies for better compatibility

* WIP

* notion formatter

* move embed parser to editor-ext package

* import embeds

* utility files

* cleanup

* Switch from happy-dom to cheerio
* Refine code

* WIP

* bug fixes and UI

* sync

* WIP

* sync

* keep import modal mounted

* Show modal during upload

* WIP

* WIP
2025-06-09 04:29:27 +01:00
fuscodev ce1503af85 fix: sidebar list when changing workspace (#1150)
* init

* navigate in overview if current page is in deleted node

* fix: implement pagination in sidebar-pages queries

* fix: appendNodeChildren()

Preserve deeper children if they exist and remove node if deleted
2025-06-08 03:27:09 +01:00
Philipinho 69447fc375 Merge branch 'main' of https://github.com/docmost/docmost 2025-05-21 08:43:56 -07:00
Philipinho 858ff9da06 sync 2025-05-20 09:27:30 -07:00
sanua356 343b2976c2 #1186/chore: add support language abap syntax highlight (#1188) 2025-05-19 20:05:31 +01:00
Philip Okugbe 7491224d0f hide shared page branding in EE (#1193)
* hide shared page branding in EE

* Hide branding in business plan
2025-05-17 19:17:34 +01:00
Philip Okugbe 4a0b4040ed Add second plan (#1187) 2025-05-17 19:03:01 +01:00
fuscodev e3ba817723 feat: comment editor emoji picker and ctrl+enter action (#1121)
* commenteditor-emoji-picker

* capture Mac command key
* remove tooltip

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-05-16 20:01:27 +01:00
fuscodev b0491d5da4 feat: create new page from mention (#1153)
* init

* create page in relative parent root
2025-05-16 19:15:11 +01:00
极地 1c200dbd0f fix(table-hover): adjust row height to prevent unexpected scrollbar on hover (#1124)
fix: Hover table style height error causing scrollbar to appear #1108
2025-05-16 16:26:05 +01:00
fuscodev fb7e4a7956 fix: copy/move select (#1174) 2025-05-16 16:24:31 +01:00
fuscodev 1413033568 feat: realtime comments (#1144)
* init

* fix: close bubblemenu after comment and wait before scroll

* scroll to comment when click

* highlight comment animation
2025-05-16 16:18:23 +01:00
fuscodev 00f4588c21 fix title update (#1154) 2025-05-16 16:11:29 +01:00
fuscodev 3a75251e75 fix alignment in shared page (#1123) 2025-05-16 16:00:47 +01:00
Philipinho c6bca6a602 fix deprecated kysely usage 2025-05-09 16:44:33 +01:00
edo0 55d1a2c932 Fix typo in enforce-sso.tsx (#1145) 2025-05-09 11:11:02 +01:00
Philipinho bc3cb2d63f fix: increase random subdomain suffix 2025-05-07 15:10:58 +01:00
Philipinho 7adbf85030 v0.20.4 2025-04-30 14:44:58 +01:00
Philip Okugbe de7982fe30 feat: copy page to different space (#1118)
* Add copy page to space endpoint
* copy storage function
* copy function
* feat: copy attachments too
* Copy page - WIP
* fix type
* sync
* cleanup
2025-04-30 14:43:16 +01:00
Philipinho 0402f7efb5 sync 2025-04-30 14:33:01 +01:00
Philipinho 8327251ab6 fix typo 2025-04-29 23:30:12 +01:00
Philip Okugbe e8847bd9cd fix: handle unhandled exceptions (#1116)
* Handle unhandled exceptions
* cleanup
2025-04-29 23:29:00 +01:00
Philipinho 9bbd62e0f0 v0.20.3 2025-04-24 23:22:53 +01:00
Philipinho 0289c5cb09 Reduce markdown checkbox space 2025-04-24 23:19:39 +01:00
Philip Okugbe 7993532111 fix page export (#1081) 2025-04-24 23:18:54 +01:00
Philipinho 31e5c0c660 v0.20.2 2025-04-24 17:57:14 +01:00
Philipinho 33c314d4e8 remove clickoutside hook 2025-04-24 17:56:54 +01:00
Philipinho 08f223899a cloud trial refactor 2025-04-23 16:07:58 +01:00
Philipinho c528f7e858 v0.20.1 2025-04-23 14:34:28 +01:00
Philip Okugbe c26a851d52 feat: enhance public sharing (#1057)
* fix tree nodes sort

* remove comment mark in shares

* remove clickoutside hook for now

* feat: search in shared pages

* fix user-select

* use Link

* render page icons
2025-04-23 14:32:35 +01:00
Philipinho de5f90309c v0.20.0 2025-04-22 22:49:45 +01:00
Philipinho 0ec3ff2965 Add empty placeholder text 2025-04-22 22:48:12 +01:00
Philipinho acffeacdbc fix TOC 2025-04-22 22:47:34 +01:00
Philip Okugbe 00d92a3690 New Crowdin updates (#1008)
* New translations translation.json (Russian)

* New translations translation.json (Russian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Spanish)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Italian)

* New translations translation.json (Japanese)

* New translations translation.json (Korean)

* New translations translation.json (Dutch)

* New translations translation.json (Russian)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)
2025-04-22 20:57:07 +01:00
Diego Ochoa 3430f715ec feat: remember and restore previous route when exiting settings (#1046)
Improves user experience by allowing users to return to the previous
page after visiting the Settings section.

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-04-22 20:47:57 +01:00
Philip Okugbe 6c422011ac feat: public page sharing (#1012)
* Share - WIP

* - public attachment links
- WIP

* WIP

* WIP

* Share - WIP

* WIP

* WIP

* include userRole in space object

* WIP

* Server render shared page meta tags

* disable user select

* Close Navbar on outside click on mobile

* update shared page spaceId

* WIP

* fix

* close sidebar on click

* close sidebar

* defaults

* update copy

* Store share key in lowercase

* refactor page breadcrumbs

* Change copy

* add link ref

* open link button

* add meta og:title

* add twitter tags

* WIP

* make shares/info endpoint public

* fix

* * add /p/ segment to share urls
* minore fixes

* change mobile breadcrumb icon
2025-04-22 20:37:32 +01:00
Philipinho 3e8824435d update vite and axios 2025-04-22 20:28:27 +01:00
Philip Okugbe 37a1804db9 Revert "switch to vite rolldown (#1048)" (#1050)
This reverts commit 1a1b2c8682.
2025-04-22 20:00:36 +01:00
Philip Okugbe 882f3093bd search space members by email (#1049) 2025-04-22 19:37:06 +01:00
Philip Okugbe 1a1b2c8682 switch to vite rolldown (#1048)
* switch to vite rolldown

* update
2025-04-22 15:52:44 +01:00
Philip Okugbe 10b67929ea Update README.md 2025-04-21 21:50:21 +01:00
Philip Okugbe 5c957fda8d fix: nested tree open state 2025-04-21 19:24:25 +01:00
Philip Okugbe 862f6d4820 use non-esm nanoid version (#1040) 2025-04-19 19:45:09 +01:00
Philipinho de57d05199 0.10.2 2025-04-15 12:48:40 +01:00
Philipinho 89ec990232 sync ee 2025-04-15 12:46:28 +01:00
Philipinho 49d0f1cc9a Add click handler 2025-04-11 13:41:43 +01:00
1085 changed files with 107578 additions and 15967 deletions
+3 -2
View File
@@ -1,5 +1,6 @@
node_modules node_modules
.git .git
.gitignore
dist dist
data /data
.env*
.nx
+19
View File
@@ -43,4 +43,23 @@ POSTMARK_TOKEN=
# for custom drawio server # for custom drawio server
DRAWIO_URL= DRAWIO_URL=
# Gotenberg URL for server-side PDF export
GOTENBERG_URL=
DISABLE_TELEMETRY=false DISABLE_TELEMETRY=false
# Allow other sites to embed Docmost in an iframe.
IFRAME_EMBED_ALLOWED=false
# Only used when IFRAME_EMBED_ALLOWED=true. When empty, any origin is allowed.
# Example: https://intranet.example.com,https://portal.example.com
IFRAME_ALLOWED_ORIGINS=
# Enable debug logging in production (default: false)
DEBUG_MODE=false
# Log database queries
DEBUG_DB=false
# Log http requests
LOG_HTTP=false
+154
View File
@@ -0,0 +1,154 @@
name: Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: 'Version tag (e.g. v0.25.3)'
required: true
permissions:
contents: write
env:
VERSION: ${{ inputs.version || github.ref_name }}
jobs:
build:
strategy:
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
suffix: amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
suffix: arm64
runs-on: ${{ matrix.runner }}
steps:
- name: Generate token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.BUILD_APP_ID }}
private-key: ${{ secrets.BUILD_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Checkout with submodules
uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ steps.app-token.outputs.token }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
with:
context: .
platforms: ${{ matrix.platform }}
outputs: type=image,name=docmost/docmost,push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=${{ matrix.suffix }}
cache-to: type=gha,scope=${{ matrix.suffix }},mode=max
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digest-${{ matrix.suffix }}
path: /tmp/digests/*
if-no-files-found: error
- name: Strip v prefix
id: strip-v
run: echo "version=${VERSION#v}" >> "$GITHUB_OUTPUT"
- name: Export Docker image
uses: docker/build-push-action@v6
with:
context: .
platforms: ${{ matrix.platform }}
push: false
tags: |
docmost/docmost:latest
docmost/docmost:${{ steps.strip-v.outputs.version }}
outputs: type=docker,dest=docmost-${{ matrix.suffix }}.docker.tar
cache-from: type=gha,scope=${{ matrix.suffix }}
- name: Compress image
run: gzip docmost-${{ matrix.suffix }}.docker.tar
- name: Upload image archive
uses: actions/upload-artifact@v4
with:
name: docker-image-${{ matrix.suffix }}
path: docmost-${{ matrix.suffix }}.docker.tar.gz
if-no-files-found: error
release:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
pattern: digest-*
path: /tmp/digests
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata for tags
id: meta
uses: docker/metadata-action@v5
with:
images: docmost/docmost
tags: |
type=semver,pattern={{version}},value=${{ env.VERSION }}
type=semver,pattern={{major}}.{{minor}},value=${{ env.VERSION }},enable=${{ !contains(env.VERSION, '-') }}
type=raw,value=latest,enable=${{ !contains(env.VERSION, '-') }}
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf 'docmost/docmost@sha256:%s ' *)
- name: Download image archives
uses: actions/download-artifact@v4
with:
pattern: docker-image-*
path: /tmp/images
merge-multiple: true
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.VERSION }}
files: |
/tmp/images/docmost-amd64.docker.tar.gz
/tmp/images/docmost-arm64.docker.tar.gz
draft: true
+7 -5
View File
@@ -1,19 +1,22 @@
FROM node:22-alpine AS base FROM node:22-slim AS base
LABEL org.opencontainers.image.source="https://github.com/docmost/docmost" LABEL org.opencontainers.image.source="https://github.com/docmost/docmost"
RUN npm install -g pnpm@10.4.0
FROM base AS builder FROM base AS builder
WORKDIR /app WORKDIR /app
COPY . . COPY . .
RUN npm install -g pnpm@10.4.0
RUN pnpm install --frozen-lockfile RUN pnpm install --frozen-lockfile
RUN pnpm build RUN pnpm build
FROM base AS installer FROM base AS installer
RUN apk add --no-cache curl bash RUN apt-get update \
&& apt-get install -y --no-install-recommends curl bash \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app WORKDIR /app
@@ -29,12 +32,11 @@ COPY --from=builder /app/packages/editor-ext/package.json /app/packages/editor-e
# Copy root package files # Copy root package files
COPY --from=builder /app/package.json /app/package.json COPY --from=builder /app/package.json /app/package.json
COPY --from=builder /app/pnpm*.yaml /app/ COPY --from=builder /app/pnpm*.yaml /app/
COPY --from=builder /app/.npmrc /app/.npmrc
# Copy patches # Copy patches
COPY --from=builder /app/patches /app/patches COPY --from=builder /app/patches /app/patches
RUN npm install -g pnpm@10.4.0
RUN chown -R node:node /app RUN chown -R node:node /app
USER node USER node
+16 -2
View File
@@ -4,14 +4,15 @@
Open-source collaborative wiki and documentation software. Open-source collaborative wiki and documentation software.
<br /> <br />
<a href="https://docmost.com"><strong>Website</strong></a> | <a href="https://docmost.com"><strong>Website</strong></a> |
<a href="https://docmost.com/docs"><strong>Documentation</strong></a> <a href="https://docmost.com/docs"><strong>Documentation</strong></a> |
<a href="https://twitter.com/DocmostHQ"><strong>Twitter / X</strong></a>
</p> </p>
</div> </div>
<br /> <br />
## Getting started ## Getting started
To get started with Docmost, please refer to our [documentation](https://docmost.com/docs). To get started with Docmost, please refer to our [documentation](https://docmost.com/docs) or try our [cloud version](https://docmost.com/pricing) .
## Features ## Features
@@ -46,3 +47,16 @@ All files in the following directories are licensed under the Docmost Enterprise
### Contributing ### Contributing
See the [development documentation](https://docmost.com/docs/self-hosting/development) See the [development documentation](https://docmost.com/docs/self-hosting/development)
## Thanks
Special thanks to;
<img width="100" alt="Crowdin" src="https://github.com/user-attachments/assets/a6c3d352-e41b-448d-b6cd-3fbca3109f07" />
[Crowdin](https://crowdin.com/) for providing access to their localization platform.
<img width="48" alt="Algolia-mark-square-white" src="https://github.com/user-attachments/assets/6ccad04a-9589-4965-b6a1-d5cb1f4f9e94" />
[Algolia](https://www.algolia.com/) for providing full-text search to the docs.
+12 -3
View File
@@ -2,10 +2,19 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16x16.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0 user-scalable=no" />
<title>Docmost</title> <title>Docmost</title>
<meta name="theme-color" content="#1f1f1f" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#f6f7f9" media="(prefers-color-scheme: light)" />
<link rel="manifest" href="/manifest.json" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-touch-fullscreen" content="yes" />
<meta name="apple-mobile-web-app-title" content="Docmost" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<!--meta-tags-->
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+79 -65
View File
@@ -1,81 +1,95 @@
{ {
"name": "client", "name": "client",
"private": true, "private": true,
"version": "0.10.1", "version": "0.90.0",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview", "preview": "vite preview",
"format": "prettier --write \"src/**/*.tsx\" \"src/**/*.ts\"" "format": "prettier --write \"src/**/*.tsx\" \"src/**/*.ts\"",
"test": "vitest run",
"test:watch": "vitest"
}, },
"dependencies": { "dependencies": {
"@casl/ability": "^6.7.2", "@atlaskit/pragmatic-drag-and-drop": "1.8.1",
"@casl/react": "^4.0.0", "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "2.1.5",
"@atlaskit/pragmatic-drag-and-drop-flourish": "2.0.15",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0",
"@atlaskit/pragmatic-drag-and-drop-live-region": "1.3.4",
"@casl/react": "5.0.1",
"@docmost/editor-ext": "workspace:*", "@docmost/editor-ext": "workspace:*",
"@emoji-mart/data": "^1.2.1", "@excalidraw/excalidraw": "0.18.0-3a5ef40",
"@emoji-mart/react": "^1.1.1", "@mantine/core": "8.3.18",
"@excalidraw/excalidraw": "^0.17.6", "@mantine/dates": "8.3.18",
"@mantine/core": "^7.17.0", "@mantine/form": "8.3.18",
"@mantine/form": "^7.17.0", "@mantine/hooks": "8.3.18",
"@mantine/hooks": "^7.17.0", "@mantine/modals": "8.3.18",
"@mantine/modals": "^7.17.0", "@mantine/notifications": "8.3.18",
"@mantine/notifications": "^7.17.0", "@mantine/spotlight": "8.3.18",
"@mantine/spotlight": "^7.17.0", "@slidoapp/emoji-mart": "5.8.7",
"@tabler/icons-react": "^3.22.0", "@slidoapp/emoji-mart-data": "1.2.4",
"@tanstack/react-query": "^5.61.4", "@slidoapp/emoji-mart-react": "1.1.5",
"@tiptap/extension-character-count": "^2.11.5", "@tabler/icons-react": "3.40.0",
"axios": "^1.7.9", "@tanstack/react-query": "5.90.17",
"clsx": "^2.1.1", "@tanstack/react-virtual": "3.13.24",
"emoji-mart": "^5.6.0", "alfaaz": "1.1.0",
"file-saver": "^2.0.5", "axios": "1.16.0",
"i18next": "^23.14.0", "blueimp-load-image": "5.16.0",
"i18next-http-backend": "^2.6.1", "clsx": "2.1.1",
"jotai": "^2.12.1", "file-saver": "2.0.5",
"jotai-optics": "^0.4.0", "highlightjs-sap-abap": "0.3.0",
"js-cookie": "^3.0.5", "i18next": "25.10.1",
"jwt-decode": "^4.0.0", "i18next-http-backend": "3.0.6",
"katex": "0.16.21", "jotai": "2.18.1",
"lowlight": "^3.2.0", "jotai-optics": "0.4.0",
"mermaid": "^11.4.1", "js-cookie": "3.0.5",
"mitt": "^3.0.1", "jwt-decode": "4.0.0",
"react": "^18.3.1", "katex": "0.16.40",
"react-arborist": "3.4.0", "lowlight": "3.3.0",
"react-clear-modal": "^2.0.11", "mantine-form-zod-resolver": "1.3.0",
"mermaid": "11.15.0",
"mitt": "3.0.1",
"posthog-js": "1.372.2",
"react": "18.3.1",
"react-clear-modal": "^2.0.18",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-drawio": "^1.0.1", "react-drawio": "1.0.7",
"react-error-boundary": "^4.1.2", "react-error-boundary": "6.1.1",
"react-helmet-async": "^2.0.5", "react-helmet-async": "3.0.0",
"react-i18next": "^15.0.1", "react-i18next": "16.5.8",
"react-router-dom": "^7.0.1", "react-router-dom": "7.13.1",
"semver": "^7.7.1", "semver": "7.7.4",
"socket.io-client": "^4.8.1", "socket.io-client": "4.8.3",
"tippy.js": "^6.3.7", "zod": "4.3.6"
"tiptap-extension-global-drag-handle": "^0.1.18",
"zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.16.0", "@eslint/js": "9.28.0",
"@tanstack/eslint-plugin-query": "^5.62.1", "@tanstack/eslint-plugin-query": "5.94.4",
"@types/file-saver": "^2.0.7", "@testing-library/jest-dom": "6.6.0",
"@types/js-cookie": "^3.0.6", "@testing-library/react": "16.1.0",
"@types/katex": "^0.16.7", "@types/blueimp-load-image": "5.16.6",
"@types/node": "22.10.0", "@types/file-saver": "2.0.7",
"@types/react": "^18.3.12", "@types/js-cookie": "3.0.6",
"@types/react-dom": "^18.3.1", "@types/katex": "0.16.8",
"@vitejs/plugin-react": "^4.3.4", "@types/node": "22.19.1",
"eslint": "^9.15.0", "@types/react": "18.3.12",
"eslint-plugin-react": "^7.37.2", "@types/react-dom": "18.3.1",
"eslint-plugin-react-hooks": "^5.1.0", "@vitejs/plugin-react": "6.0.1",
"eslint-plugin-react-refresh": "^0.4.16", "eslint": "9.28.0",
"globals": "^15.13.0", "eslint-plugin-react": "7.37.5",
"optics-ts": "^2.4.1", "eslint-plugin-react-hooks": "7.0.1",
"postcss": "^8.4.49", "eslint-plugin-react-refresh": "0.5.2",
"postcss-preset-mantine": "^1.17.0", "globals": "15.13.0",
"postcss-simple-vars": "^7.0.1", "jsdom": "25.0.0",
"prettier": "^3.4.1", "optics-ts": "2.4.1",
"typescript": "^5.7.2", "postcss": "8.5.14",
"typescript-eslint": "^8.17.0", "postcss-preset-mantine": "1.18.0",
"vite": "^6.1.0" "postcss-simple-vars": "7.0.1",
"prettier": "3.8.1",
"typescript": "5.9.3",
"typescript-eslint": "8.57.1",
"vite": "8.0.5",
"vitest": "4.1.6"
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 562 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 881 B

+752 -30
View File
@@ -7,6 +7,7 @@
"Add members": "Mitglieder hinzufügen", "Add members": "Mitglieder hinzufügen",
"Add to groups": "Zu Gruppen hinzufügen", "Add to groups": "Zu Gruppen hinzufügen",
"Add space members": "Bereichsmitglieder hinzufügen", "Add space members": "Bereichsmitglieder hinzufügen",
"Add to favorites": "Zu Favoriten hinzufügen",
"Admin": "Administrator", "Admin": "Administrator",
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Sind Sie sicher, dass Sie diese Gruppe löschen möchten? Mitglieder verlieren den Zugang zu den Ressourcen, auf die diese Gruppe zugreifen kann.", "Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Sind Sie sicher, dass Sie diese Gruppe löschen möchten? Mitglieder verlieren den Zugang zu den Ressourcen, auf die diese Gruppe zugreifen kann.",
"Are you sure you want to delete this page?": "Sind Sie sicher, dass Sie diese Seite löschen möchten?", "Are you sure you want to delete this page?": "Sind Sie sicher, dass Sie diese Seite löschen möchten?",
@@ -29,6 +30,7 @@
"Choose your preferred interface language.": "Wählen Sie Ihre bevorzugte Benutzersprache.", "Choose your preferred interface language.": "Wählen Sie Ihre bevorzugte Benutzersprache.",
"Choose your preferred page width.": "Wählen Sie Ihre bevorzugte Seitenbreite.", "Choose your preferred page width.": "Wählen Sie Ihre bevorzugte Seitenbreite.",
"Confirm": "Bestätigen", "Confirm": "Bestätigen",
"Copy as Markdown": "Als Markdown kopieren",
"Copy link": "Link kopieren", "Copy link": "Link kopieren",
"Create": "Erstellen", "Create": "Erstellen",
"Create group": "Gruppe erstellen", "Create group": "Gruppe erstellen",
@@ -40,38 +42,43 @@
"Date": "Datum", "Date": "Datum",
"Delete": "Löschen", "Delete": "Löschen",
"Delete group": "Gruppe löschen", "Delete group": "Gruppe löschen",
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Sind Sie sicher, dass Sie diese Seite löschen möchten? Dadurch werden ihre Unterseiten und die Seitengeschichte gelöscht. Diese Aktion ist unwiderruflich.", "Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Sind Sie sicher, dass Sie diese Seite löschen möchten? Dabei werden auch alle Unterseiten und der Seitenverlauf gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.",
"Description": "Beschreibung", "Description": "Beschreibung",
"Details": "Einzelheiten", "Details": "Details",
"e.g ACME": "z. B. ACME", "e.g ACME": "z. B. ACME",
"e.g ACME Inc": "z.B. ACME Inc.", "e.g ACME Inc": "z. B. ACME GmbH",
"e.g Developers": "z. B. Entwickler", "e.g Developers": "z. B. Entwickler",
"e.g Group for developers": "z. B. Gruppe für Entwickler", "e.g Group for developers": "z. B. Gruppe für Entwickler",
"e.g product": "z. B. Produkt", "e.g product": "z. B. Produkt",
"e.g Product Team": "z. B. Produktteam", "e.g Product Team": "z. B. Produktteam",
"e.g Sales": "z. B. Vertrieb", "e.g Sales": "z. B. Vertrieb",
"e.g Space for product team": "z. B. Bereich für das Produktteam", "e.g Space for product team": "z. B. Bereich für das Produktteam",
"e.g Space for sales team to collaborate": "z.B. Bereich für das Vertriebsteam zur Zusammenarbeit", "e.g Space for sales team to collaborate": "z. B. Bereich zur Zusammenarbeit für das Vertriebsteam",
"Edit": "Bearbeiten", "Edit": "Bearbeiten",
"Read": "Lesen",
"Edit group": "Gruppe bearbeiten", "Edit group": "Gruppe bearbeiten",
"Email": "E-Mail", "Email": "E-Mail",
"Enter a strong password": "Geben Sie ein starkes Passwort ein", "Enter a strong password": "Geben Sie ein starkes Passwort ein",
"Enter valid email addresses separated by comma or space max_50": "Geben Sie gültige E-Mail-Adressen ein, getrennt durch Kommas oder Leerzeichen [max: 50]", "Enter valid email addresses separated by comma or space max_50": "Geben Sie gültige E-Mail-Adressen ein, getrennt durch Kommas oder Leerzeichen [max: 50]",
"enter valid emails addresses": "gültige E-Mail-Adressen eingeben", "enter valid emails addresses": "Geben Sie gültige E-Mail-Adressen ein",
"Enter your current password": "Geben Sie Ihr aktuelles Passwort ein", "Enter your current password": "Geben Sie Ihr aktuelles Passwort ein",
"enter your full name": "Geben Sie Ihren vollständigen Namen ein", "enter your full name": "Geben Sie Ihren vollständigen Namen ein",
"Enter your new password": "Geben Sie Ihr neues Passwort ein", "Enter your new password": "Geben Sie Ihr neues Passwort ein",
"Enter your new preferred email": "Geben Sie Ihre neue bevorzugte E-Mail ein", "Enter your new preferred email": "Geben Sie Ihre neue bevorzugte E-Mail ein",
"Enter your password": "Geben Sie Ihr Passwort ein", "Enter your password": "Geben Sie Ihr Passwort ein",
"Error fetching page data.": "Fehler beim Abrufen der Seitendaten.", "Error fetching page data.": "Fehler beim Abrufen der Seitendaten.",
"Error loading page history.": "Fehler beim Laden der Seitengeschichte.", "Error loading page history.": "Fehler beim Laden des Seitenverlaufs.",
"Export": "Exportieren", "Export": "Exportieren",
"Failed to create page": "Erstellung der Seite fehlgeschlagen", "Failed to create page": "Erstellung der Seite fehlgeschlagen",
"Failed to delete page": "Löschen der Seite fehlgeschlagen", "Failed to delete page": "Löschen der Seite fehlgeschlagen",
"Failed to restore page": "Seite konnte nicht wiederhergestellt werden",
"Failed to fetch recent pages": "Fehler beim Abrufen der letzten Seiten", "Failed to fetch recent pages": "Fehler beim Abrufen der letzten Seiten",
"Failed to import pages": "Import der Seiten fehlgeschlagen", "Failed to import pages": "Import der Seiten fehlgeschlagen",
"Failed to load page. An error occurred.": "Seite konnte nicht geladen werden. Es ist ein Fehler aufgetreten.", "Failed to load page. An error occurred.": "Seite konnte nicht geladen werden. Es ist ein Fehler aufgetreten.",
"Failed to update data": "Aktualisierung der Daten fehlgeschlagen", "Failed to update data": "Aktualisierung der Daten fehlgeschlagen",
"Favorite spaces": "Favorisierte Bereiche",
"Favorite spaces appear here": "Favorisierte Bereiche werden hier angezeigt",
"Favorites": "Favoriten",
"Full access": "Voller Zugriff", "Full access": "Voller Zugriff",
"Full page width": "Volle Seitenbreite", "Full page width": "Volle Seitenbreite",
"Full width": "Volle Breite", "Full width": "Volle Breite",
@@ -85,11 +92,12 @@
"Import pages": "Seiten importieren", "Import pages": "Seiten importieren",
"Import pages & space settings": "Seiten und Bereichseinstellungen importieren", "Import pages & space settings": "Seiten und Bereichseinstellungen importieren",
"Importing pages": "Seiten werden importiert", "Importing pages": "Seiten werden importiert",
"invalid invitation link": "ungültiger Einladungslink", "invalid invitation link": "Ungültiger Einladungslink",
"Invitation signup": "Einladung zur Anmeldung", "Invitation signup": "Einladung zur Anmeldung",
"Invite by email": "Einladen per E-Mail", "Invite by email": "Einladen per E-Mail",
"Invite members": "Mitglieder einladen", "Invite members": "Mitglieder einladen",
"Invite new members": "Neue Mitglieder einladen", "Invite new members": "Neue Mitglieder einladen",
"Invite People": "Personen einladen",
"Invited members who are yet to accept their invitation will appear here.": "Eingeladene Mitglieder, die ihre Einladung noch nicht angenommen haben, werden hier angezeigt.", "Invited members who are yet to accept their invitation will appear here.": "Eingeladene Mitglieder, die ihre Einladung noch nicht angenommen haben, werden hier angezeigt.",
"Invited members will be granted access to spaces the groups can access": "Eingeladene Mitglieder erhalten Zugriff auf die Bereiche, auf die die Gruppen zugreifen können", "Invited members will be granted access to spaces the groups can access": "Eingeladene Mitglieder erhalten Zugriff auf die Bereiche, auf die die Gruppen zugreifen können",
"Join the workspace": "Dem Arbeitsbereich beitreten", "Join the workspace": "Dem Arbeitsbereich beitreten",
@@ -104,7 +112,7 @@
"Member": "Mitglied", "Member": "Mitglied",
"members": "Mitglieder", "members": "Mitglieder",
"Members": "Mitglieder", "Members": "Mitglieder",
"My preferences": "Meine Vorlieben", "My preferences": "Meine Einstellungen",
"My Profile": "Mein Profil", "My Profile": "Mein Profil",
"My profile": "Mein Profil", "My profile": "Mein Profil",
"Name": "Name", "Name": "Name",
@@ -112,27 +120,32 @@
"New page": "Neue Seite", "New page": "Neue Seite",
"New password": "Neues Passwort", "New password": "Neues Passwort",
"No group found": "Keine Gruppe gefunden", "No group found": "Keine Gruppe gefunden",
"No page history saved yet.": "Es wurde noch keine Seitengeschichte gespeichert.", "No page history saved yet.": "Es wurde noch kein Seitenverlauf gespeichert.",
"No pages yet": "Noch keine Seiten", "No pages yet": "Noch keine Seiten",
"No shared pages": "Keine freigegebenen Seiten",
"No results found...": "Keine Ergebnisse gefunden...", "No results found...": "Keine Ergebnisse gefunden...",
"No user found": "Kein Benutzer gefunden", "No user found": "Kein Benutzer gefunden",
"Overview": "Überblick", "Overview": "Überblick",
"Owner": "Besitzer", "Owner": "Besitzer",
"page": "Seite", "page": "Seite",
"Page deleted successfully": "Seite erfolgreich gelöscht", "Page deleted successfully": "Seite erfolgreich gelöscht",
"Page history": "Seitengeschichte", "Page history": "Seitenverlauf",
"Select version": "Version auswählen",
"Highlight changes": "Änderungen hervorheben",
"Page import is in progress. Please do not close this tab.": "Der Seitenimport läuft. Bitte schließen Sie diesen Tab nicht.", "Page import is in progress. Please do not close this tab.": "Der Seitenimport läuft. Bitte schließen Sie diesen Tab nicht.",
"Pages": "Seiten", "Pages": "Seiten",
"pages": "Seiten", "pages": "Seiten",
"Password": "Passwort", "Password": "Passwort",
"Password changed successfully": "Passwort erfolgreich geändert", "Password changed successfully": "Passwort erfolgreich geändert",
"People": "Personen",
"Pending": "Ausstehend", "Pending": "Ausstehend",
"Please confirm your action": "Bitte bestätigen Sie Ihre Aktion", "Please confirm your action": "Bitte bestätigen Sie Ihre Aktion",
"Preferences": "Vorlieben", "Preferences": "Einstellungen",
"Print PDF": "PDF drucken", "Print PDF": "PDF drucken",
"Profile": "Profil", "Profile": "Profil",
"Recently updated": "Kürzlich aktualisiert", "Recently updated": "Kürzlich aktualisiert",
"Remove": "Entfernen", "Remove": "Entfernen",
"Remove from favorites": "Aus Favoriten entfernen",
"Remove group member": "Gruppenmitglied entfernen", "Remove group member": "Gruppenmitglied entfernen",
"Remove space member": "Bereichsmitglied entfernen", "Remove space member": "Bereichsmitglied entfernen",
"Restore": "Wiederherstellen", "Restore": "Wiederherstellen",
@@ -145,12 +158,12 @@
"Search...": "Suche...", "Search...": "Suche...",
"Select language": "Sprache auswählen", "Select language": "Sprache auswählen",
"Select role": "Rolle auswählen", "Select role": "Rolle auswählen",
"Select role to assign to all invited members": "Rolle für alle eingeladenen Mitglieder auswählen", "Select role to assign to all invited members": "Wählen Sie die Rolle aus, die allen eingeladenen Mitgliedern zugewiesen werden soll",
"Select theme": "Design auswählen", "Select theme": "Design auswählen",
"Send invitation": "Einladung senden", "Send invitation": "Einladung senden",
"Invitation sent": "Einladung gesendet", "Invitation sent": "Einladung gesendet",
"Settings": "Einstellungen", "Settings": "Einstellungen",
"Setup workspace": "Arbeitsbereich einrichten", "Setup workspace": "Workspace einrichten",
"Sign In": "Anmelden", "Sign In": "Anmelden",
"Sign Up": "Registrieren", "Sign Up": "Registrieren",
"Slug": "Slug", "Slug": "Slug",
@@ -159,16 +172,17 @@
"Space menu": "Bereichsmenü", "Space menu": "Bereichsmenü",
"Space name": "Bereichsname", "Space name": "Bereichsname",
"Space settings": "Bereichseinstellungen", "Space settings": "Bereichseinstellungen",
"Space slug": "Slug des Bereichs", "Space slug": "Bereichs-Slug",
"Spaces": "Bereiche", "Spaces": "Bereiche",
"Spaces you belong to": "Bereiche, denen Sie angehören", "Spaces you belong to": "Bereiche, zu denen Sie gehören",
"No space found": "Keine Bereiche gefunden", "No space found": "Kein Bereich gefunden",
"Search for spaces": "Nach Bereichen suchen", "Search for spaces": "Nach Bereichen suchen",
"Start typing to search...": "Anfangen zu tippen, um zu suchen...", "Start typing to search...": "Anfangen zu tippen, um zu suchen...",
"Status": "Status", "Status": "Status",
"Successfully imported": "Erfolgreich importiert", "Successfully imported": "Erfolgreich importiert",
"Successfully restored": "Erfolgreich wiederhergestellt", "Successfully restored": "Erfolgreich wiederhergestellt",
"System settings": "Systemeinstellungen", "System settings": "Systemeinstellungen",
"Templates": "Vorlagen",
"Theme": "Design", "Theme": "Design",
"To change your email, you have to enter your password and new email.": "Um Ihre E-Mail-Adresse zu ändern, müssen Sie Ihr Passwort und Ihre neue E-Mail-Adresse eingeben.", "To change your email, you have to enter your password and new email.": "Um Ihre E-Mail-Adresse zu ändern, müssen Sie Ihr Passwort und Ihre neue E-Mail-Adresse eingeben.",
"Toggle full page width": "Volle Seitenbreite umschalten", "Toggle full page width": "Volle Seitenbreite umschalten",
@@ -177,9 +191,9 @@
"Untitled": "Ohne Titel", "Untitled": "Ohne Titel",
"Updated successfully": "Erfolgreich aktualisiert", "Updated successfully": "Erfolgreich aktualisiert",
"User": "Benutzer", "User": "Benutzer",
"Workspace": "Arbeitsbereich", "Workspace": "Workspace",
"Workspace Name": "Arbeitsbereichsname", "Workspace Name": "Workspace-Name",
"Workspace settings": "Arbeitsbereich-Einstellungen", "Workspace settings": "Workspace-Einstellungen",
"You can change your password here.": "Hier können Sie Ihr Passwort ändern.", "You can change your password here.": "Hier können Sie Ihr Passwort ändern.",
"Your Email": "Ihre E-Mail", "Your Email": "Ihre E-Mail",
"Your import is complete.": "Ihr Import ist abgeschlossen.", "Your import is complete.": "Ihr Import ist abgeschlossen.",
@@ -203,9 +217,14 @@
"Reply...": "Antworten...", "Reply...": "Antworten...",
"Error loading comments.": "Fehler beim Laden der Kommentare.", "Error loading comments.": "Fehler beim Laden der Kommentare.",
"No comments yet.": "Noch keine Kommentare.", "No comments yet.": "Noch keine Kommentare.",
"No open comments.": "Keine offenen Kommentare.",
"No resolved comments.": "Keine gelösten Kommentare.",
"Add a comment...": "Kommentar hinzufügen...",
"Edit comment": "Kommentar bearbeiten", "Edit comment": "Kommentar bearbeiten",
"Delete comment": "Kommentar löschen", "Delete comment": "Kommentar löschen",
"Are you sure you want to delete this comment?": "Sind Sie sicher, dass Sie diesen Kommentar löschen möchten?", "Are you sure you want to delete this comment?": "Sind Sie sicher, dass Sie diesen Kommentar löschen möchten?",
"Delete chat": "Chat löschen",
"Are you sure you want to delete '{{title}}'? This action cannot be undone.": "Sind Sie sicher, dass Sie '{{title}}' löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
"Comment created successfully": "Kommentar erfolgreich erstellt", "Comment created successfully": "Kommentar erfolgreich erstellt",
"Error creating comment": "Fehler beim Erstellen des Kommentars", "Error creating comment": "Fehler beim Erstellen des Kommentars",
"Comment updated successfully": "Kommentar erfolgreich aktualisiert", "Comment updated successfully": "Kommentar erfolgreich aktualisiert",
@@ -213,7 +232,17 @@
"Comment deleted successfully": "Kommentar erfolgreich gelöscht", "Comment deleted successfully": "Kommentar erfolgreich gelöscht",
"Failed to delete comment": "Löschen des Kommentars fehlgeschlagen", "Failed to delete comment": "Löschen des Kommentars fehlgeschlagen",
"Comment resolved successfully": "Kommentar erfolgreich gelöst", "Comment resolved successfully": "Kommentar erfolgreich gelöst",
"Comment re-opened successfully": "Kommentar erfolgreich wieder geöffnet",
"Comment unresolved successfully": "Kommentar erfolgreich als ungelöst markiert",
"Failed to resolve comment": "Lösen des Kommentars fehlgeschlagen", "Failed to resolve comment": "Lösen des Kommentars fehlgeschlagen",
"Resolve comment": "Kommentar lösen",
"Unresolve comment": "Kommentar als ungelöst markieren",
"Resolve Comment Thread": "Kommentarthread lösen",
"Unresolve Comment Thread": "Kommentarthread als ungelöst markieren",
"Are you sure you want to resolve this comment thread? This will mark it as completed.": "Sind Sie sicher, dass Sie diesen Kommentarthread lösen möchten? Dies wird als abgeschlossen markiert.",
"Are you sure you want to unresolve this comment thread?": "Sind Sie sicher, dass Sie diesen Kommentarthread nicht lösen möchten?",
"Resolved": "Gelöst",
"No active comments.": "Keine aktiven Kommentare.",
"Revoke invitation": "Einladung widerrufen", "Revoke invitation": "Einladung widerrufen",
"Revoke": "Widerrufen", "Revoke": "Widerrufen",
"Don't": "Nicht", "Don't": "Nicht",
@@ -222,7 +251,9 @@
"Anyone with this link can join this workspace.": "Jeder mit diesem Link kann dem Arbeitsbereich beitreten.", "Anyone with this link can join this workspace.": "Jeder mit diesem Link kann dem Arbeitsbereich beitreten.",
"Invite link": "Einladungslink", "Invite link": "Einladungslink",
"Copy": "Kopieren", "Copy": "Kopieren",
"Copy to space": "In Bereich kopieren",
"Copied": "Kopiert", "Copied": "Kopiert",
"Duplicate": "Duplizieren",
"Select a user": "Benutzer auswählen", "Select a user": "Benutzer auswählen",
"Select a group": "Gruppe auswählen", "Select a group": "Gruppe auswählen",
"Export all pages and attachments in this space.": "Alle Seiten und Anhänge in diesem Bereich exportieren.", "Export all pages and attachments in this space.": "Alle Seiten und Anhänge in diesem Bereich exportieren.",
@@ -230,7 +261,7 @@
"Are you sure you want to delete this space?": "Sind Sie sicher, dass Sie diesen Bereich löschen möchten?", "Are you sure you want to delete this space?": "Sind Sie sicher, dass Sie diesen Bereich löschen möchten?",
"Delete this space with all its pages and data.": "Diesen Bereich mit allen Seiten und Daten löschen.", "Delete this space with all its pages and data.": "Diesen Bereich mit allen Seiten und Daten löschen.",
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Alle Seiten, Kommentare, Anhänge und Berechtigungen in diesem Bereich werden unwiderruflich gelöscht.", "All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Alle Seiten, Kommentare, Anhänge und Berechtigungen in diesem Bereich werden unwiderruflich gelöscht.",
"Confirm space name": "Bestätigen Sie den Namen des Arbeitsbereichs", "Confirm space name": "Bereichsnamen bestätigen",
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Geben Sie den Namen des Bereichs <b>{{spaceName}}</b> ein, um Ihre Aktion zu bestätigen.", "Type the space name <b>{{spaceName}}</b> to confirm your action.": "Geben Sie den Namen des Bereichs <b>{{spaceName}}</b> ein, um Ihre Aktion zu bestätigen.",
"Format": "Format", "Format": "Format",
"Include subpages": "Unterseiten einbeziehen", "Include subpages": "Unterseiten einbeziehen",
@@ -239,12 +270,16 @@
"Export failed:": "Export fehlgeschlagen:", "Export failed:": "Export fehlgeschlagen:",
"export error": "Exportfehler", "export error": "Exportfehler",
"Export page": "Seite exportieren", "Export page": "Seite exportieren",
"Export successful": "Export erfolgreich",
"Export space": "Bereich exportieren", "Export space": "Bereich exportieren",
"Export {{type}}": "Exportiere {{type}}", "Export {{type}}": "Exportiere {{type}}",
"File exceeds the {{limit}} attachment limit": "Datei überschreitet das Anhängelimit von {{limit}}", "File exceeds the {{limit}} attachment limit": "Datei überschreitet das Anhängelimit von {{limit}}",
"Align left": "Links ausrichten", "Align left": "Links ausrichten",
"Align right": "Rechts ausrichten", "Align right": "Rechts ausrichten",
"Align center": "Zentrieren", "Align center": "Zentrieren",
"Alt text": "Alternativtext",
"Describe this for accessibility.": "Beschreiben Sie dies für die Barrierefreiheit.",
"Add a description": "Beschreibung hinzufügen",
"Justify": "Blocksatz", "Justify": "Blocksatz",
"Merge cells": "Zellen zusammenführen", "Merge cells": "Zellen zusammenführen",
"Split cell": "Zelle teilen", "Split cell": "Zelle teilen",
@@ -255,7 +290,21 @@
"Add row above": "Zeile oben hinzufügen", "Add row above": "Zeile oben hinzufügen",
"Add row below": "Zeile unten hinzufügen", "Add row below": "Zeile unten hinzufügen",
"Delete table": "Tabelle löschen", "Delete table": "Tabelle löschen",
"Add column left": "Spalte links hinzufügen",
"Add column right": "Spalte rechts hinzufügen",
"Clear cell": "Zelle leeren",
"Clear cells": "Zellen leeren",
"Toggle header cell": "Kopfzelle umschalten",
"Toggle header column": "Kopfspalte umschalten",
"Toggle header row": "Kopfzeile umschalten",
"Move column left": "Spalte nach links verschieben",
"Move column right": "Spalte nach rechts verschieben",
"Move row down": "Zeile nach unten verschieben",
"Move row up": "Zeile nach oben verschieben",
"Sort A → Z": "A → Z sortieren",
"Sort Z → A": "Z → A sortieren",
"Info": "Info", "Info": "Info",
"Note": "Hinweis",
"Success": "Erfolg", "Success": "Erfolg",
"Warning": "Warnung", "Warning": "Warnung",
"Danger": "Gefahr", "Danger": "Gefahr",
@@ -266,6 +315,11 @@
"Save & Exit": "Speichern & Beenden", "Save & Exit": "Speichern & Beenden",
"Double-click to edit Excalidraw diagram": "Zum Bearbeiten des Excalidraw-Diagramms doppelklicken", "Double-click to edit Excalidraw diagram": "Zum Bearbeiten des Excalidraw-Diagramms doppelklicken",
"Paste link": "Link einfügen", "Paste link": "Link einfügen",
"Paste link or search pages": "Link einfügen oder Seiten durchsuchen",
"Link to web page": "Link zur Webseite",
"Recents": "Zuletzt verwendet",
"Page or URL": "Seite oder URL",
"Link title": "Linktitel",
"Edit link": "Link bearbeiten", "Edit link": "Link bearbeiten",
"Remove link": "Link entfernen", "Remove link": "Link entfernen",
"Add link": "Link hinzufügen", "Add link": "Link hinzufügen",
@@ -311,9 +365,14 @@
"Create block quote.": "Erstellen Sie ein Blockzitat.", "Create block quote.": "Erstellen Sie ein Blockzitat.",
"Insert code snippet.": "Code-Snippet einfügen.", "Insert code snippet.": "Code-Snippet einfügen.",
"Insert horizontal rule divider": "Horizontale Trennlinie einfügen", "Insert horizontal rule divider": "Horizontale Trennlinie einfügen",
"Page break": "Seitenumbruch",
"Insert a page break for printing.": "Einen Seitenumbruch zum Drucken einfügen.",
"Upload any image from your device.": "Laden Sie ein beliebiges Bild von Ihrem Gerät hoch.", "Upload any image from your device.": "Laden Sie ein beliebiges Bild von Ihrem Gerät hoch.",
"Upload any video from your device.": "Laden Sie ein beliebiges Video von Ihrem Gerät hoch.", "Upload any video from your device.": "Laden Sie ein beliebiges Video von Ihrem Gerät hoch.",
"Upload any audio from your device.": "Laden Sie beliebige Audiodateien von Ihrem Gerät hoch.",
"Upload any file from your device.": "Laden Sie eine beliebige Datei von Ihrem Gerät hoch.", "Upload any file from your device.": "Laden Sie eine beliebige Datei von Ihrem Gerät hoch.",
"Uploading {{name}}": "Lade {{name}} hoch",
"Uploading file": "Datei wird hochgeladen",
"Table": "Tabelle", "Table": "Tabelle",
"Insert a table.": "Tabelle einfügen.", "Insert a table.": "Tabelle einfügen.",
"Insert collapsible block.": "Einklappbaren Block einfügen.", "Insert collapsible block.": "Einklappbaren Block einfügen.",
@@ -321,29 +380,53 @@
"Divider": "Trennlinie", "Divider": "Trennlinie",
"Quote": "Zitat", "Quote": "Zitat",
"Image": "Bild", "Image": "Bild",
"Audio": "Audio",
"Embed PDF": "PDF einbetten",
"Upload and embed a PDF file.": "Laden Sie eine PDF-Datei hoch und betten Sie sie ein.",
"Embed as PDF": "Als PDF einbetten",
"Failed to load PDF": "Fehler beim Laden der PDF",
"Convert to attachment": "In Anhang umwandeln",
"File attachment": "Dateianhang", "File attachment": "Dateianhang",
"Toggle block": "Block umschalten", "Toggle block": "Umschaltblock",
"Callout": "Hinweisbox", "Callout": "Hinweisblock",
"Insert callout notice.": "Hinweisbox einfügen.", "Insert callout notice.": "Hinweisbox einfügen.",
"Math inline": "Mathe inline", "Math inline": "Mathe inline",
"Insert inline math equation.": "Mathe-Gleichung inline einfügen.", "Insert inline math equation.": "Mathe-Gleichung inline einfügen.",
"Math block": "Matheblock", "Math block": "Matheblock",
"Insert math equation": "Mathe-Gleichung einfügen", "Insert math equation": "Mathematische Gleichung einfügen",
"Mermaid diagram": "Mermaid-Diagramm", "Mermaid diagram": "Mermaid-Diagramm",
"Insert mermaid diagram": "Mermaid-Diagramm einfügen", "Insert mermaid diagram": "Mermaid-Diagramm einfügen",
"Insert and design Drawio diagrams": "Drawio-Diagramme einfügen und gestalten", "Insert and design Drawio diagrams": "Drawio-Diagramme einfügen und gestalten",
"Insert current date": "Aktuelles Datum einfügen", "Insert current date": "Aktuelles Datum einfügen",
"Draw and sketch excalidraw diagrams": "Excalidraw-Diagramme zeichnen und skizzieren", "Draw and sketch excalidraw diagrams": "Excalidraw-Diagramme zeichnen und skizzieren",
"Multiple": "Mehrere", "Multiple": "Mehrfach",
"Turn into": "In verwandeln",
"Text align": "Text ausrichten",
"This page may have been deleted, moved, or you may not have access.": "\"Diese Seite wurde möglicherweise gelöscht, verschoben oder Sie haben keinen Zugriff darauf.\"",
"Go to homepage": "Zur Startseite",
"Pages you create will show up here.": "\"Die von Ihnen erstellten Seiten werden hier angezeigt.\"",
"Heading {{level}}": "Überschrift {{level}}", "Heading {{level}}": "Überschrift {{level}}",
"Toggle title": "Titel umschalten", "Toggle title": "Titel umschalten",
"Write anything. Enter \"/\" for commands": "Schreiben Sie irgendetwas. Geben Sie \"/\" für Befehle ein", "Write anything. Enter \"/\" for commands": "Schreiben Sie etwas. Geben Sie \"/\" für Befehle ein",
"Write...": "\"Schreiben...\"",
"Column count": "Spaltenanzahl",
"{{count}} Columns": "{{count}} Spalten",
"{{count}} command available_one": "1 command available",
"{{count}} command available_other": "{{count}} commands available",
"{{count}} result available_one": "1 result available",
"{{count}} result available_other": "{{count}} results available",
"Equal columns": "Gleich breite Spalten",
"Left sidebar": "Linke Seitenleiste",
"Right sidebar": "Rechte Seitenleiste",
"Wide center": "Breiter Mittelbereich",
"Left wide": "Breiter linker Bereich",
"Right wide": "Breiter rechter Bereich",
"Names do not match": "Namen stimmen nicht überein", "Names do not match": "Namen stimmen nicht überein",
"Today, {{time}}": "Heute, {{time}}", "Today, {{time}}": "Heute, {{time}}",
"Yesterday, {{time}}": "Gestern, {{time}}", "Yesterday, {{time}}": "Gestern, {{time}}",
"Space created successfully": "Der Bereich wurde erfolgreich erstellt", "Space created successfully": "Bereich erfolgreich erstellt",
"Space updated successfully": "Der Bereich wurde erfolgreich aktualisiert", "Space updated successfully": "Bereich erfolgreich aktualisiert",
"Space deleted successfully": "Der Bereich wurde erfolgreich gelöscht", "Space deleted successfully": "Bereich erfolgreich gelöscht",
"Members added successfully": "Mitglieder erfolgreich hinzugefügt", "Members added successfully": "Mitglieder erfolgreich hinzugefügt",
"Member removed successfully": "Mitglied erfolgreich entfernt", "Member removed successfully": "Mitglied erfolgreich entfernt",
"Member role updated successfully": "Mitgliederrolle erfolgreich aktualisiert", "Member role updated successfully": "Mitgliederrolle erfolgreich aktualisiert",
@@ -351,16 +434,655 @@
"Created at: {{time}}": "Erstellt am: {{time}}", "Created at: {{time}}": "Erstellt am: {{time}}",
"Edited by {{name}} {{time}}": "Bearbeitet von {{name}} {{time}}", "Edited by {{name}} {{time}}": "Bearbeitet von {{name}} {{time}}",
"Word count: {{wordCount}}": "Wortanzahl: {{wordCount}}", "Word count: {{wordCount}}": "Wortanzahl: {{wordCount}}",
"Character count: {{characterCount}}": "Zeichenzahl: {{characterCount}}", "Character count: {{characterCount}}": "Zeichenanzahl: {{characterCount}}",
"New update": "Neues Update", "New update": "Neues Update",
"{{latestVersion}} is available": "{{latestVersion}} ist verfügbar", "{{latestVersion}} is available": "{{latestVersion}} ist verfügbar",
"Default page edit mode": "Standard-Bearbeitungsmodus für Seiten",
"Choose your preferred page edit mode. Avoid accidental edits.": "Wählen Sie Ihren bevorzugten Seitenbearbeitungsmodus. Vermeiden Sie versehentliche Bearbeitungen.",
"Choose {{format}} file": "{{format}}-Datei auswählen",
"Reading": "Lesen",
"Delete member": "Mitglied löschen", "Delete member": "Mitglied löschen",
"Member deleted successfully": "Mitglied erfolgreich gelöscht", "Member deleted successfully": "Mitglied erfolgreich gelöscht",
"Are you sure you want to delete this workspace member? This action is irreversible.": "Sind Sie sicher, dass Sie dieses Arbeitsbereichsmitglied löschen möchten? Diese Aktion ist unwiderruflich.", "Are you sure you want to delete this workspace member? This action is irreversible.": "Sind Sie sicher, dass Sie dieses Arbeitsbereichsmitglied löschen möchten? Diese Aktion ist unwiderruflich.",
"Deactivate member": "Mitglied deaktivieren",
"Activate member": "Mitglied aktivieren",
"Are you sure you want to deactivate this workspace member? They will no longer be able to access this workspace.": "Sind Sie sicher, dass Sie dieses Mitglied des Arbeitsbereichs deaktivieren möchten? Dieses Mitglied kann danach nicht mehr auf diesen Arbeitsbereich zugreifen.",
"Are you sure you want to activate this workspace member?": "Sind Sie sicher, dass Sie dieses Mitglied des Arbeitsbereichs aktivieren möchten?",
"Deactivate": "Deaktivieren",
"Activate": "Aktivieren",
"Deactivated": "Deaktiviert",
"Move": "Verschieben", "Move": "Verschieben",
"Move page": "Seite verschieben", "Move page": "Seite verschieben",
"Move page to a different space.": "Seite in einen anderen Bereich verschieben.", "Move page to a different space.": "Seite in einen anderen Bereich verschieben.",
"Real-time editor connection lost. Retrying...": "Echtzeit-Editor-Verbindung verloren. Wiederholen...", "Real-time editor connection lost. Retrying...": "Echtzeit-Editor-Verbindung verloren. Wiederholen...",
"Table of contents": "Inhaltsverzeichnis", "Table of contents": "Inhaltsverzeichnis",
"Add headings (H1, H2, H3) to generate a table of contents.": "Fügen Sie Überschriften (H1, H2, H3) hinzu, um ein Inhaltsverzeichnis zu erstellen." "Add headings (H1, H2, H3) to generate a table of contents.": "Fügen Sie Überschriften (H1, H2, H3) hinzu, um ein Inhaltsverzeichnis zu erstellen.",
"Share": "Teilen",
"Public sharing": "Öffentliche Freigabe",
"Shared by": "Geteilt von",
"Shared at": "Geteilt am",
"Inherits public sharing from": "Übernimmt öffentliche Freigabe von",
"Share to web": "Im Web teilen",
"Shared to web": "Im Web geteilt",
"Anyone with the link can view this page": "Jeder mit dem Link kann diese Seite ansehen",
"Make this page publicly accessible": "Diese Seite öffentlich zugänglich machen",
"Include sub-pages": "Unterseiten einschließen",
"Make sub-pages public too": "Unterseiten ebenfalls öffentlich machen",
"Allow search engines to index page": "Suchmaschinen das Indexieren der Seite erlauben",
"Open page": "Seite öffnen",
"Page": "Seite",
"Delete public share link": "Öffentlichen Freigabelink löschen",
"Delete share": "Freigabe löschen",
"Are you sure you want to delete this shared link?": "Möchten Sie diesen Freigabelink wirklich löschen?",
"Publicly shared pages from spaces you are a member of will appear here": "Öffentlich freigegebene Seiten aus Bereichen, in denen Sie Mitglied sind, werden hier angezeigt",
"Share deleted successfully": "Freigabe erfolgreich gelöscht",
"Share not found": "Freigabe nicht gefunden",
"Failed to share page": "Seite konnte nicht geteilt werden",
"Disable public sharing": "Öffentliches Teilen deaktivieren",
"Prevent members from sharing pages publicly.": "Verhindern Sie, dass Mitglieder Seiten öffentlich teilen.",
"Toggle public sharing": "Öffentliches Teilen umschalten",
"Toggle space public sharing": "Öffentliches Teilen im Bereich umschalten",
"Allow viewers to comment": "Zuschauern erlauben, Kommentare zu hinterlassen",
"Allow viewers to add comments on pages in this space.": "Erlauben Sie Zuschauern, Kommentare auf Seiten in diesem Bereich hinzuzufügen.",
"Toggle viewer comments": "Zuschauerkommentare umschalten",
"Public sharing is disabled at the workspace level": "Öffentliches Teilen ist auf der Arbeitsbereichsebene deaktiviert",
"Prevent pages in this space from being shared publicly.": "Verhindern Sie, dass Seiten in diesem Bereich öffentlich geteilt werden.",
"Page permissions": "Seitenberechtigungen",
"Control who can view and edit individual pages. Available with an enterprise license.": "Steuern Sie, wer einzelne Seiten ansehen und bearbeiten kann. Verfügbar mit einer Enterprise-Lizenz.",
"Enable public sharing": "Öffentliches Teilen aktivieren",
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "Sind Sie sicher, dass Sie das öffentliche Teilen aktivieren möchten? Mitglieder können Seiten öffentlich teilen.",
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "Sind Sie sicher, dass Sie das öffentliche Teilen deaktivieren möchten? Alle bestehenden Freigabelinks in diesem Arbeitsbereich werden gelöscht.",
"Are you sure you want to enable public sharing for this space?": "Sind Sie sicher, dass Sie das öffentliche Teilen für diesen Bereich aktivieren möchten?",
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "Sind Sie sicher, dass Sie das öffentliche Teilen deaktivieren möchten? Alle bestehenden Freigabelinks in diesem Bereich werden gelöscht.",
"Public sharing is disabled": "Öffentliches Teilen ist deaktiviert",
"Public sharing has been disabled at the workspace level.": "Das öffentliche Teilen wurde auf der Arbeitsbereichsebene deaktiviert.",
"Public sharing has been disabled for this space.": "Das öffentliche Teilen wurde für diesen Bereich deaktiviert.",
"Copy page": "Seite kopieren",
"Copy page to a different space.": "Seite in einen anderen Bereich kopieren.",
"Page copied successfully": "Seite erfolgreich kopiert",
"Page duplicated successfully": "Seite erfolgreich dupliziert",
"Find": "Suchen",
"Not found": "Nicht gefunden",
"Previous Match (Shift+Enter)": "Vorheriger Treffer (Umschalt+Eingabe)",
"Next match (Enter)": "Nächster Treffer (Eingabe)",
"Match case (Alt+C)": "Groß-/Kleinschreibung beachten (Alt+C)",
"Replace": "Ersetzen",
"Close (Escape)": "Schließen (Escape)",
"Replace (Enter)": "Ersetzen (Eingabe)",
"Replace all (Ctrl+Alt+Enter)": "Alle ersetzen (Strg+Alt+Eingabe)",
"Replace all": "Alle ersetzen",
"View all": "Alle anzeigen",
"View all spaces": "Alle Bereiche anzeigen",
"Error": "Fehler",
"Failed to disable MFA": "MFA konnte nicht deaktiviert werden",
"Disable two-factor authentication": "Zwei-Faktor-Authentifizierung deaktivieren",
"Disabling two-factor authentication will make your account less secure. You'll only need your password to sign in.": "Die Deaktivierung der Zwei-Faktor-Authentifizierung macht Ihr Konto weniger sicher. Sie benötigen nur Ihr Passwort, um sich anzumelden.",
"Please enter your password to disable two-factor authentication:": "Bitte geben Sie Ihr Passwort ein, um die Zwei-Faktor-Authentifizierung zu deaktivieren:",
"Two-factor authentication has been enabled": "Die Zwei-Faktor-Authentifizierung wurde aktiviert",
"Two-factor authentication has been disabled": "Die Zwei-Faktor-Authentifizierung wurde deaktiviert",
"2-step verification": "Bestätigung in zwei Schritten",
"Protect your account with an additional verification layer when signing in.": "Schützen Sie Ihr Konto mit einer zusätzlichen Verifizierungsschicht beim Anmelden.",
"Two-factor authentication is active on your account.": "Die Zwei-Faktor-Authentifizierung ist auf Ihrem Konto aktiv.",
"Add 2FA method": "2FA-Methode hinzufügen",
"Backup codes": "Backup-Codes",
"Disable": "Deaktivieren",
"Invalid verification code": "Ungültiger Bestätigungscode",
"New backup codes have been generated": "Neue Backup-Codes wurden erstellt",
"Failed to regenerate backup codes": "Backup-Codes konnten nicht neu erstellt werden",
"About backup codes": "Über Backup-Codes",
"Backup codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "Sicherungscodes können verwendet werden, um auf Ihr Konto zuzugreifen, wenn Sie den Zugang zu Ihrer Authenticator-App verlieren. Jeder Code kann nur einmal verwendet werden.",
"You can regenerate new backup codes at any time. This will invalidate all existing codes.": "Sie können jederzeit neue Sicherungscodes generieren. Dies wird alle vorhandenen Codes ungültig machen.",
"Confirm password": "Passwort bestätigen",
"Generate new backup codes": "Neue Backup-Codes erstellen",
"Save your new backup codes": "Speichern Sie Ihre neuen Backup-Codes",
"Make sure to save these codes in a secure place. Your old backup codes are no longer valid.": "Speichern Sie diese Codes an einem sicheren Ort. Ihre alten Sicherungscodes sind nicht mehr gültig.",
"Your new backup codes": "Ihre neuen Backup-Codes",
"I've saved my backup codes": "Ich habe meine Backup-Codes gespeichert",
"Failed to setup MFA": "MFA konnte nicht eingerichtet werden",
"Setup & Verify": "Einrichten und bestätigen",
"Add to authenticator": "Zum Authenticator hinzufügen",
"1. Scan this QR code with your authenticator app": "1. Scannen Sie diesen QR-Code mit Ihrer Authenticator-App",
"Can't scan the code?": "Code kann nicht gescannt werden?",
"Enter this code manually in your authenticator app:": "Geben Sie diesen Code manuell in Ihrer Authenticator-App ein:",
"2. Enter the 6-digit code from your authenticator": "2. Geben Sie den 6-stelligen Code aus Ihrem Authenticator ein",
"Verify and enable": "Bestätigen und aktivieren",
"Failed to generate QR code. Please try again.": "Fehler beim Generieren des QR-Codes. Bitte versuchen Sie es erneut.",
"Backup": "Backup",
"Save codes": "Codes speichern",
"Save your backup codes": "Speichern Sie Ihre Backup-Codes",
"These codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "Diese Codes können verwendet werden, um auf Ihr Konto zuzugreifen, wenn Sie den Zugang zu Ihrer Authenticator-App verlieren. Jeder Code kann nur einmal verwendet werden.",
"Print": "Drucken",
"Two-factor authentication has been set up. Please log in again.": "Zwei-Faktor-Authentifizierung wurde eingerichtet. Bitte melden Sie sich erneut an.",
"Two-Factor authentication required": "Zwei-Faktor-Authentifizierung erforderlich",
"Your workspace requires two-factor authentication for all users": "Ihr Workspace erfordert Zwei-Faktor-Authentifizierung für alle Benutzer",
"To continue accessing your workspace, you must set up two-factor authentication. This adds an extra layer of security to your account.": "Um weiterhin auf Ihren Arbeitsbereich zuzugreifen, müssen Sie die Zwei-Faktor-Authentifizierung einrichten. Dies fügt Ihrem Konto eine zusätzliche Sicherheitsebene hinzu.",
"Set up two-factor authentication": "Zwei-Faktor-Authentifizierung einrichten",
"Cancel and logout": "Abbrechen und abmelden",
"Your workspace requires two-factor authentication. Please set it up to continue.": "Ihr Arbeitsbereich erfordert eine Zwei-Faktor-Authentifizierung. Bitte richten Sie diese ein, um fortzufahren.",
"This adds an extra layer of security to your account by requiring a verification code from your authenticator app.": "Dadurch wird Ihrem Konto eine zusätzliche Sicherheitsebene hinzugefügt, indem ein Bestätigungscode von Ihrer Authenticator-App verlangt wird.",
"Password is required": "Passwort ist erforderlich",
"Password must be at least 8 characters": "Das Passwort muss mindestens 8 Zeichen lang sein",
"Please enter a 6-digit code": "Bitte geben Sie einen 6-stelligen Code ein",
"Code must be exactly 6 digits": "Der Code muss genau 6 Ziffern haben",
"Enter the 6-digit code found in your authenticator app": "Geben Sie den 6-stelligen Code aus Ihrer Authenticator-App ein",
"Need help authenticating?": "Brauchen Sie Hilfe bei der Authentifizierung?",
"MFA QR Code": "MFA-QR-Code",
"Account created successfully. Please log in to set up two-factor authentication.": "Konto erfolgreich erstellt. Bitte melden Sie sich an, um die Zwei-Faktor-Authentifizierung einzurichten.",
"Password reset successful. Please log in with your new password and complete two-factor authentication.": "Passwort erfolgreich zurückgesetzt. Bitte melden Sie sich mit Ihrem neuen Passwort an und führen Sie die Zwei-Faktor-Authentifizierung durch.",
"Password reset successful. Please log in with your new password to set up two-factor authentication.": "Passwort erfolgreich zurückgesetzt. Bitte melden Sie sich mit Ihrem neuen Passwort an, um die Zwei-Faktor-Authentifizierung einzurichten.",
"Password reset was successful. Please log in with your new password.": "Passwort erfolgreich zurückgesetzt. Bitte melden Sie sich mit Ihrem neuen Passwort an.",
"Two-factor authentication": "Zwei-Faktor-Authentifizierung",
"Use authenticator app instead": "Stattdessen Authenticator-App verwenden",
"Verify backup code": "Backup-Code bestätigen",
"Use backup code": "Backup-Code verwenden",
"Enter one of your backup codes": "Geben Sie einen Ihrer Backup-Codes ein",
"Backup code": "Backup-Code",
"Enter one of your backup codes. Each backup code can only be used once.": "Geben Sie einen Ihrer Sicherungscodes ein. Jeder Sicherungscode kann nur einmal verwendet werden.",
"Verify": "Bestätigen",
"Trash": "Papierkorb",
"Pages in trash will be permanently deleted after {{count}} days.": "Seiten im Papierkorb werden nach {{count}} Tagen endgültig gelöscht.",
"Deleted": "Gelöscht",
"No pages in trash": "Keine Seiten im Papierkorb",
"Permanently delete page?": "Seite endgültig löschen?",
"Are you sure you want to permanently delete '{{title}}'? This action cannot be undone.": "Sind Sie sicher, dass Sie '{{title}}' endgültig löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
"Restore '{{title}}' and its sub-pages?": "'{{title}}' und seine Unterseiten wiederherstellen?",
"Move to trash": "In den Papierkorb verschieben",
"Move this page to trash?": "Diese Seite in den Papierkorb verschieben?",
"Restore page": "Seite wiederherstellen",
"Permanently delete": "Endgültig löschen",
"<b>{{name}}</b> moved this page to Trash {{time}}.": "<b>{{name}}</b> hat diese Seite {{time}} in den Papierkorb verschoben.",
"Page moved to trash": "Seite in den Papierkorb verschoben",
"Page restored successfully": "Seite erfolgreich wiederhergestellt",
"Deleted by": "Gelöscht von",
"Deleted at": "Gelöscht am",
"Preview": "Vorschau",
"Subpages": "Unterseiten",
"Failed to load subpages": "Unterseiten konnten nicht geladen werden",
"No subpages": "Keine Unterseiten",
"Subpages (Child pages)": "Unterseiten (untergeordnete Seiten)",
"List all subpages of the current page": "Alle Unterseiten der aktuellen Seite auflisten",
"Attachments": "Anhänge",
"All spaces": "Alle Bereiche",
"Unknown": "Unbekannt",
"Find a space": "Einen Bereich finden",
"Search in all your spaces": "In all Ihren Bereichen suchen",
"Type": "Typ",
"Enterprise": "Enterprise",
"Download attachment": "Anhang herunterladen",
"Allowed email domains": "Erlaubte E-Mail-Domains",
"Only users with email addresses from these domains can signup via SSO.": "Nur Benutzer mit E-Mail-Adressen aus diesen Domains können sich per SSO registrieren.",
"Enter valid domain names separated by comma or space": "Geben Sie gültige Domainnamen ein, getrennt durch Komma oder Leerzeichen",
"Enforce two-factor authentication": "Zwei-Faktor-Authentifizierung erzwingen",
"Once enforced, all members must enable two-factor authentication to access the workspace.": "Sobald es erzwungen wird, müssen alle Mitglieder die Zwei-Faktor-Authentifizierung aktivieren, um auf den Arbeitsbereich zugreifen zu können.",
"Toggle MFA enforcement": "MFA-Erzwingung umschalten",
"Display name": "Anzeigename",
"Allow signup": "Registrierung erlauben",
"Enabled": "Aktiviert",
"Advanced Settings": "Erweiterte Einstellungen",
"Enable TLS/SSL": "TLS/SSL aktivieren",
"Use secure connection to LDAP server": "Sichere Verbindung zum LDAP-Server verwenden",
"Group sync": "Gruppensynchronisierung",
"No SSO providers found.": "Keine SSO-Anbieter gefunden.",
"Delete SSO provider": "SSO-Anbieter löschen",
"Are you sure you want to delete this SSO provider?": "Sind Sie sicher, dass Sie diesen SSO-Anbieter löschen möchten?",
"Action": "Aktion",
"{{ssoProviderType}} configuration": "{{ssoProviderType}}-Konfiguration",
"Icon": "Symbol",
"Upload image": "Bild hochladen",
"Remove image": "Bild entfernen",
"Failed to remove image": "Fehler beim Entfernen des Bildes",
"Image exceeds 10MB limit.": "Bild überschreitet das Limit von 10 MB.",
"Image removed successfully": "Bild erfolgreich entfernt",
"API key": "API-Schlüssel",
"API keys": "API-Schlüssel",
"API management": "API-Verwaltung",
"Custom expiration date": "Benutzerdefiniertes Ablaufdatum",
"Enter a descriptive token name": "Geben Sie einen beschreibenden Token-Namen ein",
"Expiration": "Ablauf",
"Expired": "Abgelaufen",
"Expires": "Läuft ab",
"Last use": "Zuletzt verwendet",
"No API keys found": "Keine API-Schlüssel gefunden",
"No expiration": "Kein Ablauf",
"Revoked successfully": "Erfolgreich widerrufen",
"Select expiration date": "Ablaufdatum wählen",
"This action cannot be undone. Any applications using this API key will stop working.": "Diese Aktion kann nicht rückgängig gemacht werden. Alle Anwendungen, die diesen API-Schlüssel verwenden, werden nicht mehr funktionieren.",
"Update": "Aktualisieren",
"Update {{credential}}": "{{credential}} aktualisieren",
"Manage API keys for all users in the workspace": "Verwalten Sie API-Schlüssel für alle Benutzer im Arbeitsbereich",
"Restrict API key creation to admins": "API-Schlüsselerstellung auf Administratoren beschränken",
"Only admins and owners can create new API keys. Existing member keys will continue to work.": "Nur Administratoren und Eigentümer können neue API-Schlüssel erstellen. Bestehende Mitgliederschlüssel funktionieren weiterhin.",
"Toggle restrict API keys to admins": "Beschränkung der API-Schlüssel auf Administratoren umschalten",
"API key creation is restricted to admins by your workspace administrator.": "Die Erstellung von API-Schlüsseln ist durch Ihren Workspace-Administrator auf Administratoren beschränkt.",
"AI settings": "KI-Einstellungen",
"AI search": "KI-Suche",
"AI Answer": "KI-Antwort",
"Ask AI": "KI fragen",
"AI is thinking...": "Die KI überlegt...",
"Thinking": "Denkt nach",
"Ask a question...": "Fragen stellen...",
"AI Answers": "KI-Antworten",
"AI-powered search (AI Answers)": "KI-unterstützte Suche (KI-Antworten)",
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "Die KI-Suche verwendet Vektor-Einbettungen, um semantische Suchfunktionen in Ihrem Arbeitsbereich bereitzustellen.",
"Toggle AI search": "KI-Suche umschalten",
"Generative AI (Ask AI)": "Generative KI (KI fragen)",
"Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.": "Aktivieren Sie die KI-unterstützte Inhaltserstellung im Editor. Ermöglicht Benutzern das Erzeugen, Verbessern, Übersetzen und Transformieren von Text.",
"Toggle generative AI": "Generative KI umschalten",
"Upgrade your plan": "Upgrade Ihres Plans",
"Available with a paid license": "Verfügbar mit einer kostenpflichtigen Lizenz",
"Upgrade your license tier.": "Stufen Sie Ihre Lizenz hoch.",
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "KI ist nur in der Docmost Enterprise-Edition verfügbar. Kontaktieren Sie sales@docmost.com.",
"AI & MCP": "KI & MCP",
"AI": "KI",
"MCP": "MCP",
"Model Context Protocol (MCP)": "Model Context Protocol (MCP)",
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Aktivieren Sie den MCP-Server, damit KI-Assistenten und -Tools mit den Inhalten Ihres Arbeitsbereichs interagieren können.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP ist nur in der Docmost Enterprise-Edition verfügbar. Kontaktieren Sie sales@docmost.com.",
"MCP Server URL": "MCP-Server-URL",
"Use your API key for authentication. You can manage API keys in your account settings.": "Verwenden Sie Ihren API-Schlüssel zur Authentifizierung. API-Schlüssel können in Ihren Kontoeinstellungen verwaltet werden.",
"Supported tools": "Unterstützte Tools",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "In Ihrem Arbeitsbereich ist MCP aktiviert. Verwenden Sie Ihren API-Schlüssel, um KI-Assistenten anzubinden.",
"MCP server URL:": "MCP-Server-URL:",
"Learn more": "Mehr erfahren",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Verwalten Sie API-Schlüssel für alle Nutzer im Arbeitsbereich. Siehe die <anchor>API-Dokumentation</anchor> für Details zur Verwendung.",
"View the <anchor>API documentation</anchor> for usage details.": "Siehe die <anchor>API-Dokumentation</anchor> für Details zur Verwendung.",
"View the <anchor>MCP documentation</anchor>.": "Sehen Sie die <anchor>MCP-Dokumentation</anchor> ein.",
"Sources": "Quellen",
"AI Answers not available for attachments": "KI-Antworten sind für Anhänge nicht verfügbar",
"No answer available": "Keine Antwort verfügbar",
"Background color": "Hintergrundfarbe",
"Highlight color": "Hervorhebungsfarbe",
"Remove color": "Farbe entfernen",
"Notifications": "Benachrichtigungen",
"No notifications": "Keine Benachrichtigungen",
"No unread notifications": "Keine ungelesenen Benachrichtigungen",
"All notifications": "Alle Benachrichtigungen",
"Unread only": "Nur ungelesen",
"Mark all as read": "Alle als gelesen markieren",
"Mark as read": "Als gelesen markieren",
"More options": "Weitere Optionen",
"<bold>{{name}}</bold> mentioned you in a comment": "<bold>{{name}}</bold> hat Sie in einem Kommentar erwähnt",
"<bold>{{name}}</bold> commented on a page": "<bold>{{name}}</bold> hat einen Kommentar auf einer Seite hinterlassen",
"<bold>{{name}}</bold> resolved a comment": "<bold>{{name}}</bold> hat einen Kommentar gelöst",
"<bold>{{name}}</bold> mentioned you on a page": "<bold>{{name}}</bold> hat Sie auf einer Seite erwähnt",
"<bold>{{name}}</bold> gave you edit access to a page": "<bold>{{name}}</bold> hat Ihnen Bearbeitungszugriff auf eine Seite gegeben",
"<bold>{{name}}</bold> gave you view access to a page": "<bold>{{name}}</bold> hat Ihnen Ansichtszugriff auf eine Seite gegeben",
"<bold>{{name}}</bold> updated a page": "<bold>{{name}}</bold> hat eine Seite aktualisiert",
"Watch page": "Seite beobachten",
"Stop watching": "Nicht mehr beobachten",
"Watch space": "Bereich beobachten",
"Stop watching space": "Bereich nicht mehr beobachten",
"Email notifications": "E-Mail-Benachrichtigungen",
"Page updates": "Seitenaktualisierungen",
"Get notified when pages you watch are updated.": "Erhalten Sie eine Benachrichtigung, wenn Seiten, die Sie beobachten, aktualisiert werden.",
"Page mentions": "Seiten-Erwähnungen",
"Get notified when someone mentions you on a page.": "Erhalten Sie eine Benachrichtigung, wenn Sie jemand auf einer Seite erwähnt.",
"Comment mentions": "Kommentar-Erwähnungen",
"Get notified when someone mentions you in a comment.": "Erhalten Sie eine Benachrichtigung, wenn Sie jemand in einem Kommentar erwähnt.",
"New comments": "Neue Kommentare",
"Get notified about new comments on threads you participate in.": "Erhalten Sie eine Benachrichtigung über neue Kommentare in Threads, an denen Sie teilnehmen.",
"Resolved comments": "Erledigte Kommentare",
"Get notified when your comment is resolved.": "Erhalten Sie eine Benachrichtigung, wenn Ihr Kommentar erledigt wurde.",
"You are now watching this page": "Sie beobachten diese Seite jetzt",
"You are no longer watching this page": "Sie beobachten diese Seite nicht mehr",
"You are now watching this space": "Sie beobachten diesen Bereich jetzt",
"You are no longer watching this space": "Sie beobachten diesen Bereich nicht mehr",
"Direct": "Direkt",
"Updates": "Aktualisierungen",
"Today": "Heute",
"Yesterday": "Gestern",
"This week": "Diese Woche",
"Older": "Älter",
"Restricted page": "Eingeschränkte Seite",
"Restricted pages cannot be shared publicly.": "Eingeschränkte Seiten können nicht öffentlich geteilt werden.",
"Restricted by parent": "Eingeschränkt durch die übergeordnete Seite",
"Restricted": "Eingeschränkt",
"Open": "Offen",
"Inherits restrictions from ancestor page": "Erbt Einschränkungen von einer übergeordneten Seite",
"Only people listed below can access this page": "Nur die unten aufgeführten Personen können auf diese Seite zugreifen.",
"Everyone in this space can access": "Jeder in diesem Bereich kann darauf zugreifen.",
"No additional restrictions on this page": "Keine zusätzlichen Einschränkungen auf dieser Seite",
"Only specific people can access": "Nur bestimmte Personen können zugreifen",
"Use only inherited restrictions": "Nur geerbte Einschränkungen verwenden",
"Add restrictions on top of inherited": "Einschränkungen zusätzlich zu den geerbten hinzufügen",
"Inherited restriction": "Geerbte Einschränkung",
"Access limited by": "Zugriff beschränkt durch",
"Restrict access to control who can view and edit this page": "Beschränken Sie den Zugriff, um festzulegen, wer diese Seite ansehen und bearbeiten kann.",
"Add additional restrictions specific to this page": "Fügen Sie zusätzliche, für diese Seite spezifische Einschränkungen hinzu.",
"Access": "Zugriff",
"People with access": "Personen mit Zugriff",
"Remove all": "Alle entfernen",
"Remove access": "Zugriff entfernen",
"Remove all access": "Alle Zugriffsrechte entfernen",
"Are you sure you want to remove this member's access to the page?": "Sind Sie sicher, dass Sie den Zugriff dieses Mitglieds auf die Seite entfernen möchten?",
"Are you sure you want to remove all specific access? This will make the page open to everyone in the space.": "Sind Sie sicher, dass Sie alle spezifischen Zugriffsrechte entfernen möchten? Dadurch wird die Seite für alle in diesem Bereich zugänglich.",
"Trash retention": "Aufbewahrungsdauer des Papierkorbs",
"Pages in trash will be permanently deleted after this period.": "Seiten im Papierkorb werden nach Ablauf dieses Zeitraums endgültig gelöscht.",
"Trash retention updated": "Aufbewahrungsdauer des Papierkorbs aktualisiert",
"Failed to update trash retention": "Aktualisierung der Aufbewahrungsdauer des Papierkorbs fehlgeschlagen",
"Removed page restriction": "Seitenbeschränkung entfernt",
"Added page permission": "Seitenberechtigung hinzugefügt",
"Removed page permission": "Seitenberechtigung entfernt",
"day": "Tag",
"days": "Tage",
"week": "Woche",
"weeks": "Wochen",
"month": "Monat",
"months": "Monate",
"year": "Jahr",
"years": "Jahre",
"Period": "Zeitraum",
"Fixed date": "Festes Datum",
"Indefinitely": "Unbegrenzt",
"Days": "Tage",
"Weeks": "Wochen",
"Months": "Monate",
"Years": "Jahre",
"Pick a date": "Datum auswählen",
"Maximum is {{max}} {{unit}} for this unit": "Das Maximum für diese Einheit beträgt {{max}} {{unit}}",
"Never expires. Verifiers can re-verify at any time.": "Läuft nie ab. Prüfer können die Seite jederzeit erneut verifizieren.",
"Verified": "Verifiziert",
"Review needed": "Prüfung erforderlich",
"Verification expired": "Verifizierung abgelaufen",
"Draft": "Entwurf",
"In Approval": "In Genehmigung",
"In approval": "In Genehmigung",
"Approved": "Genehmigt",
"Obsolete": "Veraltet",
"Expiring": "Läuft bald ab",
"Set up verification": "Verifizierung einrichten",
"Verify page": "Seite verifizieren",
"Page verification": "Seitenverifizierung",
"Add verification": "Verifizierung hinzufügen",
"Edit verification": "Verifizierung bearbeiten",
"Search by title": "Nach Titel suchen",
"Choose how this page should stay accurate.": "Wählen Sie aus, wie diese Seite aktuell gehalten werden soll.",
"Recurring verification": "Wiederkehrende Verifizierung",
"Verifiers re-confirm this page on a schedule.": "Prüfer bestätigen diese Seite nach einem Zeitplan erneut.",
"Re-verify on a schedule (e.g every 30 days )": "Nach einem Zeitplan erneut verifizieren (z. B. alle 30 Tage)",
"Page stays editable at all times": "Die Seite bleibt jederzeit bearbeitbar",
"Best for runbooks, FAQs, living documentation": "Am besten für Runbooks, FAQs und lebende Dokumentation geeignet",
"Approval workflow": "Genehmigungsworkflow",
"Formal document lifecycle with named approvers.": "Formaler Dokumentenlebenszyklus mit benannten Genehmigern.",
"Draft → In approval → Approved → Obsolete": "Entwurf → In Genehmigung → Genehmigt → Veraltet",
"Locked once approved, with full history": "Nach der Genehmigung gesperrt, mit vollständiger Historie",
"Designed for ISO 9001, ISO 13485, and FDA": "Entwickelt für ISO 9001, ISO 13485 und FDA",
"Best for SOPs and controlled documents": "Am besten für SOPs und kontrollierte Dokumente geeignet",
"Back": "Zurück",
"Quality management": "Qualitätsmanagement",
"Recurring": "Wiederkehrend",
"Pages move through draft, approval, and approved stages.": "Seiten durchlaufen die Phasen Entwurf, Genehmigung und Genehmigt.",
"Verifiers": "Prüfer",
"Add verifier": "Prüfer hinzufügen",
"I've reviewed this page for accuracy": "Ich habe diese Seite auf Richtigkeit geprüft",
"Set up": "Einrichten",
"Remove verification": "Verifizierung entfernen",
"Are you sure you want to remove verification from this page?": "Möchten Sie die Verifizierung wirklich von dieser Seite entfernen?",
"Assigned verifiers must periodically re-verify this page.": "Zugewiesene Prüfer müssen diese Seite regelmäßig erneut verifizieren.",
"Last verified by {{name}} {{time}} (expired)": "Zuletzt von {{name}} {{time}} verifiziert (abgelaufen)",
"The fixed expiration date has passed.": "Das feste Ablaufdatum ist überschritten.",
"Verified by {{name}} {{time}}": "Verifiziert von {{name}} {{time}}",
"Expires {{date}}": "Läuft ab am {{date}}",
"Expired {{date}}": "Abgelaufen am {{date}}",
"Mark as obsolete": "Als veraltet markieren",
"Mark obsolete": "Als veraltet markieren",
"Returned by {{name}} {{time}}": "Zurückgegeben von {{name}} {{time}}",
"No approval has been requested yet.": "Es wurde noch keine Genehmigung angefordert.",
"Submitted by {{name}} {{time}}": "Eingereicht von {{name}} {{time}}",
"Someone": "Jemand",
"Approved by {{name}} {{time}}": "Genehmigt von {{name}} {{time}}",
"This document has been marked as obsolete.": "Dieses Dokument wurde als veraltet markiert.",
"Rejection comment": "Ablehnungskommentar",
"Reason for returning this document...": "Grund für die Rückgabe dieses Dokuments...",
"Confirm rejection": "Ablehnung bestätigen",
"Submit for approval": "Zur Genehmigung einreichen",
"Reject": "Ablehnen",
"Approve": "Genehmigen",
"Re-submit for approval": "Erneut zur Genehmigung einreichen",
"Verified until": "Verifiziert bis",
"QMS": "QMS",
"Verified pages": "Verifizierte Seiten",
"Search pages...": "Seiten suchen...",
"Filter by space": "Nach Bereich filtern",
"Filter by type": "Nach Typ filtern",
"<bold>{{name}}</bold> verified a page": "<bold>{{name}}</bold> hat eine Seite verifiziert",
"<bold>{{name}}</bold> submitted a page for your approval": "<bold>{{name}}</bold> hat eine Seite zu Ihrer Genehmigung eingereicht",
"<bold>{{name}}</bold> returned a page for revision": "<bold>{{name}}</bold> hat eine Seite zur Überarbeitung zurückgegeben",
"Page verification expires soon": "Die Seitenverifizierung läuft bald ab",
"Page verification has expired": "Die Seitenverifizierung ist abgelaufen",
"Verifying your email": "Ihre E-Mail wird bestätigt",
"Please wait...": "Bitte warten...",
"Verification failed. The link may have expired.": "Überprüfung fehlgeschlagen. Der Link ist möglicherweise abgelaufen.",
"Check your email": "Prüfen Sie Ihre E-Mails",
"We sent a verification link to {{email}}.": "Wir haben einen Bestätigungslink an {{email}} gesendet.",
"We sent a verification link to your email.": "Wir haben einen Bestätigungslink an Ihre E-Mail-Adresse gesendet.",
"Click the link to verify your email and access your workspace.": "Klicken Sie auf den Link, um Ihre E-Mail zu bestätigen und auf Ihren Arbeitsbereich zuzugreifen.",
"Resend verification email": "Bestätigungs-E-Mail erneut senden",
"Verification email sent. Please check your inbox.": "Bestätigungs-E-Mail gesendet. Bitte überprüfen Sie Ihr Postfach.",
"Failed to resend verification email. Please try again.": "Fehler beim erneuten Senden der Bestätigungs-E-Mail. Bitte versuchen Sie es erneut.",
"We've sent you an email with your associated workspaces.": "Wir haben Ihnen eine E-Mail mit Ihren zugehörigen Arbeitsbereichen gesendet.",
"Load more": "Mehr laden",
"Log out of all devices": "Auf allen Geräten abmelden",
"Log out of all sessions except this device": "Von allen Sitzungen außer diesem Gerät abmelden",
"This Device": "Dieses Gerät",
"Unknown device": "Unbekanntes Gerät",
"No active sessions": "Keine aktiven Sitzungen",
"Session revoked": "Sitzung widerrufen",
"All other sessions revoked": "Alle anderen Sitzungen widerrufen",
"Last used": "Zuletzt verwendet",
"Created": "Erstellt",
"Rename": "Umbenennen",
"Publish": "Veröffentlichen",
"Security": "Sicherheit",
"Enforce SSO": "SSO erzwingen",
"Once enforced, members will not be able to login with email and password.": "Sobald dies erzwungen wird, können sich Mitglieder nicht mehr mit E-Mail und Passwort anmelden.",
"AI-generated content may not be accurate.": "KI-generierte Inhalte sind möglicherweise nicht korrekt.",
"AI Chat": "KI-Chat",
"Analyze for insights": "Für Erkenntnisse analysieren",
"Ask anything...": "Fragen Sie irgendetwas...",
"Assistant said:": "Assistent sagte:",
"Chat history": "Chatverlauf",
"Chat name": "Chatname",
"Chat transcript": "Chatprotokoll",
"Close": "Schließen",
"Copy assistant response": "Antwort des Assistenten kopieren",
"Docmost AI": "Docmost KI",
"Failed to load chat. An error occurred.": "Chat konnte nicht geladen werden. Ein Fehler ist aufgetreten.",
"Failed to render this message.": "Diese Nachricht konnte nicht dargestellt werden.",
"How can I help you today?": "Wie kann ich Ihnen heute helfen?",
"New chat": "Neuer Chat",
"No chat history": "Kein Chatverlauf",
"No chats found": "Keine Chats gefunden",
"No conversations yet": "Noch keine Unterhaltungen",
"Open full page": "Ganze Seite öffnen",
"Scroll to bottom": "Nach unten scrollen",
"You said:": "Sie sagten:",
"Previous 7 days": "Letzte 7 Tage",
"Previous 30 days": "Letzte 30 Tage",
"Search chats...": "Chats durchsuchen...",
"Search chats": "Chats durchsuchen",
"Ask anything... Use @ to mention pages": "Frag etwas ... Verwende @, um Seiten zu erwähnen",
"Ask anything or search your workspace": "Fragen Sie etwas oder durchsuchen Sie Ihren Workspace",
"Welcome to {{name}}": "Willkommen bei {{name}}",
"Add files": "Dateien hinzufügen",
"Mention a page": "Eine Seite einfügen",
"Start a new chat to see it here.": "Starten Sie einen neuen Chat, damit er hier angezeigt wird.",
"Summarize this page": "Diese Seite zusammenfassen",
"Toggle AI Chat": "KI-Chat umschalten",
"Translate this page": "Diese Seite übersetzen",
"Try a different search term.": "Versuchen Sie einen anderen Suchbegriff.",
"Try again": "Erneut versuchen",
"Untitled chat": "Chat ohne Titel",
"What can I help you with?": "Womit kann ich Ihnen helfen?",
"Are you sure you want to revoke this {{credential}}": "Sind Sie sicher, dass Sie diese(n) {{credential}} widerrufen möchten?",
"Automatically provision users and groups from your identity provider via SCIM.": "Stellen Sie Benutzer und Gruppen automatisch über SCIM von Ihrem Identitätsanbieter bereit.",
"Configure your identity provider with this URL to provision users and groups.": "Konfigurieren Sie Ihren Identitätsanbieter mit dieser URL, um Benutzer und Gruppen bereitzustellen.",
"Create {{credential}}": "{{credential}} erstellen",
"{{credential}} created": "{{credential}} erstellt",
"{{credential}} created successfully": "{{credential}} erfolgreich erstellt",
"Created by": "Erstellt von",
"Custom": "Benutzerdefiniert",
"Enable SCIM": "SCIM aktivieren",
"Enter a descriptive name": "Geben Sie einen beschreibenden Namen ein",
"I've saved my {{credential}}": "Ich habe meine(n) {{credential}} gespeichert",
"Important": "Wichtig",
"Make sure to copy your {{credential}} now. You won't be able to see it again!": "Stellen Sie sicher, dass Sie Ihre(n) {{credential}} jetzt kopieren. Sie können sie/ihn später nicht erneut anzeigen!",
"Never": "Nie",
"Revoke {{credential}}": "{{credential}} widerrufen",
"SCIM endpoint URL": "SCIM-Endpunkt-URL",
"SCIM provisioning": "SCIM-Bereitstellung",
"SCIM takes precedence over SSO group sync while enabled.": "SCIM hat Vorrang vor der SSO-Gruppensynchronisierung, solange es aktiviert ist.",
"You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "Sie haben die maximale Anzahl von {{max}} SCIM-Token erreicht. Löschen Sie ein vorhandenes Token, um ein neues zu erstellen.",
"SCIM token": "SCIM-Token",
"SCIM tokens": "SCIM-Token",
"This action cannot be undone. Your identity provider will stop syncing immediately.": "Diese Aktion kann nicht rückgängig gemacht werden. Ihr Identitätsanbieter wird die Synchronisierung sofort beenden.",
"Toggle SCIM provisioning": "SCIM-Bereitstellung umschalten",
"Token": "Token",
"Page menu": "Seitenmenü",
"Expand": "Erweitern",
"Collapse": "Reduzieren",
"Comment menu": "Kommentarmenü",
"Group menu": "Gruppenmenü",
"Show hidden breadcrumbs": "Ausgeblendete Breadcrumbs anzeigen",
"Breadcrumbs": "Navigationspfade",
"Page actions": "Seitenaktionen",
"Pick emoji": "Emoji auswählen",
"Template menu": "Vorlagenmenü",
"Use": "Verwenden",
"Use template": "Vorlage verwenden",
"Preview template: {{title}}": "Vorlage anzeigen: {{title}}",
"Use a template": "Eine Vorlage verwenden",
"Search templates...": "Vorlagen suchen...",
"Search spaces...": "Bereiche suchen...",
"No templates found": "Keine Vorlagen gefunden",
"No spaces found": "Keine Bereiche gefunden",
"Browse all templates": "Alle Vorlagen durchsuchen",
"This space": "Dieser Bereich",
"All templates": "Alle Vorlagen",
"Global": "Global",
"New template": "Neue Vorlage",
"Edit template": "Vorlage bearbeiten",
"Are you sure you want to delete this template?": "Sind Sie sicher, dass Sie diese Vorlage löschen möchten?",
"Template scope updated": "Vorlagenbereich aktualisiert",
"Choose which space this template belongs to": "Wählen Sie den Bereich aus, zu dem diese Vorlage gehört",
"Scope": "Bereich",
"Select scope": "Bereich auswählen",
"Title": "Titel",
"Saving...": "Wird gespeichert...",
"Saved": "Gespeichert",
"Save failed. Retry": "Speichern fehlgeschlagen. Erneut versuchen",
"By {{name}}": "Von {{name}}",
"Updated {{time}}": "Aktualisiert {{time}}",
"Choose destination": "Ziel auswählen",
"Search pages and spaces...": "Seiten und Bereiche suchen...",
"No results found": "Keine Ergebnisse gefunden",
"You don't have permission to create pages here": "Sie haben hier keine Berechtigung, Seiten zu erstellen",
"Chat menu": "Chatmenü",
"API key menu": "API-Schlüssel-Menü",
"Jump to comment selection": "Zur Kommentarauswahl springen",
"Slash commands": "Slash-Befehle",
"Mention suggestions": "Erwähnungsvorschläge",
"Link suggestions": "Linkvorschläge",
"Diagram editor": "Diagrammeditor",
"Add comment": "Kommentar hinzufügen",
"Find and replace": "Suchen und ersetzen",
"Main navigation": "Hauptnavigation",
"Space navigation": "Bereichsnavigation",
"Settings navigation": "Einstellungsnavigation",
"AI navigation": "KI-Navigation",
"Breadcrumb": "Navigationspfad",
"Synced block": "Synchronisierter Block",
"Create a block that stays in sync across pages.": "Erstellt einen Block der über mehrere Seiten synchronisiert wird",
"Editing original": "Original bearbeiten",
"Copy synced block": "Synchronisierten Block kopieren",
"Unsync": "Synchronisierung aufheben",
"Delete synced block": "Synchronisierten Block löschen",
"Synced to {{count}} other page_one": "Mit {{count}} anderer Seite synchronisiert",
"Synced to {{count}} other page_other": "Mit {{count}} anderen Seiten synchronisiert",
"ORIGINAL": "ORIGINAL",
"THIS PAGE": "DIESE SEITE",
"No pages": "Keine Seiten",
"The original synced block no longer exists": "Der originale synchronisierte Block existiert nicht mehr",
"You don't have access to this synced block": "Sie haben keinen Zugriff auf diesen synchronisierten Block",
"Failed to load this synced block": "Dieser synchronisierte Block konnte nicht geladen werden",
"Fixed editor toolbar": "Fixierte Editor-Symbolleiste",
"Show a formatting toolbar above the editor with quick access to common actions.": "Anzeige einer Formatierungs-Symbolleiste über dem Editor für schnellen Zugriff auf Aktionen.",
"Toggle fixed editor toolbar": "Fixierte Editor-Symbolleiste ein/aus",
"Normal text": "Normaler Text",
"More inline formatting": "Weitere Formatierung",
"Subscript": "Tiefgestellt",
"Superscript": "Hochgestellt",
"Inline code": "Inline-Code",
"Insert media": "Medien einfügen",
"Mention": "Erwähnung",
"Emoji": "Emoji",
"Columns": "Spalten",
"More inserts": "Weiteren Inhalt einfügen",
"Embeds": "Einbettungen",
"Diagrams": "Diagramme",
"Advanced": "Erweitert",
"Utility": "Dienstprogramme",
"Decrease indent": "Einzug verkleinern",
"Increase indent": "Einzug vergrößern",
"Clear formatting": "Formatierung zurücksetzen",
"Code block": "Codeblock",
"Experimental": "Experimentell",
"Strikethrough": "Durchgestrichen",
"Undo": "Rückgängig",
"Redo": "Wiederholen",
"Backlinks": "Rückverweise",
"Last updated by": "Zuletzt aktualisiert von",
"Last updated": "Zuletzt aktualisiert",
"Stats": "Statistiken",
"Word count": "Wörter",
"Characters": "Zeichen",
"Incoming links": "Eingehende Links",
"Outgoing links": "Ausgehende Links",
"Incoming links ({{count}})": "Eingehende Links ({{count}})",
"Outgoing links ({{count}})": "Ausgehende Links ({{count}})",
"No pages link here yet.": "Aktuell verlinken keine Seiten hierher.",
"This page doesn't link to other pages yet.": "Diese Seite verlinkt noch nicht auf andere Seiten.",
"Verified until {{date}}": "Verifiziert bis zum {{date}}",
"Labels": "Beschriftungen",
"Add label": "Beschriftung hinzufügen",
"No labels yet": "Noch keine Beschriftungen",
"Already added": "Bereits hinzugefügt",
"Invalid label name": "Ungültiger Beschriftungsname",
"No matches": "Keine Treffer",
"Search or create…": "Suchen oder erstellen…",
"Remove label {{name}}": "Beschriftung {{name}} entfernen",
"Failed to add label": "Beschriftung konnte nicht hinzugefügt werden",
"Failed to remove label": "Beschriftung konnte nicht entfernt werden",
"No pages with this label": "Keine Seiten mit dieser Beschriftung",
"Pages tagged with this label will appear here.": "Hier werden Seiten angezeigt, die mit dieser Beschriftung versehen sind.",
"No pages match your search.": "Es konnten keine Seiten gefunden werden, die mit Ihrer Suche übereinstimmen.",
"Updated {{date}}": "Aktualisiert am {{date}}",
"Cell actions": "Zellaktionen",
"Column actions": "Spaltenaktionen",
"Row actions": "Zeilenaktionen",
"Filter": "Filter",
"Page title": "Seitentitel",
"Page content": "Seiteninhalt",
"Member actions": "Mitgliederaktionen",
"Toggle password visibility": "Passwortsichtbarkeit umschalten",
"Send comment": "Kommentar senden",
"Token actions": "Token-Aktionen",
"Template settings": "Vorlageneinstellungen",
"Edit diagram": "Diagramm bearbeiten",
"Edit embed": "Einbettung bearbeiten",
"Edit drawing": "Zeichnung bearbeiten",
"Delete equation": "Gleichung löschen",
"Invite actions": "Einladungsaktionen",
"Get started": "Erste Schritte",
"* indicates required fields": "* kennzeichnet Pflichtfelder",
"List of spaces in this workspace": "Liste der Bereiche in diesem Workspace",
"Active sessions": "Aktive Sitzungen",
"Add {{name}} to favorites": "{{name}} zu Favoriten hinzufügen",
"Remove {{name}} from favorites": "{{name}} aus Favoriten entfernen",
"Added to favorites": "Zu Favoriten hinzugefügt",
"Removed from favorites": "Aus Favoriten entfernt",
"Added {{name}} to favorites": "{{name}} zu Favoriten hinzugefügt",
"Removed {{name}} from favorites": "{{name}} aus Favoriten entfernt",
"Page menu for {{name}}": "Seitenmenü für {{name}}",
"Create subpage of {{name}}": "Unterseite von {{name}} erstellen"
} }
@@ -7,6 +7,7 @@
"Add members": "Add members", "Add members": "Add members",
"Add to groups": "Add to groups", "Add to groups": "Add to groups",
"Add space members": "Add space members", "Add space members": "Add space members",
"Add to favorites": "Add to favorites",
"Admin": "Admin", "Admin": "Admin",
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Are you sure you want to delete this group? Members will lose access to resources this group has access to.", "Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Are you sure you want to delete this group? Members will lose access to resources this group has access to.",
"Are you sure you want to delete this page?": "Are you sure you want to delete this page?", "Are you sure you want to delete this page?": "Are you sure you want to delete this page?",
@@ -29,6 +30,7 @@
"Choose your preferred interface language.": "Choose your preferred interface language.", "Choose your preferred interface language.": "Choose your preferred interface language.",
"Choose your preferred page width.": "Choose your preferred page width.", "Choose your preferred page width.": "Choose your preferred page width.",
"Confirm": "Confirm", "Confirm": "Confirm",
"Copy as Markdown": "Copy as Markdown",
"Copy link": "Copy link", "Copy link": "Copy link",
"Create": "Create", "Create": "Create",
"Create group": "Create group", "Create group": "Create group",
@@ -53,6 +55,7 @@
"e.g Space for product team": "e.g Space for product team", "e.g Space for product team": "e.g Space for product team",
"e.g Space for sales team to collaborate": "e.g Space for sales team to collaborate", "e.g Space for sales team to collaborate": "e.g Space for sales team to collaborate",
"Edit": "Edit", "Edit": "Edit",
"Read": "Read",
"Edit group": "Edit group", "Edit group": "Edit group",
"Email": "Email", "Email": "Email",
"Enter a strong password": "Enter a strong password", "Enter a strong password": "Enter a strong password",
@@ -68,10 +71,14 @@
"Export": "Export", "Export": "Export",
"Failed to create page": "Failed to create page", "Failed to create page": "Failed to create page",
"Failed to delete page": "Failed to delete page", "Failed to delete page": "Failed to delete page",
"Failed to restore page": "Failed to restore page",
"Failed to fetch recent pages": "Failed to fetch recent pages", "Failed to fetch recent pages": "Failed to fetch recent pages",
"Failed to import pages": "Failed to import pages", "Failed to import pages": "Failed to import pages",
"Failed to load page. An error occurred.": "Failed to load page. An error occurred.", "Failed to load page. An error occurred.": "Failed to load page. An error occurred.",
"Failed to update data": "Failed to update data", "Failed to update data": "Failed to update data",
"Favorite spaces": "Favorite spaces",
"Favorite spaces appear here": "Favorite spaces appear here",
"Favorites": "Favorites",
"Full access": "Full access", "Full access": "Full access",
"Full page width": "Full page width", "Full page width": "Full page width",
"Full width": "Full width", "Full width": "Full width",
@@ -90,6 +97,7 @@
"Invite by email": "Invite by email", "Invite by email": "Invite by email",
"Invite members": "Invite members", "Invite members": "Invite members",
"Invite new members": "Invite new members", "Invite new members": "Invite new members",
"Invite People": "Invite People",
"Invited members who are yet to accept their invitation will appear here.": "Invited members who are yet to accept their invitation will appear here.", "Invited members who are yet to accept their invitation will appear here.": "Invited members who are yet to accept their invitation will appear here.",
"Invited members will be granted access to spaces the groups can access": "Invited members will be granted access to spaces the groups can access", "Invited members will be granted access to spaces the groups can access": "Invited members will be granted access to spaces the groups can access",
"Join the workspace": "Join the workspace", "Join the workspace": "Join the workspace",
@@ -114,6 +122,7 @@
"No group found": "No group found", "No group found": "No group found",
"No page history saved yet.": "No page history saved yet.", "No page history saved yet.": "No page history saved yet.",
"No pages yet": "No pages yet", "No pages yet": "No pages yet",
"No shared pages": "No shared pages",
"No results found...": "No results found...", "No results found...": "No results found...",
"No user found": "No user found", "No user found": "No user found",
"Overview": "Overview", "Overview": "Overview",
@@ -121,11 +130,14 @@
"page": "page", "page": "page",
"Page deleted successfully": "Page deleted successfully", "Page deleted successfully": "Page deleted successfully",
"Page history": "Page history", "Page history": "Page history",
"Select version": "Select version",
"Highlight changes": "Highlight changes",
"Page import is in progress. Please do not close this tab.": "Page import is in progress. Please do not close this tab.", "Page import is in progress. Please do not close this tab.": "Page import is in progress. Please do not close this tab.",
"Pages": "Pages", "Pages": "Pages",
"pages": "pages", "pages": "pages",
"Password": "Password", "Password": "Password",
"Password changed successfully": "Password changed successfully", "Password changed successfully": "Password changed successfully",
"People": "People",
"Pending": "Pending", "Pending": "Pending",
"Please confirm your action": "Please confirm your action", "Please confirm your action": "Please confirm your action",
"Preferences": "Preferences", "Preferences": "Preferences",
@@ -133,6 +145,7 @@
"Profile": "Profile", "Profile": "Profile",
"Recently updated": "Recently updated", "Recently updated": "Recently updated",
"Remove": "Remove", "Remove": "Remove",
"Remove from favorites": "Remove from favorites",
"Remove group member": "Remove group member", "Remove group member": "Remove group member",
"Remove space member": "Remove space member", "Remove space member": "Remove space member",
"Restore": "Restore", "Restore": "Restore",
@@ -169,6 +182,7 @@
"Successfully imported": "Successfully imported", "Successfully imported": "Successfully imported",
"Successfully restored": "Successfully restored", "Successfully restored": "Successfully restored",
"System settings": "System settings", "System settings": "System settings",
"Templates": "Templates",
"Theme": "Theme", "Theme": "Theme",
"To change your email, you have to enter your password and new email.": "To change your email, you have to enter your password and new email.", "To change your email, you have to enter your password and new email.": "To change your email, you have to enter your password and new email.",
"Toggle full page width": "Toggle full page width", "Toggle full page width": "Toggle full page width",
@@ -203,9 +217,14 @@
"Reply...": "Reply...", "Reply...": "Reply...",
"Error loading comments.": "Error loading comments.", "Error loading comments.": "Error loading comments.",
"No comments yet.": "No comments yet.", "No comments yet.": "No comments yet.",
"No open comments.": "No open comments.",
"No resolved comments.": "No resolved comments.",
"Add a comment...": "Add a comment...",
"Edit comment": "Edit comment", "Edit comment": "Edit comment",
"Delete comment": "Delete comment", "Delete comment": "Delete comment",
"Are you sure you want to delete this comment?": "Are you sure you want to delete this comment?", "Are you sure you want to delete this comment?": "Are you sure you want to delete this comment?",
"Delete chat": "Delete chat",
"Are you sure you want to delete '{{title}}'? This action cannot be undone.": "Are you sure you want to delete '{{title}}'? This action cannot be undone.",
"Comment created successfully": "Comment created successfully", "Comment created successfully": "Comment created successfully",
"Error creating comment": "Error creating comment", "Error creating comment": "Error creating comment",
"Comment updated successfully": "Comment updated successfully", "Comment updated successfully": "Comment updated successfully",
@@ -213,7 +232,17 @@
"Comment deleted successfully": "Comment deleted successfully", "Comment deleted successfully": "Comment deleted successfully",
"Failed to delete comment": "Failed to delete comment", "Failed to delete comment": "Failed to delete comment",
"Comment resolved successfully": "Comment resolved successfully", "Comment resolved successfully": "Comment resolved successfully",
"Comment re-opened successfully": "Comment re-opened successfully",
"Comment unresolved successfully": "Comment unresolved successfully",
"Failed to resolve comment": "Failed to resolve comment", "Failed to resolve comment": "Failed to resolve comment",
"Resolve comment": "Resolve comment",
"Unresolve comment": "Unresolve comment",
"Resolve Comment Thread": "Resolve Comment Thread",
"Unresolve Comment Thread": "Unresolve Comment Thread",
"Are you sure you want to resolve this comment thread? This will mark it as completed.": "Are you sure you want to resolve this comment thread? This will mark it as completed.",
"Are you sure you want to unresolve this comment thread?": "Are you sure you want to unresolve this comment thread?",
"Resolved": "Resolved",
"No active comments.": "No active comments.",
"Revoke invitation": "Revoke invitation", "Revoke invitation": "Revoke invitation",
"Revoke": "Revoke", "Revoke": "Revoke",
"Don't": "Don't", "Don't": "Don't",
@@ -222,7 +251,9 @@
"Anyone with this link can join this workspace.": "Anyone with this link can join this workspace.", "Anyone with this link can join this workspace.": "Anyone with this link can join this workspace.",
"Invite link": "Invite link", "Invite link": "Invite link",
"Copy": "Copy", "Copy": "Copy",
"Copy to space": "Copy to space",
"Copied": "Copied", "Copied": "Copied",
"Duplicate": "Duplicate",
"Select a user": "Select a user", "Select a user": "Select a user",
"Select a group": "Select a group", "Select a group": "Select a group",
"Export all pages and attachments in this space.": "Export all pages and attachments in this space.", "Export all pages and attachments in this space.": "Export all pages and attachments in this space.",
@@ -239,12 +270,16 @@
"Export failed:": "Export failed:", "Export failed:": "Export failed:",
"export error": "export error", "export error": "export error",
"Export page": "Export page", "Export page": "Export page",
"Export successful": "Export successful",
"Export space": "Export space", "Export space": "Export space",
"Export {{type}}": "Export {{type}}", "Export {{type}}": "Export {{type}}",
"File exceeds the {{limit}} attachment limit": "File exceeds the {{limit}} attachment limit", "File exceeds the {{limit}} attachment limit": "File exceeds the {{limit}} attachment limit",
"Align left": "Align left", "Align left": "Align left",
"Align right": "Align right", "Align right": "Align right",
"Align center": "Align center", "Align center": "Align center",
"Alt text": "Alt text",
"Describe this for accessibility.": "Describe this for accessibility.",
"Add a description": "Add a description",
"Justify": "Justify", "Justify": "Justify",
"Merge cells": "Merge cells", "Merge cells": "Merge cells",
"Split cell": "Split cell", "Split cell": "Split cell",
@@ -255,7 +290,21 @@
"Add row above": "Add row above", "Add row above": "Add row above",
"Add row below": "Add row below", "Add row below": "Add row below",
"Delete table": "Delete table", "Delete table": "Delete table",
"Add column left": "Add column left",
"Add column right": "Add column right",
"Clear cell": "Clear cell",
"Clear cells": "Clear cells",
"Toggle header cell": "Toggle header cell",
"Toggle header column": "Toggle header column",
"Toggle header row": "Toggle header row",
"Move column left": "Move column left",
"Move column right": "Move column right",
"Move row down": "Move row down",
"Move row up": "Move row up",
"Sort A → Z": "Sort A → Z",
"Sort Z → A": "Sort Z → A",
"Info": "Info", "Info": "Info",
"Note": "Note",
"Success": "Success", "Success": "Success",
"Warning": "Warning", "Warning": "Warning",
"Danger": "Danger", "Danger": "Danger",
@@ -266,6 +315,11 @@
"Save & Exit": "Save & Exit", "Save & Exit": "Save & Exit",
"Double-click to edit Excalidraw diagram": "Double-click to edit Excalidraw diagram", "Double-click to edit Excalidraw diagram": "Double-click to edit Excalidraw diagram",
"Paste link": "Paste link", "Paste link": "Paste link",
"Paste link or search pages": "Paste link or search pages",
"Link to web page": "Link to web page",
"Recents": "Recents",
"Page or URL": "Page or URL",
"Link title": "Link title",
"Edit link": "Edit link", "Edit link": "Edit link",
"Remove link": "Remove link", "Remove link": "Remove link",
"Add link": "Add link", "Add link": "Add link",
@@ -311,9 +365,14 @@
"Create block quote.": "Create block quote.", "Create block quote.": "Create block quote.",
"Insert code snippet.": "Insert code snippet.", "Insert code snippet.": "Insert code snippet.",
"Insert horizontal rule divider": "Insert horizontal rule divider", "Insert horizontal rule divider": "Insert horizontal rule divider",
"Page break": "Page break",
"Insert a page break for printing.": "Insert a page break for printing.",
"Upload any image from your device.": "Upload any image from your device.", "Upload any image from your device.": "Upload any image from your device.",
"Upload any video from your device.": "Upload any video from your device.", "Upload any video from your device.": "Upload any video from your device.",
"Upload any audio from your device.": "Upload any audio from your device.",
"Upload any file from your device.": "Upload any file from your device.", "Upload any file from your device.": "Upload any file from your device.",
"Uploading {{name}}": "Uploading {{name}}",
"Uploading file": "Uploading file",
"Table": "Table", "Table": "Table",
"Insert a table.": "Insert a table.", "Insert a table.": "Insert a table.",
"Insert collapsible block.": "Insert collapsible block.", "Insert collapsible block.": "Insert collapsible block.",
@@ -321,6 +380,12 @@
"Divider": "Divider", "Divider": "Divider",
"Quote": "Quote", "Quote": "Quote",
"Image": "Image", "Image": "Image",
"Audio": "Audio",
"Embed PDF": "Embed PDF",
"Upload and embed a PDF file.": "Upload and embed a PDF file.",
"Embed as PDF": "Embed as PDF",
"Failed to load PDF": "Failed to load PDF",
"Convert to attachment": "Convert to attachment",
"File attachment": "File attachment", "File attachment": "File attachment",
"Toggle block": "Toggle block", "Toggle block": "Toggle block",
"Callout": "Callout", "Callout": "Callout",
@@ -335,9 +400,27 @@
"Insert current date": "Insert current date", "Insert current date": "Insert current date",
"Draw and sketch excalidraw diagrams": "Draw and sketch excalidraw diagrams", "Draw and sketch excalidraw diagrams": "Draw and sketch excalidraw diagrams",
"Multiple": "Multiple", "Multiple": "Multiple",
"Turn into": "Turn into",
"Text align": "Text align",
"This page may have been deleted, moved, or you may not have access.": "This page may have been deleted, moved, or you may not have access.",
"Go to homepage": "Go to homepage",
"Pages you create will show up here.": "Pages you create will show up here.",
"Heading {{level}}": "Heading {{level}}", "Heading {{level}}": "Heading {{level}}",
"Toggle title": "Toggle title", "Toggle title": "Toggle title",
"Write anything. Enter \"/\" for commands": "Write anything. Enter \"/\" for commands", "Write anything. Enter \"/\" for commands": "Write anything. Enter \"/\" for commands",
"Write...": "Write...",
"Column count": "Column count",
"{{count}} Columns": "{{count}} Columns",
"{{count}} command available_one": "1 command available",
"{{count}} command available_other": "{{count}} commands available",
"{{count}} result available_one": "1 result available",
"{{count}} result available_other": "{{count}} results available",
"Equal columns": "Equal columns",
"Left sidebar": "Left sidebar",
"Right sidebar": "Right sidebar",
"Wide center": "Wide center",
"Left wide": "Left wide",
"Right wide": "Right wide",
"Names do not match": "Names do not match", "Names do not match": "Names do not match",
"Today, {{time}}": "Today, {{time}}", "Today, {{time}}": "Today, {{time}}",
"Yesterday, {{time}}": "Yesterday, {{time}}", "Yesterday, {{time}}": "Yesterday, {{time}}",
@@ -354,13 +437,652 @@
"Character count: {{characterCount}}": "Character count: {{characterCount}}", "Character count: {{characterCount}}": "Character count: {{characterCount}}",
"New update": "New update", "New update": "New update",
"{{latestVersion}} is available": "{{latestVersion}} is available", "{{latestVersion}} is available": "{{latestVersion}} is available",
"Default page edit mode": "Default page edit mode",
"Choose your preferred page edit mode. Avoid accidental edits.": "Choose your preferred page edit mode. Avoid accidental edits.",
"Choose {{format}} file": "Choose {{format}} file",
"Reading": "Reading",
"Delete member": "Delete member", "Delete member": "Delete member",
"Member deleted successfully": "Member deleted successfully", "Member deleted successfully": "Member deleted successfully",
"Are you sure you want to delete this workspace member? This action is irreversible.": "Are you sure you want to delete this workspace member? This action is irreversible.", "Are you sure you want to delete this workspace member? This action is irreversible.": "Are you sure you want to delete this workspace member? This action is irreversible.",
"Deactivate member": "Deactivate member",
"Activate member": "Activate member",
"Are you sure you want to deactivate this workspace member? They will no longer be able to access this workspace.": "Are you sure you want to deactivate this workspace member? They will no longer be able to access this workspace.",
"Are you sure you want to activate this workspace member?": "Are you sure you want to activate this workspace member?",
"Deactivate": "Deactivate",
"Activate": "Activate",
"Deactivated": "Deactivated",
"Move": "Move", "Move": "Move",
"Move page": "Move page", "Move page": "Move page",
"Move page to a different space.": "Move page to a different space.", "Move page to a different space.": "Move page to a different space.",
"Real-time editor connection lost. Retrying...": "Real-time editor connection lost. Retrying...", "Real-time editor connection lost. Retrying...": "Real-time editor connection lost. Retrying...",
"Table of contents": "Table of contents", "Table of contents": "Table of contents",
"Add headings (H1, H2, H3) to generate a table of contents.": "Add headings (H1, H2, H3) to generate a table of contents." "Add headings (H1, H2, H3) to generate a table of contents.": "Add headings (H1, H2, H3) to generate a table of contents.",
"Share": "Share",
"Public sharing": "Public sharing",
"Shared by": "Shared by",
"Shared at": "Shared at",
"Inherits public sharing from": "Inherits public sharing from",
"Share to web": "Share to web",
"Shared to web": "Shared to web",
"Anyone with the link can view this page": "Anyone with the link can view this page",
"Make this page publicly accessible": "Make this page publicly accessible",
"Include sub-pages": "Include sub-pages",
"Make sub-pages public too": "Make sub-pages public too",
"Allow search engines to index page": "Allow search engines to index page",
"Open page": "Open page",
"Page": "Page",
"Delete public share link": "Delete public share link",
"Delete share": "Delete share",
"Are you sure you want to delete this shared link?": "Are you sure you want to delete this shared link?",
"Publicly shared pages from spaces you are a member of will appear here": "Publicly shared pages from spaces you are a member of will appear here",
"Share deleted successfully": "Share deleted successfully",
"Share not found": "Share not found",
"Failed to share page": "Failed to share page",
"Disable public sharing": "Disable public sharing",
"Prevent members from sharing pages publicly.": "Prevent members from sharing pages publicly.",
"Toggle public sharing": "Toggle public sharing",
"Toggle space public sharing": "Toggle space public sharing",
"Allow viewers to comment": "Allow viewers to comment",
"Allow viewers to add comments on pages in this space.": "Allow viewers to add comments on pages in this space.",
"Toggle viewer comments": "Toggle viewer comments",
"Public sharing is disabled at the workspace level": "Public sharing is disabled at the workspace level",
"Prevent pages in this space from being shared publicly.": "Prevent pages in this space from being shared publicly.",
"Page permissions": "Page permissions",
"Control who can view and edit individual pages. Available with an enterprise license.": "Control who can view and edit individual pages. Available with an enterprise license.",
"Enable public sharing": "Enable public sharing",
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "Are you sure you want to enable public sharing? Members will be able to share pages publicly.",
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.",
"Are you sure you want to enable public sharing for this space?": "Are you sure you want to enable public sharing for this space?",
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.",
"Public sharing is disabled": "Public sharing is disabled",
"Public sharing has been disabled at the workspace level.": "Public sharing has been disabled at the workspace level.",
"Public sharing has been disabled for this space.": "Public sharing has been disabled for this space.",
"Copy page": "Copy page",
"Copy page to a different space.": "Copy page to a different space.",
"Page copied successfully": "Page copied successfully",
"Page duplicated successfully": "Page duplicated successfully",
"Find": "Find",
"Not found": "Not found",
"Previous Match (Shift+Enter)": "Previous Match (Shift+Enter)",
"Next match (Enter)": "Next match (Enter)",
"Match case (Alt+C)": "Match case (Alt+C)",
"Replace": "Replace",
"Close (Escape)": "Close (Escape)",
"Replace (Enter)": "Replace (Enter)",
"Replace all (Ctrl+Alt+Enter)": "Replace all (Ctrl+Alt+Enter)",
"Replace all": "Replace all",
"View all": "View all",
"View all spaces": "View all spaces",
"Error": "Error",
"Failed to disable MFA": "Failed to disable MFA",
"Disable two-factor authentication": "Disable two-factor authentication",
"Disabling two-factor authentication will make your account less secure. You'll only need your password to sign in.": "Disabling two-factor authentication will make your account less secure. You'll only need your password to sign in.",
"Please enter your password to disable two-factor authentication:": "Please enter your password to disable two-factor authentication:",
"Two-factor authentication has been enabled": "Two-factor authentication has been enabled",
"Two-factor authentication has been disabled": "Two-factor authentication has been disabled",
"2-step verification": "2-step verification",
"Protect your account with an additional verification layer when signing in.": "Protect your account with an additional verification layer when signing in.",
"Two-factor authentication is active on your account.": "Two-factor authentication is active on your account.",
"Add 2FA method": "Add 2FA method",
"Backup codes": "Backup codes",
"Disable": "Disable",
"Invalid verification code": "Invalid verification code",
"New backup codes have been generated": "New backup codes have been generated",
"Failed to regenerate backup codes": "Failed to regenerate backup codes",
"About backup codes": "About backup codes",
"Backup codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "Backup codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.",
"You can regenerate new backup codes at any time. This will invalidate all existing codes.": "You can regenerate new backup codes at any time. This will invalidate all existing codes.",
"Confirm password": "Confirm password",
"Generate new backup codes": "Generate new backup codes",
"Save your new backup codes": "Save your new backup codes",
"Make sure to save these codes in a secure place. Your old backup codes are no longer valid.": "Make sure to save these codes in a secure place. Your old backup codes are no longer valid.",
"Your new backup codes": "Your new backup codes",
"I've saved my backup codes": "I've saved my backup codes",
"Failed to setup MFA": "Failed to setup MFA",
"Setup & Verify": "Setup & Verify",
"Add to authenticator": "Add to authenticator",
"1. Scan this QR code with your authenticator app": "1. Scan this QR code with your authenticator app",
"Can't scan the code?": "Can't scan the code?",
"Enter this code manually in your authenticator app:": "Enter this code manually in your authenticator app:",
"2. Enter the 6-digit code from your authenticator": "2. Enter the 6-digit code from your authenticator",
"Verify and enable": "Verify and enable",
"Failed to generate QR code. Please try again.": "Failed to generate QR code. Please try again.",
"Backup": "Backup",
"Save codes": "Save codes",
"Save your backup codes": "Save your backup codes",
"These codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "These codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.",
"Print": "Print",
"Two-factor authentication has been set up. Please log in again.": "Two-factor authentication has been set up. Please log in again.",
"Two-Factor authentication required": "Two-factor authentication required",
"Your workspace requires two-factor authentication for all users": "Your workspace requires two-factor authentication for all users",
"To continue accessing your workspace, you must set up two-factor authentication. This adds an extra layer of security to your account.": "To continue accessing your workspace, you must set up two-factor authentication. This adds an extra layer of security to your account.",
"Set up two-factor authentication": "Set up two-factor authentication",
"Cancel and logout": "Cancel and logout",
"Your workspace requires two-factor authentication. Please set it up to continue.": "Your workspace requires two-factor authentication. Please set it up to continue.",
"This adds an extra layer of security to your account by requiring a verification code from your authenticator app.": "This adds an extra layer of security to your account by requiring a verification code from your authenticator app.",
"Password is required": "Password is required",
"Password must be at least 8 characters": "Password must be at least 8 characters",
"Please enter a 6-digit code": "Please enter a 6-digit code",
"Code must be exactly 6 digits": "Code must be exactly 6 digits",
"Enter the 6-digit code found in your authenticator app": "Enter the 6-digit code found in your authenticator app",
"Need help authenticating?": "Need help authenticating?",
"MFA QR Code": "MFA QR Code",
"Account created successfully. Please log in to set up two-factor authentication.": "Account created successfully. Please log in to set up two-factor authentication.",
"Password reset successful. Please log in with your new password and complete two-factor authentication.": "Password reset successful. Please log in with your new password and complete two-factor authentication.",
"Password reset successful. Please log in with your new password to set up two-factor authentication.": "Password reset successful. Please log in with your new password to set up two-factor authentication.",
"Password reset was successful. Please log in with your new password.": "Password reset was successful. Please log in with your new password.",
"Two-factor authentication": "Two-factor authentication",
"Use authenticator app instead": "Use authenticator app instead",
"Verify backup code": "Verify backup code",
"Use backup code": "Use backup code",
"Enter one of your backup codes": "Enter one of your backup codes",
"Backup code": "Backup code",
"Enter one of your backup codes. Each backup code can only be used once.": "Enter one of your backup codes. Each backup code can only be used once.",
"Verify": "Verify",
"Trash": "Trash",
"Pages in trash will be permanently deleted after {{count}} days.": "Pages in trash will be permanently deleted after {{count}} days.",
"Deleted": "Deleted",
"No pages in trash": "No pages in trash",
"Permanently delete page?": "Permanently delete page?",
"Are you sure you want to permanently delete '{{title}}'? This action cannot be undone.": "Are you sure you want to permanently delete '{{title}}'? This action cannot be undone.",
"Restore '{{title}}' and its sub-pages?": "Restore '{{title}}' and its sub-pages?",
"Move to trash": "Move to trash",
"Move this page to trash?": "Move this page to trash?",
"Restore page": "Restore page",
"Permanently delete": "Permanently delete",
"<b>{{name}}</b> moved this page to Trash {{time}}.": "<b>{{name}}</b> moved this page to Trash {{time}}.",
"Page moved to trash": "Page moved to trash",
"Page restored successfully": "Page restored successfully",
"Deleted by": "Deleted by",
"Deleted at": "Deleted at",
"Preview": "Preview",
"Subpages": "Subpages",
"Failed to load subpages": "Failed to load subpages",
"No subpages": "No subpages",
"Subpages (Child pages)": "Subpages (Child pages)",
"List all subpages of the current page": "List all subpages of the current page",
"Attachments": "Attachments",
"All spaces": "All spaces",
"Unknown": "Unknown",
"Find a space": "Find a space",
"Search in all your spaces": "Search in all your spaces",
"Type": "Type",
"Enterprise": "Enterprise",
"Download attachment": "Download attachment",
"Allowed email domains": "Allowed email domains",
"Only users with email addresses from these domains can signup via SSO.": "Only users with email addresses from these domains can signup via SSO.",
"Enter valid domain names separated by comma or space": "Enter valid domain names separated by comma or space",
"Enforce two-factor authentication": "Enforce two-factor authentication",
"Once enforced, all members must enable two-factor authentication to access the workspace.": "Once enforced, all members must enable two-factor authentication to access the workspace.",
"Toggle MFA enforcement": "Toggle MFA enforcement",
"Display name": "Display name",
"Allow signup": "Allow signup",
"Enabled": "Enabled",
"Advanced Settings": "Advanced Settings",
"Enable TLS/SSL": "Enable TLS/SSL",
"Use secure connection to LDAP server": "Use secure connection to LDAP server",
"Group sync": "Group sync",
"No SSO providers found.": "No SSO providers found.",
"Delete SSO provider": "Delete SSO provider",
"Are you sure you want to delete this SSO provider?": "Are you sure you want to delete this SSO provider?",
"Action": "Action",
"{{ssoProviderType}} configuration": "{{ssoProviderType}} configuration",
"Icon": "Icon",
"Upload image": "Upload image",
"Remove image": "Remove image",
"Failed to remove image": "Failed to remove image",
"Image exceeds 10MB limit.": "Image exceeds 10MB limit.",
"Image removed successfully": "Image removed successfully",
"API key": "API key",
"API keys": "API keys",
"API management": "API management",
"Custom expiration date": "Custom expiration date",
"Enter a descriptive token name": "Enter a descriptive token name",
"Expiration": "Expiration",
"Expired": "Expired",
"Expires": "Expires",
"Last use": "Last Used",
"No API keys found": "No API keys found",
"No expiration": "No expiration",
"Revoked successfully": "Revoked successfully",
"Select expiration date": "Select expiration date",
"This action cannot be undone. Any applications using this API key will stop working.": "This action cannot be undone. Any applications using this API key will stop working.",
"Update": "Update",
"Update {{credential}}": "Update {{credential}}",
"Manage API keys for all users in the workspace": "Manage API keys for all users in the workspace",
"Restrict API key creation to admins": "Restrict API key creation to admins",
"Only admins and owners can create new API keys. Existing member keys will continue to work.": "Only admins and owners can create new API keys. Existing member keys will continue to work.",
"Toggle restrict API keys to admins": "Toggle restrict API keys to admins",
"API key creation is restricted to admins by your workspace administrator.": "API key creation is restricted to admins by your workspace administrator.",
"AI settings": "AI settings",
"AI search": "AI search",
"AI Answer": "AI Answer",
"Ask AI": "Ask AI",
"AI is thinking...": "AI is thinking...",
"Thinking": "Thinking",
"Ask a question...": "Ask a question...",
"AI Answers": "AI Answers",
"AI-powered search (AI Answers)": "AI-powered search (AI Answers)",
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.",
"Toggle AI search": "Toggle AI search",
"Generative AI (Ask AI)": "Generative AI (Ask AI)",
"Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.": "Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.",
"Toggle generative AI": "Toggle generative AI",
"Upgrade your plan": "Upgrade your plan",
"Available with a paid license": "Available with a paid license",
"Upgrade your license tier.": "Upgrade your license tier.",
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.",
"AI & MCP": "AI & MCP",
"AI": "AI",
"MCP": "MCP",
"Model Context Protocol (MCP)": "Model Context Protocol (MCP)",
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Enable the MCP server to allow AI assistants and tools to interact with your workspace content.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.",
"MCP Server URL": "MCP Server URL",
"Use your API key for authentication. You can manage API keys in your account settings.": "Use your API key for authentication. You can manage API keys in your account settings.",
"Supported tools": "Supported tools",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "Your workspace has MCP enabled. Use your API key to connect AI assistants.",
"MCP server URL:": "MCP server URL:",
"Learn more": "Learn more",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.",
"View the <anchor>API documentation</anchor> for usage details.": "View the <anchor>API documentation</anchor> for usage details.",
"View the <anchor>MCP documentation</anchor>.": "View the <anchor>MCP documentation</anchor>.",
"Sources": "Sources",
"AI Answers not available for attachments": "AI Answers not available for attachments",
"No answer available": "No answer available",
"Background color": "Background color",
"Highlight color": "Highlight color",
"Remove color": "Remove color",
"Notifications": "Notifications",
"No notifications": "No notifications",
"No unread notifications": "No unread notifications",
"All notifications": "All notifications",
"Unread only": "Unread only",
"Mark all as read": "Mark all as read",
"Mark as read": "Mark as read",
"More options": "More options",
"<bold>{{name}}</bold> mentioned you in a comment": "<bold>{{name}}</bold> mentioned you in a comment",
"<bold>{{name}}</bold> commented on a page": "<bold>{{name}}</bold> commented on a page",
"<bold>{{name}}</bold> resolved a comment": "<bold>{{name}}</bold> resolved a comment",
"<bold>{{name}}</bold> mentioned you on a page": "<bold>{{name}}</bold> mentioned you on a page",
"<bold>{{name}}</bold> gave you edit access to a page": "<bold>{{name}}</bold> gave you edit access to a page",
"<bold>{{name}}</bold> gave you view access to a page": "<bold>{{name}}</bold> gave you view access to a page",
"<bold>{{name}}</bold> updated a page": "<bold>{{name}}</bold> updated a page",
"Watch page": "Watch page",
"Stop watching": "Stop watching",
"Watch space": "Watch space",
"Stop watching space": "Stop watching space",
"Email notifications": "Email notifications",
"Page updates": "Page updates",
"Get notified when pages you watch are updated.": "Receive notifications when the pages you watch are updated.",
"Page mentions": "Page mentions",
"Get notified when someone mentions you on a page.": "Receive notifications when someone mentions you on a page.",
"Comment mentions": "Comment mentions",
"Get notified when someone mentions you in a comment.": "Receive notifications when someone mentions you in a comment.",
"New comments": "New comments",
"Get notified about new comments on threads you participate in.": "Receive notifications about new comments in threads you are participating in.",
"Resolved comments": "Resolved comments",
"Get notified when your comment is resolved.": "Receive a notification when your comment is resolved.",
"You are now watching this page": "Youre now watching this page",
"You are no longer watching this page": "Youre no longer watching this page",
"You are now watching this space": "Youre now watching this space",
"You are no longer watching this space": "Youre no longer watching this space",
"Direct": "Direct",
"Updates": "Updates",
"Today": "Today",
"Yesterday": "Yesterday",
"This week": "This week",
"Older": "Older",
"Restricted page": "Restricted page",
"Restricted pages cannot be shared publicly.": "Restricted pages cannot be shared publicly.",
"Restricted by parent": "Restricted by parent",
"Restricted": "Restricted",
"Open": "Open",
"Inherits restrictions from ancestor page": "Inherits restrictions from ancestor page",
"Only people listed below can access this page": "Only people listed below can access this page",
"Everyone in this space can access": "Everyone in this space can access",
"No additional restrictions on this page": "No additional restrictions on this page",
"Only specific people can access": "Only specific people can access",
"Use only inherited restrictions": "Use only inherited restrictions",
"Add restrictions on top of inherited": "Add restrictions on top of inherited",
"Inherited restriction": "Inherited restriction",
"Access limited by": "Access limited by",
"Restrict access to control who can view and edit this page": "Restrict access to control who can view and edit this page",
"Add additional restrictions specific to this page": "Add additional restrictions specific to this page",
"Access": "Access",
"People with access": "People with access",
"Remove all": "Remove all",
"Remove access": "Remove access",
"Remove all access": "Remove all access",
"Are you sure you want to remove this member's access to the page?": "Are you sure you want to remove this member's access to the page?",
"Are you sure you want to remove all specific access? This will make the page open to everyone in the space.": "Are you sure you want to remove all specific access? This will make the page open to everyone in the space.",
"Trash retention": "Trash retention",
"Pages in trash will be permanently deleted after this period.": "Pages in trash will be permanently deleted after this period.",
"Trash retention updated": "Trash retention updated",
"Failed to update trash retention": "Failed to update trash retention",
"Removed page restriction": "Removed page restriction",
"Added page permission": "Added page permission",
"Removed page permission": "Removed page permission",
"day": "day",
"days": "days",
"week": "week",
"weeks": "weeks",
"month": "month",
"months": "months",
"year": "year",
"years": "years",
"Period": "Period",
"Fixed date": "Fixed date",
"Indefinitely": "Indefinitely",
"Days": "Days",
"Weeks": "Weeks",
"Months": "Months",
"Years": "Years",
"Pick a date": "Pick a date",
"Maximum is {{max}} {{unit}} for this unit": "Maximum is {{max}} {{unit}} for this unit",
"Never expires. Verifiers can re-verify at any time.": "Never expires. Verifiers can re-verify at any time.",
"Verified": "Verified",
"Review needed": "Review needed",
"Verification expired": "Verification expired",
"Draft": "Draft",
"In Approval": "In Approval",
"In approval": "In approval",
"Approved": "Approved",
"Obsolete": "Obsolete",
"Expiring": "Expiring",
"Set up verification": "Set up verification",
"Verify page": "Verify page",
"Page verification": "Page verification",
"Add verification": "Add verification",
"Edit verification": "Edit verification",
"Search by title": "Search by title",
"Choose how this page should stay accurate.": "Choose how this page should stay accurate.",
"Recurring verification": "Recurring verification",
"Verifiers re-confirm this page on a schedule.": "Verifiers re-confirm this page on a schedule.",
"Re-verify on a schedule (e.g every 30 days )": "Re-verify on a schedule (e.g every 30 days )",
"Page stays editable at all times": "Page stays editable at all times",
"Best for runbooks, FAQs, living documentation": "Best for runbooks, FAQs, living documentation",
"Approval workflow": "Approval workflow",
"Formal document lifecycle with named approvers.": "Formal document lifecycle with named approvers.",
"Draft → In approval → Approved → Obsolete": "Draft → In approval → Approved → Obsolete",
"Locked once approved, with full history": "Locked once approved, with full history",
"Designed for ISO 9001, ISO 13485, and FDA": "Designed for ISO 9001, ISO 13485, and FDA",
"Best for SOPs and controlled documents": "Best for SOPs and controlled documents",
"Back": "Back",
"Quality management": "Quality management",
"Recurring": "Recurring",
"Pages move through draft, approval, and approved stages.": "Pages move through draft, approval, and approved stages.",
"Verifiers": "Verifiers",
"Add verifier": "Add verifier",
"I've reviewed this page for accuracy": "I've reviewed this page for accuracy",
"Set up": "Set up",
"Remove verification": "Remove verification",
"Are you sure you want to remove verification from this page?": "Are you sure you want to remove verification from this page?",
"Assigned verifiers must periodically re-verify this page.": "Assigned verifiers must periodically re-verify this page.",
"Last verified by {{name}} {{time}} (expired)": "Last verified by {{name}} {{time}} (expired)",
"The fixed expiration date has passed.": "The fixed expiration date has passed.",
"Verified by {{name}} {{time}}": "Verified by {{name}} {{time}}",
"Expires {{date}}": "Expires {{date}}",
"Expired {{date}}": "Expired {{date}}",
"Mark as obsolete": "Mark as obsolete",
"Mark obsolete": "Mark obsolete",
"Returned by {{name}} {{time}}": "Returned by {{name}} {{time}}",
"No approval has been requested yet.": "No approval has been requested yet.",
"Submitted by {{name}} {{time}}": "Submitted by {{name}} {{time}}",
"Someone": "Someone",
"Approved by {{name}} {{time}}": "Approved by {{name}} {{time}}",
"This document has been marked as obsolete.": "This document has been marked as obsolete.",
"Rejection comment": "Rejection comment",
"Reason for returning this document...": "Reason for returning this document...",
"Confirm rejection": "Confirm rejection",
"Submit for approval": "Submit for approval",
"Reject": "Reject",
"Approve": "Approve",
"Re-submit for approval": "Re-submit for approval",
"Verified until": "Verified until",
"QMS": "QMS",
"Verified pages": "Verified pages",
"Search pages...": "Search pages...",
"Filter by space": "Filter by space",
"Filter by type": "Filter by type",
"<bold>{{name}}</bold> verified a page": "<bold>{{name}}</bold> verified a page",
"<bold>{{name}}</bold> submitted a page for your approval": "<bold>{{name}}</bold> submitted a page for your approval",
"<bold>{{name}}</bold> returned a page for revision": "<bold>{{name}}</bold> returned a page for revision",
"Page verification expires soon": "Page verification expires soon",
"Page verification has expired": "Page verification has expired",
"Verifying your email": "Verifying your email",
"Please wait...": "Please wait...",
"Verification failed. The link may have expired.": "Verification failed. The link may have expired.",
"Check your email": "Check your email",
"We sent a verification link to {{email}}.": "We sent a verification link to {{email}}.",
"We sent a verification link to your email.": "We sent a verification link to your email.",
"Click the link to verify your email and access your workspace.": "Click the link to verify your email and access your workspace.",
"Resend verification email": "Resend verification email",
"Verification email sent. Please check your inbox.": "Verification email sent. Please check your inbox.",
"Failed to resend verification email. Please try again.": "Failed to resend verification email. Please try again.",
"We've sent you an email with your associated workspaces.": "We've sent you an email with your associated workspaces.",
"Load more": "Load more",
"Log out of all devices": "Log out of all devices",
"Log out of all sessions except this device": "Log out of all sessions except this device",
"This Device": "This Device",
"Unknown device": "Unknown device",
"No active sessions": "No active sessions",
"Session revoked": "Session revoked",
"All other sessions revoked": "All other sessions revoked",
"Last used": "Last used",
"Created": "Created",
"Rename": "Rename",
"Publish": "Publish",
"Security": "Security",
"Enforce SSO": "Enforce SSO",
"Once enforced, members will not be able to login with email and password.": "Once enforced, members will not be able to login with email and password.",
"AI-generated content may not be accurate.": "AI-generated content may not be accurate.",
"AI Chat": "AI Chat",
"Analyze for insights": "Analyze for insights",
"Ask anything...": "Ask anything...",
"Assistant said:": "Assistant said:",
"Chat history": "Chat history",
"Chat name": "Chat name",
"Chat transcript": "Chat transcript",
"Close": "Close",
"Copy assistant response": "Copy assistant response",
"Docmost AI": "Docmost AI",
"Failed to load chat. An error occurred.": "Failed to load chat. An error occurred.",
"Failed to render this message.": "Failed to render this message.",
"How can I help you today?": "How can I help you today?",
"New chat": "New chat",
"No chat history": "No chat history",
"No chats found": "No chats found",
"No conversations yet": "No conversations yet",
"Open full page": "Open full page",
"Scroll to bottom": "Scroll to bottom",
"You said:": "You said:",
"Previous 7 days": "Previous 7 days",
"Previous 30 days": "Previous 30 days",
"Search chats...": "Search chats...",
"Search chats": "Search chats",
"Ask anything... Use @ to mention pages": "Ask anything... Use @ to mention pages",
"Ask anything or search your workspace": "Ask anything or search your workspace",
"Welcome to {{name}}": "Welcome to {{name}}",
"Add files": "Add files",
"Mention a page": "Mention a page",
"Start a new chat to see it here.": "Start a new chat to see it here.",
"Summarize this page": "Summarize this page",
"Toggle AI Chat": "Toggle AI Chat",
"Translate this page": "Translate this page",
"Try a different search term.": "Try a different search term.",
"Try again": "Try again",
"Untitled chat": "Untitled chat",
"What can I help you with?": "What can I help you with?",
"Are you sure you want to revoke this {{credential}}": "Are you sure you want to revoke this {{credential}}",
"Automatically provision users and groups from your identity provider via SCIM.": "Automatically provision users and groups from your identity provider via SCIM.",
"Configure your identity provider with this URL to provision users and groups.": "Configure your identity provider with this URL to provision users and groups.",
"Create {{credential}}": "Create {{credential}}",
"{{credential}} created": "{{credential}} created",
"{{credential}} created successfully": "{{credential}} created successfully",
"Created by": "Created by",
"Custom": "Custom",
"Enable SCIM": "Enable SCIM",
"Enter a descriptive name": "Enter a descriptive name",
"I've saved my {{credential}}": "I've saved my {{credential}}",
"Important": "Important",
"Make sure to copy your {{credential}} now. You won't be able to see it again!": "Make sure to copy your {{credential}} now. You won't be able to see it again!",
"Never": "Never",
"Revoke {{credential}}": "Revoke {{credential}}",
"SCIM endpoint URL": "SCIM endpoint URL",
"SCIM provisioning": "SCIM provisioning",
"SCIM takes precedence over SSO group sync while enabled.": "SCIM takes precedence over SSO group sync while enabled.",
"You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.",
"SCIM token": "SCIM token",
"SCIM tokens": "SCIM tokens",
"This action cannot be undone. Your identity provider will stop syncing immediately.": "This action cannot be undone. Your identity provider will stop syncing immediately.",
"Toggle SCIM provisioning": "Toggle SCIM provisioning",
"Token": "Token",
"Page menu": "Page menu",
"Expand": "Expand",
"Collapse": "Collapse",
"Comment menu": "Comment menu",
"Group menu": "Group menu",
"Show hidden breadcrumbs": "Show hidden breadcrumbs",
"Breadcrumbs": "Breadcrumbs",
"Page actions": "Page actions",
"Pick emoji": "Pick emoji",
"Template menu": "Template menu",
"Use": "Use",
"Use template": "Use template",
"Preview template: {{title}}": "Preview template: {{title}}",
"Use a template": "Use a template",
"Search templates...": "Search templates...",
"Search spaces...": "Search spaces...",
"No templates found": "No templates found",
"No spaces found": "No spaces found",
"Browse all templates": "Browse all templates",
"This space": "This space",
"All templates": "All templates",
"Global": "Global",
"New template": "New template",
"Edit template": "Edit template",
"Are you sure you want to delete this template?": "Are you sure you want to delete this template?",
"Template scope updated": "Template scope updated",
"Choose which space this template belongs to": "Choose which space this template belongs to",
"Scope": "Scope",
"Select scope": "Select scope",
"Title": "Title",
"Saving...": "Saving...",
"Saved": "Saved",
"Save failed. Retry": "Save failed. Retry",
"By {{name}}": "By {{name}}",
"Updated {{time}}": "Updated {{time}}",
"Choose destination": "Choose destination",
"Search pages and spaces...": "Search pages and spaces...",
"No results found": "No results found",
"You don't have permission to create pages here": "You don't have permission to create pages here",
"Chat menu": "Chat menu",
"API key menu": "API key menu",
"Jump to comment selection": "Jump to comment selection",
"Slash commands": "Slash commands",
"Mention suggestions": "Mention suggestions",
"Link suggestions": "Link suggestions",
"Diagram editor": "Diagram editor",
"Add comment": "Add comment",
"Find and replace": "Find and replace",
"Main navigation": "Main navigation",
"Space navigation": "Space navigation",
"Settings navigation": "Settings navigation",
"AI navigation": "AI navigation",
"Breadcrumb": "Breadcrumb",
"Synced block": "Synced block",
"Create a block that stays in sync across pages.": "Create a block that stays in sync across pages.",
"Editing original": "Editing original",
"Copy synced block": "Copy synced block",
"Unsync": "Unsync",
"Delete synced block": "Delete synced block",
"Synced to {{count}} other page_one": "Synced to {{count}} other page",
"Synced to {{count}} other page_other": "Synced to {{count}} other pages",
"ORIGINAL": "ORIGINAL",
"THIS PAGE": "THIS PAGE",
"No pages": "No pages",
"The original synced block no longer exists": "The original synced block no longer exists",
"You don't have access to this synced block": "You don't have access to this synced block",
"Failed to load this synced block": "Failed to load this synced block",
"Fixed editor toolbar": "Fixed editor toolbar",
"Show a formatting toolbar above the editor with quick access to common actions.": "Show a formatting toolbar above the editor with quick access to common actions.",
"Toggle fixed editor toolbar": "Toggle fixed editor toolbar",
"Normal text": "Normal text",
"More inline formatting": "More inline formatting",
"Subscript": "Subscript",
"Superscript": "Superscript",
"Inline code": "Inline code",
"Insert media": "Insert media",
"Mention": "Mention",
"Emoji": "Emoji",
"Columns": "Columns",
"More inserts": "More inserts",
"Embeds": "Embeds",
"Diagrams": "Diagrams",
"Advanced": "Advanced",
"Utility": "Utility",
"Decrease indent": "Decrease indent",
"Increase indent": "Increase indent",
"Clear formatting": "Clear formatting",
"Code block": "Code block",
"Experimental": "Experimental",
"Strikethrough": "Strikethrough",
"Undo": "Undo",
"Redo": "Redo",
"Backlinks": "Backlinks",
"Last updated by": "Last updated by",
"Last updated": "Last updated",
"Stats": "Stats",
"Word count": "Word count",
"Characters": "Characters",
"Incoming links": "Incoming links",
"Outgoing links": "Outgoing links",
"Incoming links ({{count}})": "Incoming links ({{count}})",
"Outgoing links ({{count}})": "Outgoing links ({{count}})",
"No pages link here yet.": "No pages link here yet.",
"This page doesn't link to other pages yet.": "This page doesn't link to other pages yet.",
"Verified until {{date}}": "Verified until {{date}}",
"Labels": "Labels",
"Add label": "Add label",
"No labels yet": "No labels yet",
"Already added": "Already added",
"Invalid label name": "Invalid label name",
"No matches": "No matches",
"Search or create…": "Search or create…",
"Remove label {{name}}": "Remove label {{name}}",
"Failed to add label": "Failed to add label",
"Failed to remove label": "Failed to remove label",
"No pages with this label": "No pages with this label",
"Pages tagged with this label will appear here.": "Pages tagged with this label will appear here.",
"No pages match your search.": "No pages match your search.",
"Updated {{date}}": "Updated {{date}}",
"Cell actions": "Cell actions",
"Column actions": "Column actions",
"Row actions": "Row actions",
"Filter": "Filter",
"Page title": "Page title",
"Page content": "Page content",
"Member actions": "Member actions",
"Toggle password visibility": "Toggle password visibility",
"Send comment": "Send comment",
"Token actions": "Token actions",
"Template settings": "Template settings",
"Edit diagram": "Edit diagram",
"Edit embed": "Edit embed",
"Edit drawing": "Edit drawing",
"Delete equation": "Delete equation",
"Invite actions": "Invite actions",
"Get started": "Get started",
"* indicates required fields": "* indicates required fields",
"List of spaces in this workspace": "List of spaces in this workspace",
"Active sessions": "Active sessions",
"Add {{name}} to favorites": "Add {{name}} to favorites",
"Remove {{name}} from favorites": "Remove {{name}} from favorites",
"Added to favorites": "Added to favorites",
"Removed from favorites": "Removed from favorites",
"Added {{name}} to favorites": "Added {{name}} to favorites",
"Removed {{name}} from favorites": "Removed {{name}} from favorites",
"Page menu for {{name}}": "Page menu for {{name}}",
"Create subpage of {{name}}": "Create subpage of {{name}}"
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+754 -32
View File
@@ -7,6 +7,7 @@
"Add members": "Aggiungi membri", "Add members": "Aggiungi membri",
"Add to groups": "Aggiungi ai gruppi", "Add to groups": "Aggiungi ai gruppi",
"Add space members": "Aggiungi membri allo spazio", "Add space members": "Aggiungi membri allo spazio",
"Add to favorites": "Aggiungi ai preferiti",
"Admin": "Amministratore", "Admin": "Amministratore",
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Sei sicuro di voler eliminare questo gruppo? I membri perderanno l'accesso alle risorse accessibili da questo gruppo.", "Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Sei sicuro di voler eliminare questo gruppo? I membri perderanno l'accesso alle risorse accessibili da questo gruppo.",
"Are you sure you want to delete this page?": "Sei sicuro di voler eliminare questa pagina?", "Are you sure you want to delete this page?": "Sei sicuro di voler eliminare questa pagina?",
@@ -29,6 +30,7 @@
"Choose your preferred interface language.": "Scegli la lingua da utilizzare per l'interfaccia.", "Choose your preferred interface language.": "Scegli la lingua da utilizzare per l'interfaccia.",
"Choose your preferred page width.": "Scegli la larghezza della pagina che preferisci.", "Choose your preferred page width.": "Scegli la larghezza della pagina che preferisci.",
"Confirm": "Conferma", "Confirm": "Conferma",
"Copy as Markdown": "Copia come Markdown",
"Copy link": "Copia link", "Copy link": "Copia link",
"Create": "Crea", "Create": "Crea",
"Create group": "Crea gruppo", "Create group": "Crea gruppo",
@@ -46,18 +48,19 @@
"e.g ACME": "es. ACME", "e.g ACME": "es. ACME",
"e.g ACME Inc": "es. ACME Inc", "e.g ACME Inc": "es. ACME Inc",
"e.g Developers": "es. Sviluppatori", "e.g Developers": "es. Sviluppatori",
"e.g Group for developers": "es. Gruppo per gli sviluppatori", "e.g Group for developers": "es. Gruppo per sviluppatori",
"e.g product": "es. prodotto", "e.g product": "es. prodotto",
"e.g Product Team": "es. Team di Prodotto", "e.g Product Team": "es. Team di prodotto",
"e.g Sales": "es. Vendite", "e.g Sales": "es. Vendite",
"e.g Space for product team": "es. Spazio per il team di prodotto", "e.g Space for product team": "es. Spazio per il team di prodotto",
"e.g Space for sales team to collaborate": "es. Spazio per la collaborazione del team di vendita", "e.g Space for sales team to collaborate": "es. Spazio per la collaborazione del team vendite",
"Edit": "Modifica", "Edit": "Modifica",
"Read": "Leggi",
"Edit group": "Modifica gruppo", "Edit group": "Modifica gruppo",
"Email": "Email", "Email": "Email",
"Enter a strong password": "Inserisci una password sicura", "Enter a strong password": "Inserisci una password sicura",
"Enter valid email addresses separated by comma or space max_50": "Inserisci degli indirizzi email validi separati da virgola o spazio [max: 50]", "Enter valid email addresses separated by comma or space max_50": "Inserisci degli indirizzi email validi separati da virgola o spazio [max: 50]",
"enter valid emails addresses": "inserisci degli indirizzi email validi", "enter valid emails addresses": "inserisci indirizzi email validi",
"Enter your current password": "Inserisci la tua password attuale", "Enter your current password": "Inserisci la tua password attuale",
"enter your full name": "inserisci il tuo nome completo", "enter your full name": "inserisci il tuo nome completo",
"Enter your new password": "Inserisci la tua nuova password", "Enter your new password": "Inserisci la tua nuova password",
@@ -68,10 +71,14 @@
"Export": "Esporta", "Export": "Esporta",
"Failed to create page": "Impossibile creare la pagina", "Failed to create page": "Impossibile creare la pagina",
"Failed to delete page": "Impossibile eliminare la pagina", "Failed to delete page": "Impossibile eliminare la pagina",
"Failed to restore page": "Impossibile ripristinare la pagina",
"Failed to fetch recent pages": "Impossibile recuperare le pagine recenti", "Failed to fetch recent pages": "Impossibile recuperare le pagine recenti",
"Failed to import pages": "Impossibile importare le pagine", "Failed to import pages": "Impossibile importare le pagine",
"Failed to load page. An error occurred.": "Il caricamento della pagina è fallito. Si è verificato un errore.", "Failed to load page. An error occurred.": "Il caricamento della pagina è fallito. Si è verificato un errore.",
"Failed to update data": "Impossibile aggiornare i dati", "Failed to update data": "Impossibile aggiornare i dati",
"Favorite spaces": "Spazi preferiti",
"Favorite spaces appear here": "Gli spazi preferiti appariranno qui",
"Favorites": "Preferiti",
"Full access": "Accesso completo", "Full access": "Accesso completo",
"Full page width": "Pagina a larghezza intera", "Full page width": "Pagina a larghezza intera",
"Full width": "Larghezza intera", "Full width": "Larghezza intera",
@@ -90,6 +97,7 @@
"Invite by email": "Invita tramite email", "Invite by email": "Invita tramite email",
"Invite members": "Invita membri", "Invite members": "Invita membri",
"Invite new members": "Invita nuovi membri", "Invite new members": "Invita nuovi membri",
"Invite People": "Invita persone",
"Invited members who are yet to accept their invitation will appear here.": "I membri invitati che non hanno ancora accettato il loro invito appariranno qui.", "Invited members who are yet to accept their invitation will appear here.": "I membri invitati che non hanno ancora accettato il loro invito appariranno qui.",
"Invited members will be granted access to spaces the groups can access": "I membri invitati avranno accesso agli spazi a cui i gruppi possono accedere", "Invited members will be granted access to spaces the groups can access": "I membri invitati avranno accesso agli spazi a cui i gruppi possono accedere",
"Join the workspace": "Unisciti all'area di lavoro", "Join the workspace": "Unisciti all'area di lavoro",
@@ -114,6 +122,7 @@
"No group found": "Nessun gruppo trovato", "No group found": "Nessun gruppo trovato",
"No page history saved yet.": "La pagina non ha una cronologia per ora.", "No page history saved yet.": "La pagina non ha una cronologia per ora.",
"No pages yet": "Nessuna pagina per ora", "No pages yet": "Nessuna pagina per ora",
"No shared pages": "Nessuna pagina condivisa.",
"No results found...": "Nessun risultato trovato...", "No results found...": "Nessun risultato trovato...",
"No user found": "Nessun utente trovato", "No user found": "Nessun utente trovato",
"Overview": "Panoramica", "Overview": "Panoramica",
@@ -121,11 +130,14 @@
"page": "pagina", "page": "pagina",
"Page deleted successfully": "Pagina eliminata con successo", "Page deleted successfully": "Pagina eliminata con successo",
"Page history": "Cronologia della pagina", "Page history": "Cronologia della pagina",
"Select version": "Seleziona versione",
"Highlight changes": "Evidenzia modifiche",
"Page import is in progress. Please do not close this tab.": "L'importazione della pagina è in corso. Si prega di non chiudere questa scheda.", "Page import is in progress. Please do not close this tab.": "L'importazione della pagina è in corso. Si prega di non chiudere questa scheda.",
"Pages": "Pagine", "Pages": "Pagine",
"pages": "pagine", "pages": "pagine",
"Password": "Password", "Password": "Password",
"Password changed successfully": "Password cambiata con successo", "Password changed successfully": "Password cambiata con successo",
"People": "Persone",
"Pending": "In sospeso", "Pending": "In sospeso",
"Please confirm your action": "Si prega di confermare la propria azione", "Please confirm your action": "Si prega di confermare la propria azione",
"Preferences": "Preferenze", "Preferences": "Preferenze",
@@ -133,6 +145,7 @@
"Profile": "Profilo", "Profile": "Profilo",
"Recently updated": "Aggiornato di recente", "Recently updated": "Aggiornato di recente",
"Remove": "Rimuovi", "Remove": "Rimuovi",
"Remove from favorites": "Rimuovi dai preferiti",
"Remove group member": "Rimuovi membro dal gruppo", "Remove group member": "Rimuovi membro dal gruppo",
"Remove space member": "Rimuovi membro dallo spazio", "Remove space member": "Rimuovi membro dallo spazio",
"Restore": "Ripristina", "Restore": "Ripristina",
@@ -143,55 +156,56 @@
"Search for users": "Cerca un utente", "Search for users": "Cerca un utente",
"Search for users and groups": "Cerca un utente o un gruppo", "Search for users and groups": "Cerca un utente o un gruppo",
"Search...": "Cerca...", "Search...": "Cerca...",
"Select language": "Seleziona una lingua", "Select language": "Seleziona lingua",
"Select role": "Seleziona un ruolo", "Select role": "Seleziona ruolo",
"Select role to assign to all invited members": "Seleziona il ruolo da assegnare a tutti i membri invitati", "Select role to assign to all invited members": "Seleziona il ruolo da assegnare a tutti i membri invitati",
"Select theme": "Seleziona un tema", "Select theme": "Seleziona tema",
"Send invitation": "Invia invito", "Send invitation": "Invia invito",
"Invitation sent": "Invito inviato", "Invitation sent": "Invito inviato",
"Settings": "Impostazioni", "Settings": "Impostazioni",
"Setup workspace": "Configura l'area di lavoro", "Setup workspace": "Configura workspace",
"Sign In": "Accedi", "Sign In": "Accedi",
"Sign Up": "Registrati", "Sign Up": "Registrati",
"Slug": "Slug", "Slug": "Slug",
"Space": "Spazio", "Space": "Spazio",
"Space description": "Descrizione dello spazio", "Space description": "Descrizione dello spazio",
"Space menu": "Menu spazio", "Space menu": "Menu dello spazio",
"Space name": "Nome dello spazio", "Space name": "Nome dello spazio",
"Space settings": "Impostazioni dello spazio", "Space settings": "Impostazioni dello spazio",
"Space slug": "Slug dello spazio", "Space slug": "Slug dello spazio",
"Spaces": "Spazi", "Spaces": "Spazi",
"Spaces you belong to": "Spazi a cui appartieni", "Spaces you belong to": "Spazi di cui fai parte",
"No space found": "Nessuno spazio trovato", "No space found": "Nessuno spazio trovato",
"Search for spaces": "Cerca uno spazio", "Search for spaces": "Cerca spazi",
"Start typing to search...": "Inizia a digitare per cercare...", "Start typing to search...": "Inizia a digitare per cercare...",
"Status": "Stato", "Status": "Stato",
"Successfully imported": "Importato con successo", "Successfully imported": "Importato con successo",
"Successfully restored": "Ripristinato con successo", "Successfully restored": "Ripristinato con successo",
"System settings": "Impostazioni di sistema", "System settings": "Impostazioni di sistema",
"Templates": "Modelli",
"Theme": "Tema", "Theme": "Tema",
"To change your email, you have to enter your password and new email.": "Per cambiare la tua email, devi inserire la tua password e la nuova email.", "To change your email, you have to enter your password and new email.": "Per cambiare la tua email, devi inserire la tua password e la nuova email.",
"Toggle full page width": "Attiva/disattiva pagina a larghezza intera", "Toggle full page width": "Attiva/disattiva larghezza completa della pagina",
"Unable to import pages. Please try again.": "Impossibile importare le pagine. Riprova.", "Unable to import pages. Please try again.": "Impossibile importare le pagine. Riprova.",
"untitled": "senza titolo", "untitled": "senza titolo",
"Untitled": "Senza titolo", "Untitled": "Senza titolo",
"Updated successfully": "Aggiornato con successo", "Updated successfully": "Aggiornato con successo",
"User": "Utente", "User": "Utente",
"Workspace": "Area di lavoro", "Workspace": "Workspace",
"Workspace Name": "Nome dell'area di lavoro", "Workspace Name": "Nome del workspace",
"Workspace settings": "Impostazioni dell'area di lavoro", "Workspace settings": "Impostazioni del workspace",
"You can change your password here.": "Qui puoi cambiare la tua password.", "You can change your password here.": "Qui puoi cambiare la tua password.",
"Your Email": "La tua email", "Your Email": "La tua email",
"Your import is complete.": "La tua importazione è completata.", "Your import is complete.": "La tua importazione è completata.",
"Your name": "Il tuo nome", "Your name": "Il tuo nome",
"Your Name": "Il Tuo Nome", "Your Name": "Il tuo nome",
"Your password": "La tua password", "Your password": "La tua password",
"Your password must be a minimum of 8 characters.": "La tua password deve contenere almeno 8 caratteri.", "Your password must be a minimum of 8 characters.": "La tua password deve contenere almeno 8 caratteri.",
"Sidebar toggle": "Attiva/disattiva barra laterale", "Sidebar toggle": "Attiva/disattiva barra laterale",
"Comments": "Commenti", "Comments": "Commenti",
"404 page not found": "404 pagina non trovata", "404 page not found": "404 pagina non trovata",
"Sorry, we can't find the page you are looking for.": "Siamo spiacenti, non riusciamo a trovare la pagina che stai cercando.", "Sorry, we can't find the page you are looking for.": "Siamo spiacenti, non riusciamo a trovare la pagina che stai cercando.",
"Take me back to homepage": "Torna all'homepage", "Take me back to homepage": "Torna alla homepage",
"Forgot password": "Password dimenticata", "Forgot password": "Password dimenticata",
"Forgot your password?": "Hai dimenticato la password?", "Forgot your password?": "Hai dimenticato la password?",
"A password reset link has been sent to your email. Please check your inbox.": "Un link per il reset della password è stato inviato al tuo indirizzo email. Per favore, controlla la tua casella di posta.", "A password reset link has been sent to your email. Please check your inbox.": "Un link per il reset della password è stato inviato al tuo indirizzo email. Per favore, controlla la tua casella di posta.",
@@ -203,9 +217,14 @@
"Reply...": "Rispondi...", "Reply...": "Rispondi...",
"Error loading comments.": "Si è verificato un errore durante il caricamento dei commenti.", "Error loading comments.": "Si è verificato un errore durante il caricamento dei commenti.",
"No comments yet.": "Nessun commento per ora.", "No comments yet.": "Nessun commento per ora.",
"No open comments.": "Nessun commento aperto.",
"No resolved comments.": "Nessun commento risolto.",
"Add a comment...": "Aggiungi un commento...",
"Edit comment": "Modifica commento", "Edit comment": "Modifica commento",
"Delete comment": "Elimina commento", "Delete comment": "Elimina commento",
"Are you sure you want to delete this comment?": "Sei sicuro di voler eliminare questo commento?", "Are you sure you want to delete this comment?": "Sei sicuro di voler eliminare questo commento?",
"Delete chat": "Elimina chat",
"Are you sure you want to delete '{{title}}'? This action cannot be undone.": "Sei sicuro di voler eliminare '{{title}}'? Questa azione non può essere annullata.",
"Comment created successfully": "Commento creato con successo", "Comment created successfully": "Commento creato con successo",
"Error creating comment": "Si è verificato un errore durante la creazione del commento", "Error creating comment": "Si è verificato un errore durante la creazione del commento",
"Comment updated successfully": "Commento aggiornato con successo", "Comment updated successfully": "Commento aggiornato con successo",
@@ -213,7 +232,17 @@
"Comment deleted successfully": "Commento eliminato con successo", "Comment deleted successfully": "Commento eliminato con successo",
"Failed to delete comment": "Impossibile eliminare il commento", "Failed to delete comment": "Impossibile eliminare il commento",
"Comment resolved successfully": "Commento risolto con successo", "Comment resolved successfully": "Commento risolto con successo",
"Comment re-opened successfully": "Commento riaperto con successo",
"Comment unresolved successfully": "Commento contrassegnato come non risolto con successo",
"Failed to resolve comment": "Impossibile risolvere il commento", "Failed to resolve comment": "Impossibile risolvere il commento",
"Resolve comment": "Risolvi commento",
"Unresolve comment": "Segna commento come non risolto",
"Resolve Comment Thread": "Risolvi discussione del commento",
"Unresolve Comment Thread": "Segna discussione del commento come non risolta",
"Are you sure you want to resolve this comment thread? This will mark it as completed.": "Sei sicuro di voler risolvere questa discussione di commenti? Questo la contrassegnerà come completata.",
"Are you sure you want to unresolve this comment thread?": "Sei sicuro di voler annullare la risoluzione di questa discussione di commenti?",
"Resolved": "Risolto",
"No active comments.": "Nessun commento attivo.",
"Revoke invitation": "Revoca invito", "Revoke invitation": "Revoca invito",
"Revoke": "Revoca", "Revoke": "Revoca",
"Don't": "Non", "Don't": "Non",
@@ -222,7 +251,9 @@
"Anyone with this link can join this workspace.": "Chiunque con questo link può unirsi a questa area di lavoro.", "Anyone with this link can join this workspace.": "Chiunque con questo link può unirsi a questa area di lavoro.",
"Invite link": "Link d'invito", "Invite link": "Link d'invito",
"Copy": "Copia", "Copy": "Copia",
"Copy to space": "Copia nello spazio",
"Copied": "Copiato", "Copied": "Copiato",
"Duplicate": "Duplica",
"Select a user": "Seleziona un utente", "Select a user": "Seleziona un utente",
"Select a group": "Seleziona un gruppo", "Select a group": "Seleziona un gruppo",
"Export all pages and attachments in this space.": "Esporta tutte le pagine e gli allegati di questo spazio.", "Export all pages and attachments in this space.": "Esporta tutte le pagine e gli allegati di questo spazio.",
@@ -230,7 +261,7 @@
"Are you sure you want to delete this space?": "Sei sicuro di voler eliminare questo spazio?", "Are you sure you want to delete this space?": "Sei sicuro di voler eliminare questo spazio?",
"Delete this space with all its pages and data.": "Elimina questo spazio con tutte le sue pagine e i suoi dati.", "Delete this space with all its pages and data.": "Elimina questo spazio con tutte le sue pagine e i suoi dati.",
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Tutte le pagine, i commenti, gli allegati e i permessi di questo spazio verranno eliminati irreversibilmente.", "All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Tutte le pagine, i commenti, gli allegati e i permessi di questo spazio verranno eliminati irreversibilmente.",
"Confirm space name": "Conferma nome spazio", "Confirm space name": "Conferma nome dello spazio",
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Digita il nome dello spazio <b>{{spaceName}}</b> per confermare la tua azione.", "Type the space name <b>{{spaceName}}</b> to confirm your action.": "Digita il nome dello spazio <b>{{spaceName}}</b> per confermare la tua azione.",
"Format": "Formato", "Format": "Formato",
"Include subpages": "Includi sottopagine", "Include subpages": "Includi sottopagine",
@@ -239,12 +270,16 @@
"Export failed:": "Esportazione fallita:", "Export failed:": "Esportazione fallita:",
"export error": "errore di esportazione", "export error": "errore di esportazione",
"Export page": "Esporta pagina", "Export page": "Esporta pagina",
"Export successful": "Esportazione riuscita",
"Export space": "Esporta spazio", "Export space": "Esporta spazio",
"Export {{type}}": "Esporta {{type}}", "Export {{type}}": "Esporta {{type}}",
"File exceeds the {{limit}} attachment limit": "Il file supera il limite per gli allegati di {{limit}}", "File exceeds the {{limit}} attachment limit": "Il file supera il limite per gli allegati di {{limit}}",
"Align left": "Allinea a sinistra", "Align left": "Allinea a sinistra",
"Align right": "Allinea a destra", "Align right": "Allinea a destra",
"Align center": "Allinea al centro", "Align center": "Allinea al centro",
"Alt text": "Testo alternativo",
"Describe this for accessibility.": "Descrivi questo contenuto per l'accessibilità.",
"Add a description": "Aggiungi una descrizione",
"Justify": "Giustifica", "Justify": "Giustifica",
"Merge cells": "Unisci celle", "Merge cells": "Unisci celle",
"Split cell": "Dividi cella", "Split cell": "Dividi cella",
@@ -255,7 +290,21 @@
"Add row above": "Aggiungi riga sopra", "Add row above": "Aggiungi riga sopra",
"Add row below": "Aggiungi riga sotto", "Add row below": "Aggiungi riga sotto",
"Delete table": "Elimina tabella", "Delete table": "Elimina tabella",
"Add column left": "Aggiungi colonna a sinistra",
"Add column right": "Aggiungi colonna a destra",
"Clear cell": "Cancella cella",
"Clear cells": "Cancella celle",
"Toggle header cell": "Attiva/disattiva cella di intestazione",
"Toggle header column": "Attiva/disattiva colonna di intestazione",
"Toggle header row": "Attiva/disattiva riga di intestazione",
"Move column left": "Sposta colonna a sinistra",
"Move column right": "Sposta colonna a destra",
"Move row down": "Sposta riga in basso",
"Move row up": "Sposta riga in alto",
"Sort A → Z": "Ordina A → Z",
"Sort Z → A": "Ordina Z → A",
"Info": "Informazioni", "Info": "Informazioni",
"Note": "Nota",
"Success": "Successo", "Success": "Successo",
"Warning": "Avviso", "Warning": "Avviso",
"Danger": "Pericolo", "Danger": "Pericolo",
@@ -266,6 +315,11 @@
"Save & Exit": "Salva ed esci", "Save & Exit": "Salva ed esci",
"Double-click to edit Excalidraw diagram": "Fai doppio clic per modificare il diagramma di Excalidraw", "Double-click to edit Excalidraw diagram": "Fai doppio clic per modificare il diagramma di Excalidraw",
"Paste link": "Incolla link", "Paste link": "Incolla link",
"Paste link or search pages": "Incolla il link o cerca le pagine",
"Link to web page": "Collega a una pagina web",
"Recents": "Recenti",
"Page or URL": "Pagina o URL",
"Link title": "Titolo del link",
"Edit link": "Modifica link", "Edit link": "Modifica link",
"Remove link": "Rimuovi link", "Remove link": "Rimuovi link",
"Add link": "Aggiungi link", "Add link": "Aggiungi link",
@@ -284,7 +338,7 @@
"Pink": "Rosa", "Pink": "Rosa",
"Gray": "Grigio", "Gray": "Grigio",
"Embed link": "Incorpora collegamento", "Embed link": "Incorpora collegamento",
"Invalid {{provider}} embed link": "Link di incorporamento {{provider}} non valido", "Invalid {{provider}} embed link": "Link incorporato {{provider}} non valido",
"Embed {{provider}}": "Incorpora {{provider}}", "Embed {{provider}}": "Incorpora {{provider}}",
"Enter {{provider}} link to embed": "Inserisci il link {{provider}} per incorporare", "Enter {{provider}} link to embed": "Inserisci il link {{provider}} per incorporare",
"Bold": "Grassetto", "Bold": "Grassetto",
@@ -311,33 +365,62 @@
"Create block quote.": "Crea blocco citazione.", "Create block quote.": "Crea blocco citazione.",
"Insert code snippet.": "Inserisci frammento di codice.", "Insert code snippet.": "Inserisci frammento di codice.",
"Insert horizontal rule divider": "Inserisci divisore di regola orizzontale", "Insert horizontal rule divider": "Inserisci divisore di regola orizzontale",
"Page break": "Interruzione di pagina",
"Insert a page break for printing.": "Inserisci un'interruzione di pagina per la stampa.",
"Upload any image from your device.": "Carica un'immagine dal tuo dispositivo.", "Upload any image from your device.": "Carica un'immagine dal tuo dispositivo.",
"Upload any video from your device.": "Carica qualsiasi video dal tuo dispositivo.", "Upload any video from your device.": "Carica qualsiasi video dal tuo dispositivo.",
"Upload any audio from your device.": "Carica qualsiasi audio dal tuo dispositivo.",
"Upload any file from your device.": "Carica qualsiasi file dal tuo dispositivo.", "Upload any file from your device.": "Carica qualsiasi file dal tuo dispositivo.",
"Uploading {{name}}": "Caricamento di {{name}}",
"Uploading file": "Caricamento file",
"Table": "Tabella", "Table": "Tabella",
"Insert a table.": "Inserisci una tabella.", "Insert a table.": "Inserisci una tabella.",
"Insert collapsible block.": "Inserisci blocco comprimibile.", "Insert collapsible block.": "Inserisci blocco comprimibile.",
"Video": "Video", "Video": "Video",
"Divider": "Divisore", "Divider": "Separatore",
"Quote": "Preventivo", "Quote": "Citazione",
"Image": "Immagine", "Image": "Immagine",
"Audio": "Audio",
"Embed PDF": "Incorpora PDF",
"Upload and embed a PDF file.": "Carica e incorpora un file PDF.",
"Embed as PDF": "Incorpora come PDF",
"Failed to load PDF": "Caricamento del PDF non riuscito",
"Convert to attachment": "Converti in allegato",
"File attachment": "Allegato file", "File attachment": "Allegato file",
"Toggle block": "Attiva blocco", "Toggle block": "Blocco a comparsa",
"Callout": "Avviso", "Callout": "Riquadro evidenziato",
"Insert callout notice.": "Inserisci avviso di richiamo.", "Insert callout notice.": "Inserisci avviso di richiamo.",
"Math inline": "Matematica in linea", "Math inline": "Formula matematica in linea",
"Insert inline math equation.": "Inserisci equazione matematica in linea.", "Insert inline math equation.": "Inserisci equazione matematica in linea.",
"Math block": "Blocco matematico", "Math block": "Blocco matematico",
"Insert math equation": "Inserisci equazione matematica", "Insert math equation": "Inserisci equazione matematica",
"Mermaid diagram": "Diagramma di Mermaid", "Mermaid diagram": "Diagramma Mermaid",
"Insert mermaid diagram": "Inserisci un diagramma di Mermaid", "Insert mermaid diagram": "Inserisci diagramma Mermaid",
"Insert and design Drawio diagrams": "Inserisci e progetta diagrammi Drawio", "Insert and design Drawio diagrams": "Inserisci e progetta diagrammi Drawio",
"Insert current date": "Inserisci la data corrente", "Insert current date": "Inserisci data corrente",
"Draw and sketch excalidraw diagrams": "Disegna e schizza diagrammi excalidraw", "Draw and sketch excalidraw diagrams": "Disegna e abbozza diagrammi Excalidraw",
"Multiple": "Multiplo", "Multiple": "Multiplo",
"Turn into": "Trasforma in",
"Text align": "Allinea testo",
"This page may have been deleted, moved, or you may not have access.": "Questa pagina potrebbe essere stata eliminata o spostata, oppure potresti non avere accesso.",
"Go to homepage": "Vai alla pagina principale",
"Pages you create will show up here.": "Le pagine che crei appariranno qui.",
"Heading {{level}}": "Intestazione {{level}}", "Heading {{level}}": "Intestazione {{level}}",
"Toggle title": "Attiva/disattiva titolo", "Toggle title": "Attiva/disattiva titolo",
"Write anything. Enter \"/\" for commands": "Scrivi qualcosa. Digita \"/\" per i comandi", "Write anything. Enter \"/\" for commands": "Scrivi qualsiasi cosa. Digita \"/\" per i comandi",
"Write...": "Scrivi...",
"Column count": "Numero di colonne",
"{{count}} Columns": "{{count}} colonne",
"{{count}} command available_one": "1 command available",
"{{count}} command available_other": "{{count}} commands available",
"{{count}} result available_one": "1 result available",
"{{count}} result available_other": "{{count}} results available",
"Equal columns": "Colonne uguali",
"Left sidebar": "Barra laterale sinistra",
"Right sidebar": "Barra laterale destra",
"Wide center": "Centro ampio",
"Left wide": "Ampia a sinistra",
"Right wide": "Ampia a destra",
"Names do not match": "I nomi non corrispondono", "Names do not match": "I nomi non corrispondono",
"Today, {{time}}": "Oggi, {{time}}", "Today, {{time}}": "Oggi, {{time}}",
"Yesterday, {{time}}": "Ieri, {{time}}", "Yesterday, {{time}}": "Ieri, {{time}}",
@@ -349,18 +432,657 @@
"Member role updated successfully": "Ruolo del membro aggiornato con successo", "Member role updated successfully": "Ruolo del membro aggiornato con successo",
"Created by: <b>{{creatorName}}</b>": "Creato da: <b>{{creatorName}}</b>", "Created by: <b>{{creatorName}}</b>": "Creato da: <b>{{creatorName}}</b>",
"Created at: {{time}}": "Creato il: {{time}}", "Created at: {{time}}": "Creato il: {{time}}",
"Edited by {{name}} {{time}}": "Modificato da {{name}} il {{time}}", "Edited by {{name}} {{time}}": "Modificato da {{name}} {{time}}",
"Word count: {{wordCount}}": "Conteggio parole: {{wordCount}}", "Word count: {{wordCount}}": "Conteggio parole: {{wordCount}}",
"Character count: {{characterCount}}": "Conteggio caratteri: {{characterCount}}", "Character count: {{characterCount}}": "Conteggio caratteri: {{characterCount}}",
"New update": "Nuovo aggiornamento", "New update": "Nuovo aggiornamento",
"{{latestVersion}} is available": "{{latestVersion}} è disponibile", "{{latestVersion}} is available": "{{latestVersion}} è disponibile",
"Default page edit mode": "Modalità di modifica predefinita della pagina",
"Choose your preferred page edit mode. Avoid accidental edits.": "Scegli la tua modalità di modifica della pagina preferita. Evita modifiche accidentali.",
"Choose {{format}} file": "Scegli file {{format}}",
"Reading": "Lettura",
"Delete member": "Elimina membro", "Delete member": "Elimina membro",
"Member deleted successfully": "Membro eliminato con successo", "Member deleted successfully": "Membro eliminato con successo",
"Are you sure you want to delete this workspace member? This action is irreversible.": "Sei sicuro di voler eliminare questo membro del workspace? Questa azione è irreversibile.", "Are you sure you want to delete this workspace member? This action is irreversible.": "Sei sicuro di voler eliminare questo membro del workspace? Questa azione è irreversibile.",
"Deactivate member": "Disattiva membro",
"Activate member": "Attiva membro",
"Are you sure you want to deactivate this workspace member? They will no longer be able to access this workspace.": "Sei sicuro di voler disattivare questo membro dello spazio di lavoro? Non potrà più accedere a questo spazio di lavoro.",
"Are you sure you want to activate this workspace member?": "Sei sicuro di voler attivare questo membro dello spazio di lavoro?",
"Deactivate": "Disattiva",
"Activate": "Attiva",
"Deactivated": "Disattivato",
"Move": "Sposta", "Move": "Sposta",
"Move page": "Sposta pagina", "Move page": "Sposta pagina",
"Move page to a different space.": "Sposta la pagina in un altro spazio.", "Move page to a different space.": "Sposta la pagina in un altro spazio.",
"Real-time editor connection lost. Retrying...": "Connessione all'editor in tempo reale persa. Riprovo...", "Real-time editor connection lost. Retrying...": "Connessione all'editor in tempo reale persa. Riprovo...",
"Table of contents": "Indice dei contenuti", "Table of contents": "Indice",
"Add headings (H1, H2, H3) to generate a table of contents.": "Aggiungi intestazioni (H1, H2, H3) per generare un sommario." "Add headings (H1, H2, H3) to generate a table of contents.": "Aggiungi intestazioni (H1, H2, H3) per generare un sommario.",
"Share": "Condividi",
"Public sharing": "Condivisione pubblica",
"Shared by": "Condiviso da",
"Shared at": "Condiviso il",
"Inherits public sharing from": "Eredita la condivisione pubblica da",
"Share to web": "Condividi sul web",
"Shared to web": "Condiviso sul web",
"Anyone with the link can view this page": "Chiunque abbia il link può visualizzare questa pagina",
"Make this page publicly accessible": "Rendi questa pagina accessibile pubblicamente",
"Include sub-pages": "Includi sottopagine",
"Make sub-pages public too": "Rendi pubbliche anche le sottopagine",
"Allow search engines to index page": "Consenti ai motori di ricerca di indicizzare la pagina",
"Open page": "Apri pagina",
"Page": "Pagina",
"Delete public share link": "Elimina link di condivisione pubblica",
"Delete share": "Elimina condivisione",
"Are you sure you want to delete this shared link?": "Sei sicuro di voler eliminare questo link condiviso?",
"Publicly shared pages from spaces you are a member of will appear here": "Le pagine condivise pubblicamente degli spazi di cui fai parte appariranno qui",
"Share deleted successfully": "Condivisione eliminata con successo",
"Share not found": "Condivisione non trovata",
"Failed to share page": "Condivisione della pagina non riuscita",
"Disable public sharing": "Disabilita la condivisione pubblica",
"Prevent members from sharing pages publicly.": "Impedisci ai membri di condividere pubblicamente le pagine.",
"Toggle public sharing": "Attiva/disattiva la condivisione pubblica",
"Toggle space public sharing": "Attiva/disattiva la condivisione pubblica nello spazio",
"Allow viewers to comment": "Consenti agli utenti di commentare",
"Allow viewers to add comments on pages in this space.": "Consenti agli utenti di aggiungere commenti alle pagine in questo spazio.",
"Toggle viewer comments": "Attiva/disattiva i commenti degli utenti",
"Public sharing is disabled at the workspace level": "La condivisione pubblica è disabilitata a livello di area di lavoro",
"Prevent pages in this space from being shared publicly.": "Impedisci che le pagine in questo spazio vengano condivise pubblicamente.",
"Page permissions": "Autorizzazioni della pagina.",
"Control who can view and edit individual pages. Available with an enterprise license.": "Controlla chi può visualizzare e modificare le singole pagine. Disponibile con una licenza Enterprise.",
"Enable public sharing": "Abilita la condivisione pubblica",
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "Sei sicuro di voler abilitare la condivisione pubblica? I membri potranno condividere le pagine pubblicamente.",
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "Sei sicuro di voler disabilitare la condivisione pubblica? Tutti i link condivisi esistenti in questa area di lavoro verranno eliminati.",
"Are you sure you want to enable public sharing for this space?": "Sei sicuro di voler abilitare la condivisione pubblica per questo spazio?",
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "Sei sicuro di voler disabilitare la condivisione pubblica? Tutti i link condivisi esistenti in questo spazio verranno eliminati.",
"Public sharing is disabled": "La condivisione pubblica è disabilitata",
"Public sharing has been disabled at the workspace level.": "La condivisione pubblica è stata disabilitata a livello di area di lavoro.",
"Public sharing has been disabled for this space.": "La condivisione pubblica è stata disabilitata per questo spazio.",
"Copy page": "Copia pagina",
"Copy page to a different space.": "Copia pagina in un altro spazio.",
"Page copied successfully": "Pagina copiata con successo",
"Page duplicated successfully": "Pagina duplicata con successo",
"Find": "Trova",
"Not found": "Non trovato",
"Previous Match (Shift+Enter)": "Corrispondenza precedente (Maiusc+Invio)",
"Next match (Enter)": "Corrispondenza successiva (Invio)",
"Match case (Alt+C)": "Distingui maiuscole/minuscole (Alt+C)",
"Replace": "Sostituisci",
"Close (Escape)": "Chiudi (Esc)",
"Replace (Enter)": "Sostituisci (Invio)",
"Replace all (Ctrl+Alt+Enter)": "Sostituisci tutto (Ctrl+Alt+Invio)",
"Replace all": "Sostituisci tutto",
"View all": "Visualizza tutto",
"View all spaces": "Visualizza tutti gli spazi",
"Error": "Errore",
"Failed to disable MFA": "Disattivazione MFA non riuscita",
"Disable two-factor authentication": "Disattiva autenticazione a due fattori",
"Disabling two-factor authentication will make your account less secure. You'll only need your password to sign in.": "Disabilitare l'autenticazione a due fattori renderà il tuo account meno sicuro. Avrai bisogno solo della tua password per accedere.",
"Please enter your password to disable two-factor authentication:": "Inserisci la tua password per disabilitare l'autenticazione a due fattori:",
"Two-factor authentication has been enabled": "L'autenticazione a due fattori è stata abilitata",
"Two-factor authentication has been disabled": "L'autenticazione a due fattori è stata disabilitata",
"2-step verification": "Verifica in 2 passaggi",
"Protect your account with an additional verification layer when signing in.": "Proteggi il tuo account con un ulteriore livello di verifica durante l'accesso.",
"Two-factor authentication is active on your account.": "L'autenticazione a due fattori è attiva sul tuo account.",
"Add 2FA method": "Aggiungi metodo 2FA",
"Backup codes": "Codici di backup",
"Disable": "Disattiva",
"Invalid verification code": "Codice di verifica non valido",
"New backup codes have been generated": "Sono stati generati nuovi codici di backup",
"Failed to regenerate backup codes": "Rigenerazione dei codici di backup non riuscita",
"About backup codes": "Informazioni sui codici di backup",
"Backup codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "I codici di backup possono essere utilizzati per accedere al tuo account se perdi l'accesso alla tua app di autenticazione. Ogni codice può essere usato solo una volta.",
"You can regenerate new backup codes at any time. This will invalidate all existing codes.": "Puoi rigenerare nuovi codici di backup in qualsiasi momento. Questo invaliderà tutti i codici esistenti.",
"Confirm password": "Conferma password",
"Generate new backup codes": "Genera nuovi codici di backup",
"Save your new backup codes": "Salva i tuoi nuovi codici di backup",
"Make sure to save these codes in a secure place. Your old backup codes are no longer valid.": "Assicurati di salvare questi codici in un luogo sicuro. I tuoi vecchi codici di backup non sono più validi.",
"Your new backup codes": "I tuoi nuovi codici di backup",
"I've saved my backup codes": "Ho salvato i miei codici di backup",
"Failed to setup MFA": "Configurazione MFA non riuscita",
"Setup & Verify": "Configura e verifica",
"Add to authenticator": "Aggiungi all'autenticatore",
"1. Scan this QR code with your authenticator app": "1. Scansiona questo codice QR con la tua app di autenticazione",
"Can't scan the code?": "Non riesci a scansionare il codice?",
"Enter this code manually in your authenticator app:": "Inserisci questo codice manualmente nella tua app di autenticazione:",
"2. Enter the 6-digit code from your authenticator": "2. Inserisci il codice a 6 cifre dal tuo autenticatore",
"Verify and enable": "Verifica e abilita",
"Failed to generate QR code. Please try again.": "Generazione del codice QR non riuscita. Si prega di riprovare.",
"Backup": "Backup",
"Save codes": "Salva codici",
"Save your backup codes": "Salva i tuoi codici di backup",
"These codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "Questi codici possono essere utilizzati per accedere al tuo account se perdi l'accesso alla tua app di autenticazione. Ogni codice può essere usato solo una volta.",
"Print": "Stampa",
"Two-factor authentication has been set up. Please log in again.": "L'autenticazione a due fattori è stata impostata. Effettua nuovamente l'accesso, per favore.",
"Two-Factor authentication required": "Autenticazione a due fattori richiesta",
"Your workspace requires two-factor authentication for all users": "Il tuo workspace richiede l'autenticazione a due fattori per tutti gli utenti",
"To continue accessing your workspace, you must set up two-factor authentication. This adds an extra layer of security to your account.": "Per continuare ad accedere al tuo spazio di lavoro, devi impostare l'autenticazione a due fattori. Questo aggiunge un ulteriore livello di sicurezza al tuo account.",
"Set up two-factor authentication": "Configura l'autenticazione a due fattori",
"Cancel and logout": "Annulla e disconnettiti",
"Your workspace requires two-factor authentication. Please set it up to continue.": "Il tuo spazio di lavoro richiede l'autenticazione a due fattori. Impostala per continuare.",
"This adds an extra layer of security to your account by requiring a verification code from your authenticator app.": "Questo aggiunge un ulteriore livello di sicurezza al tuo account richiedendo un codice di verifica dalla tua app di autenticazione.",
"Password is required": "La password è obbligatoria",
"Password must be at least 8 characters": "La password deve contenere almeno 8 caratteri",
"Please enter a 6-digit code": "Inserisci un codice di 6 cifre",
"Code must be exactly 6 digits": "Il codice deve essere esattamente di 6 cifre",
"Enter the 6-digit code found in your authenticator app": "Inserisci il codice di 6 cifre presente nella tua app di autenticazione",
"Need help authenticating?": "Hai bisogno di aiuto per autenticarti?",
"MFA QR Code": "Codice QR MFA",
"Account created successfully. Please log in to set up two-factor authentication.": "Account creato con successo. Effettua l'accesso per impostare l'autenticazione a due fattori.",
"Password reset successful. Please log in with your new password and complete two-factor authentication.": "Reimpostazione della password riuscita. Accedi con la tua nuova password e completa l'autenticazione a due fattori.",
"Password reset successful. Please log in with your new password to set up two-factor authentication.": "Reimpostazione della password riuscita. Accedi con la tua nuova password per impostare l'autenticazione a due fattori.",
"Password reset was successful. Please log in with your new password.": "Reimpostazione della password riuscita. Accedi con la tua nuova password.",
"Two-factor authentication": "Autenticazione a due fattori",
"Use authenticator app instead": "Usa invece l'app di autenticazione",
"Verify backup code": "Verifica codice di backup",
"Use backup code": "Usa codice di backup",
"Enter one of your backup codes": "Inserisci uno dei tuoi codici di backup",
"Backup code": "Codice di backup",
"Enter one of your backup codes. Each backup code can only be used once.": "Inserisci uno dei tuoi codici di backup. Ogni codice di backup può essere utilizzato solo una volta.",
"Verify": "Verifica",
"Trash": "Cestino",
"Pages in trash will be permanently deleted after {{count}} days.": "Le pagine nel cestino verranno eliminate definitivamente dopo {{count}} giorni.",
"Deleted": "Eliminato",
"No pages in trash": "Nessuna pagina nel cestino",
"Permanently delete page?": "Eliminare definitivamente la pagina?",
"Are you sure you want to permanently delete '{{title}}'? This action cannot be undone.": "Sei sicuro di voler eliminare definitivamente '{{title}}'? Questa azione non può essere annullata.",
"Restore '{{title}}' and its sub-pages?": "Ripristinare '{{title}}' e le sue sottopagine?",
"Move to trash": "Sposta nel cestino",
"Move this page to trash?": "Spostare questa pagina nel cestino?",
"Restore page": "Ripristina pagina",
"Permanently delete": "Elimina definitivamente",
"<b>{{name}}</b> moved this page to Trash {{time}}.": "<b>{{name}}</b> ha spostato questa pagina nel Cestino {{time}}.",
"Page moved to trash": "Pagina spostata nel cestino",
"Page restored successfully": "Pagina ripristinata con successo",
"Deleted by": "Eliminato da",
"Deleted at": "Eliminato il",
"Preview": "Anteprima",
"Subpages": "Sottopagine",
"Failed to load subpages": "Caricamento delle sottopagine non riuscito",
"No subpages": "Nessuna sottopagina",
"Subpages (Child pages)": "Sottopagine (Pagine figlie)",
"List all subpages of the current page": "Elenca tutte le sottopagine della pagina corrente",
"Attachments": "Allegati",
"All spaces": "Tutti gli spazi",
"Unknown": "Sconosciuto",
"Find a space": "Trova uno spazio",
"Search in all your spaces": "Cerca in tutti i tuoi spazi",
"Type": "Tipo",
"Enterprise": "Enterprise",
"Download attachment": "Scarica allegato",
"Allowed email domains": "Domini email consentiti",
"Only users with email addresses from these domains can signup via SSO.": "Solo gli utenti con indirizzi email di questi domini possono registrarsi tramite SSO.",
"Enter valid domain names separated by comma or space": "Inserisci nomi di dominio validi separati da virgola o spazio",
"Enforce two-factor authentication": "Rendi obbligatoria l'autenticazione a due fattori",
"Once enforced, all members must enable two-factor authentication to access the workspace.": "Una volta impostata, tutti i membri devono abilitare l'autenticazione a due fattori per accedere all'area di lavoro.",
"Toggle MFA enforcement": "Attiva/disattiva obbligatorietà MFA",
"Display name": "Nome visualizzato",
"Allow signup": "Consenti registrazione",
"Enabled": "Abilitato",
"Advanced Settings": "Impostazioni avanzate",
"Enable TLS/SSL": "Abilita TLS/SSL",
"Use secure connection to LDAP server": "Usa una connessione sicura al server LDAP",
"Group sync": "Sincronizzazione gruppi",
"No SSO providers found.": "Nessun provider SSO trovato.",
"Delete SSO provider": "Elimina provider SSO",
"Are you sure you want to delete this SSO provider?": "Sei sicuro di voler eliminare questo provider SSO?",
"Action": "Azione",
"{{ssoProviderType}} configuration": "Configurazione {{ssoProviderType}}",
"Icon": "Icona",
"Upload image": "Carica immagine",
"Remove image": "Rimuovi immagine",
"Failed to remove image": "Rimozione immagine fallita",
"Image exceeds 10MB limit.": "L'immagine supera il limite di 10MB.",
"Image removed successfully": "Immagine rimossa con successo",
"API key": "Chiave API",
"API keys": "Chiavi API",
"API management": "Gestione API",
"Custom expiration date": "Data di scadenza personalizzata",
"Enter a descriptive token name": "Inserisci un nome descrittivo del token",
"Expiration": "Scadenza",
"Expired": "Scaduto",
"Expires": "Scade",
"Last use": "Ultimo utilizzo",
"No API keys found": "Nessuna chiave API trovata",
"No expiration": "Nessuna scadenza",
"Revoked successfully": "Revocata con successo",
"Select expiration date": "Seleziona la data di scadenza",
"This action cannot be undone. Any applications using this API key will stop working.": "Questa azione non può essere annullata. Qualsiasi applicazione che utilizza questa chiave API smetterà di funzionare.",
"Update": "Aggiorna",
"Update {{credential}}": "Aggiorna {{credential}}",
"Manage API keys for all users in the workspace": "Gestisci le chiavi API per tutti gli utenti nell'area di lavoro",
"Restrict API key creation to admins": "Limita la creazione delle chiavi API agli amministratori",
"Only admins and owners can create new API keys. Existing member keys will continue to work.": "Solo gli amministratori e i proprietari possono creare nuove chiavi API. Le chiavi dei membri esistenti continueranno a funzionare.",
"Toggle restrict API keys to admins": "Attiva/disattiva la limitazione delle chiavi API agli amministratori",
"API key creation is restricted to admins by your workspace administrator.": "La creazione delle chiavi API è limitata agli amministratori dal tuo amministratore dello spazio di lavoro.",
"AI settings": "Impostazioni AI",
"AI search": "Ricerca AI",
"AI Answer": "Risposta AI",
"Ask AI": "Chiedi all'AI",
"AI is thinking...": "L'AI sta pensando...",
"Thinking": "Sto pensando",
"Ask a question...": "Fai una domanda...",
"AI Answers": "Risposte AI",
"AI-powered search (AI Answers)": "Ricerca con AI (Risposte AI)",
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "La ricerca AI utilizza embeddings vettoriali per fornire capacità di ricerca semantica nel contenuto della tua area di lavoro.",
"Toggle AI search": "Attiva/disattiva ricerca AI",
"Generative AI (Ask AI)": "AI generativa (Chiedi AI)",
"Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.": "Abilita la generazione di contenuti con AI nell'editor. Consente agli utenti di generare, migliorare, tradurre e trasformare il testo.",
"Toggle generative AI": "Attiva/Disattiva AI generativa",
"Upgrade your plan": "Aggiorna il tuo piano",
"Available with a paid license": "Disponibile con una licenza a pagamento",
"Upgrade your license tier.": "Aggiorna il livello della tua licenza.",
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "L'IA è disponibile solo nell'edizione Enterprise di Docmost. Contatta sales@docmost.com.",
"AI & MCP": "IA e MCP",
"AI": "IA",
"MCP": "MCP",
"Model Context Protocol (MCP)": "Model Context Protocol (MCP)",
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Abilita il server MCP per consentire ad assistenti e strumenti IA di interagire con i contenuti del tuo spazio di lavoro.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP è disponibile solo nell'edizione Enterprise di Docmost. Contatta sales@docmost.com.",
"MCP Server URL": "URL del server MCP",
"Use your API key for authentication. You can manage API keys in your account settings.": "Usa la tua chiave API per l'autenticazione. Puoi gestire le chiavi API nelle impostazioni del tuo account.",
"Supported tools": "Strumenti supportati",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "Il tuo spazio di lavoro ha MCP abilitato. Usa la tua chiave API per collegare gli assistenti IA.",
"MCP server URL:": "URL del server MCP:",
"Learn more": "Scopri di più",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Gestisci le API key per tutti gli utenti nello spazio di lavoro. Consulta la <anchor>documentazione API</anchor> per i dettagli sull'utilizzo.",
"View the <anchor>API documentation</anchor> for usage details.": "Consulta la <anchor>documentazione API</anchor> per i dettagli sull'utilizzo.",
"View the <anchor>MCP documentation</anchor>.": "Consulta la <anchor>documentazione MCP</anchor>.",
"Sources": "Fonti",
"AI Answers not available for attachments": "Risposte AI non disponibili per gli allegati",
"No answer available": "Nessuna risposta disponibile",
"Background color": "Colore di sfondo",
"Highlight color": "Colore evidenziato",
"Remove color": "Rimuovi colore",
"Notifications": "Notifiche",
"No notifications": "Nessuna notifica",
"No unread notifications": "Nessuna notifica non letta",
"All notifications": "Tutte le notifiche",
"Unread only": "Solo non lette",
"Mark all as read": "Segna tutto come letto",
"Mark as read": "Segna come letto",
"More options": "Altre opzioni",
"<bold>{{name}}</bold> mentioned you in a comment": "<bold>{{name}}</bold> ti ha menzionato in un commento",
"<bold>{{name}}</bold> commented on a page": "<bold>{{name}}</bold> ha commentato una pagina",
"<bold>{{name}}</bold> resolved a comment": "<bold>{{name}}</bold> ha risolto un commento",
"<bold>{{name}}</bold> mentioned you on a page": "<bold>{{name}}</bold> ti ha menzionato in una pagina",
"<bold>{{name}}</bold> gave you edit access to a page": "<bold>{{name}}</bold> ti ha dato accesso in modifica a una pagina",
"<bold>{{name}}</bold> gave you view access to a page": "<bold>{{name}}</bold> ti ha dato accesso in visualizzazione a una pagina",
"<bold>{{name}}</bold> updated a page": "<bold>{{name}}</bold> ha aggiornato una pagina",
"Watch page": "Segui pagina",
"Stop watching": "Smetti di seguire",
"Watch space": "Segui spazio",
"Stop watching space": "Smetti di seguire lo spazio",
"Email notifications": "Notifiche email",
"Page updates": "Aggiornamenti pagina",
"Get notified when pages you watch are updated.": "Ricevi una notifica quando le pagine che segui vengono aggiornate.",
"Page mentions": "Menzioni nella pagina",
"Get notified when someone mentions you on a page.": "Ricevi una notifica quando qualcuno ti menziona su una pagina.",
"Comment mentions": "Menzioni nei commenti",
"Get notified when someone mentions you in a comment.": "Ricevi una notifica quando qualcuno ti menziona in un commento.",
"New comments": "Nuovi commenti",
"Get notified about new comments on threads you participate in.": "Ricevi una notifica sui nuovi commenti nelle discussioni a cui partecipi.",
"Resolved comments": "Commenti risolti",
"Get notified when your comment is resolved.": "Ricevi una notifica quando il tuo commento viene risolto.",
"You are now watching this page": "Ora stai seguendo questa pagina",
"You are no longer watching this page": "Non stai più seguendo questa pagina",
"You are now watching this space": "Ora stai seguendo questo spazio",
"You are no longer watching this space": "Non stai più seguendo questo spazio",
"Direct": "Diretto",
"Updates": "Aggiornamenti",
"Today": "Oggi",
"Yesterday": "Ieri",
"This week": "Questa settimana",
"Older": "Più vecchie",
"Restricted page": "Pagina con accesso ristretto",
"Restricted pages cannot be shared publicly.": "Le pagine con accesso ristretto non possono essere condivise pubblicamente.",
"Restricted by parent": "Limitata dalla pagina genitore",
"Restricted": "Limitata",
"Open": "Aperta",
"Inherits restrictions from ancestor page": "Eredita le restrizioni dalla pagina genitore",
"Only people listed below can access this page": "Solo le persone elencate di seguito possono accedere a questa pagina",
"Everyone in this space can access": "Chiunque in questo spazio può accedere",
"No additional restrictions on this page": "Nessuna restrizione aggiuntiva su questa pagina",
"Only specific people can access": "Solo persone specifiche possono accedere",
"Use only inherited restrictions": "Usa solo le restrizioni ereditate",
"Add restrictions on top of inherited": "Aggiungi restrizioni oltre a quelle ereditate",
"Inherited restriction": "Restrizione ereditata",
"Access limited by": "Accesso limitato da",
"Restrict access to control who can view and edit this page": "Limita l'accesso per controllare chi può visualizzare e modificare questa pagina",
"Add additional restrictions specific to this page": "Aggiungi restrizioni aggiuntive specifiche per questa pagina",
"Access": "Accesso",
"People with access": "Persone con accesso",
"Remove all": "Rimuovi tutto",
"Remove access": "Rimuovi accesso",
"Remove all access": "Rimuovi tutti gli accessi",
"Are you sure you want to remove this member's access to the page?": "Sei sicuro di voler rimuovere l'accesso di questo membro alla pagina?",
"Are you sure you want to remove all specific access? This will make the page open to everyone in the space.": "Sei sicuro di voler rimuovere tutti gli accessi specifici? Questo renderà la pagina accessibile a tutti nello spazio.",
"Trash retention": "Conservazione del cestino",
"Pages in trash will be permanently deleted after this period.": "Le pagine nel cestino verranno eliminate definitivamente dopo questo periodo.",
"Trash retention updated": "Conservazione del cestino aggiornata",
"Failed to update trash retention": "Impossibile aggiornare la conservazione del cestino",
"Removed page restriction": "Restrizione della pagina rimossa",
"Added page permission": "Permesso sulla pagina aggiunto",
"Removed page permission": "Permesso sulla pagina rimosso",
"day": "giorno",
"days": "giorni",
"week": "settimana",
"weeks": "settimane",
"month": "mese",
"months": "mesi",
"year": "anno",
"years": "anni",
"Period": "Periodo",
"Fixed date": "Data fissa",
"Indefinitely": "A tempo indeterminato",
"Days": "Giorni",
"Weeks": "Settimane",
"Months": "Mesi",
"Years": "Anni",
"Pick a date": "Scegli una data",
"Maximum is {{max}} {{unit}} for this unit": "Il massimo consentito è {{max}} {{unit}} per questa unità",
"Never expires. Verifiers can re-verify at any time.": "Non scade mai. I verificatori possono verificare nuovamente in qualsiasi momento.",
"Verified": "Verificato",
"Review needed": "Revisione necessaria",
"Verification expired": "Verifica scaduta",
"Draft": "Bozza",
"In Approval": "In approvazione",
"In approval": "In approvazione",
"Approved": "Approvato",
"Obsolete": "Obsoleto",
"Expiring": "In scadenza",
"Set up verification": "Configura la verifica",
"Verify page": "Verifica la pagina",
"Page verification": "Verifica della pagina",
"Add verification": "Aggiungi verifica",
"Edit verification": "Modifica verifica",
"Search by title": "Cerca per titolo",
"Choose how this page should stay accurate.": "Scegli come mantenere accurata questa pagina.",
"Recurring verification": "Verifica ricorrente",
"Verifiers re-confirm this page on a schedule.": "I verificatori riconfermano questa pagina secondo una pianificazione.",
"Re-verify on a schedule (e.g every 30 days )": "Verifica nuovamente secondo una pianificazione (ad es. ogni 30 giorni)",
"Page stays editable at all times": "La pagina resta sempre modificabile",
"Best for runbooks, FAQs, living documentation": "Ideale per runbook, FAQ e documentazione dinamica",
"Approval workflow": "Flusso di approvazione",
"Formal document lifecycle with named approvers.": "Ciclo di vita formale del documento con approvatori nominati.",
"Draft → In approval → Approved → Obsolete": "Bozza → In approvazione → Approvato → Obsoleto",
"Locked once approved, with full history": "Bloccato una volta approvato, con cronologia completa",
"Designed for ISO 9001, ISO 13485, and FDA": "Progettato per ISO 9001, ISO 13485 e FDA",
"Best for SOPs and controlled documents": "Ideale per SOP e documenti controllati",
"Back": "Indietro",
"Quality management": "Gestione della qualità",
"Recurring": "Ricorrente",
"Pages move through draft, approval, and approved stages.": "Le pagine passano attraverso le fasi di bozza, approvazione e approvato.",
"Verifiers": "Verificatori",
"Add verifier": "Aggiungi verificatore",
"I've reviewed this page for accuracy": "Ho controllato l'accuratezza di questa pagina",
"Set up": "Configura",
"Remove verification": "Rimuovi verifica",
"Are you sure you want to remove verification from this page?": "Sei sicuro di voler rimuovere la verifica da questa pagina?",
"Assigned verifiers must periodically re-verify this page.": "I verificatori assegnati devono verificare nuovamente questa pagina periodicamente.",
"Last verified by {{name}} {{time}} (expired)": "Ultima verifica effettuata da {{name}} {{time}} (scaduta)",
"The fixed expiration date has passed.": "La data di scadenza fissa è trascorsa.",
"Verified by {{name}} {{time}}": "Verificato da {{name}} {{time}}",
"Expires {{date}}": "Scade il {{date}}",
"Expired {{date}}": "Scaduto il {{date}}",
"Mark as obsolete": "Contrassegna come obsoleto",
"Mark obsolete": "Contrassegna come obsoleto",
"Returned by {{name}} {{time}}": "Restituito da {{name}} {{time}}",
"No approval has been requested yet.": "Non è stata ancora richiesta alcuna approvazione.",
"Submitted by {{name}} {{time}}": "Inviato da {{name}} {{time}}",
"Someone": "Qualcuno",
"Approved by {{name}} {{time}}": "Approvato da {{name}} {{time}}",
"This document has been marked as obsolete.": "Questo documento è stato contrassegnato come obsoleto.",
"Rejection comment": "Commento di rifiuto",
"Reason for returning this document...": "Motivo della restituzione di questo documento...",
"Confirm rejection": "Conferma rifiuto",
"Submit for approval": "Invia per approvazione",
"Reject": "Rifiuta",
"Approve": "Approva",
"Re-submit for approval": "Invia nuovamente per approvazione",
"Verified until": "Verificato fino al",
"QMS": "QMS",
"Verified pages": "Pagine verificate",
"Search pages...": "Cerca pagine...",
"Filter by space": "Filtra per spazio",
"Filter by type": "Filtra per tipo",
"<bold>{{name}}</bold> verified a page": "<bold>{{name}}</bold> ha verificato una pagina",
"<bold>{{name}}</bold> submitted a page for your approval": "<bold>{{name}}</bold> ha inviato una pagina per la tua approvazione",
"<bold>{{name}}</bold> returned a page for revision": "<bold>{{name}}</bold> ha restituito una pagina per la revisione",
"Page verification expires soon": "La verifica della pagina scadrà presto",
"Page verification has expired": "La verifica della pagina è scaduta",
"Verifying your email": "Verifica della tua email in corso",
"Please wait...": "Attendere...",
"Verification failed. The link may have expired.": "Verifica non riuscita. Il link potrebbe essere scaduto.",
"Check your email": "Controlla la tua email",
"We sent a verification link to {{email}}.": "Abbiamo inviato un link di verifica a {{email}}.",
"We sent a verification link to your email.": "Abbiamo inviato un link di verifica alla tua email.",
"Click the link to verify your email and access your workspace.": "Clicca sul link per verificare la tua email e accedere al tuo workspace.",
"Resend verification email": "Invia nuovamente email di verifica",
"Verification email sent. Please check your inbox.": "Email di verifica inviata. Controlla la tua casella di posta.",
"Failed to resend verification email. Please try again.": "Invio dell'email di verifica non riuscito. Si prega di riprovare.",
"We've sent you an email with your associated workspaces.": "Ti abbiamo inviato un'email con i workspace associati.",
"Load more": "Carica altro",
"Log out of all devices": "Disconnetti da tutti i dispositivi",
"Log out of all sessions except this device": "Disconnetti da tutte le sessioni tranne questo dispositivo",
"This Device": "Questo dispositivo",
"Unknown device": "Dispositivo sconosciuto",
"No active sessions": "Nessuna sessione attiva",
"Session revoked": "Sessione revocata",
"All other sessions revoked": "Tutte le altre sessioni sono state revocate",
"Last used": "Ultimo utilizzo",
"Created": "Creato",
"Rename": "Rinomina",
"Publish": "Pubblica",
"Security": "Sicurezza",
"Enforce SSO": "Rendi obbligatorio SSO",
"Once enforced, members will not be able to login with email and password.": "Una volta reso obbligatorio, i membri non potranno accedere con email e password.",
"AI-generated content may not be accurate.": "I contenuti generati dall'IA potrebbero non essere accurati.",
"AI Chat": "Chat IA",
"Analyze for insights": "Analizza per ottenere approfondimenti",
"Ask anything...": "Chiedi qualsiasi cosa...",
"Assistant said:": "L'assistente ha detto:",
"Chat history": "Cronologia chat",
"Chat name": "Nome chat",
"Chat transcript": "Trascrizione della chat",
"Close": "Chiudi",
"Copy assistant response": "Copia risposta dell'assistente",
"Docmost AI": "Docmost AI",
"Failed to load chat. An error occurred.": "Caricamento della chat non riuscito. Si è verificato un errore.",
"Failed to render this message.": "Impossibile visualizzare questo messaggio.",
"How can I help you today?": "Come posso aiutarti oggi?",
"New chat": "Nuova chat",
"No chat history": "Nessuna cronologia chat",
"No chats found": "Nessuna chat trovata",
"No conversations yet": "Nessuna conversazione al momento",
"Open full page": "Apri pagina completa",
"Scroll to bottom": "Scorri in basso",
"You said:": "Hai detto:",
"Previous 7 days": "Ultimi 7 giorni",
"Previous 30 days": "Ultimi 30 giorni",
"Search chats...": "Cerca nelle chat...",
"Search chats": "Cerca nelle chat",
"Ask anything... Use @ to mention pages": "Chiedi qualsiasi cosa... Usa @ per menzionare le pagine",
"Ask anything or search your workspace": "Chiedi qualsiasi cosa o cerca nel tuo spazio di lavoro",
"Welcome to {{name}}": "Benvenuto in {{name}}",
"Add files": "Aggiungi file",
"Mention a page": "Menziona una pagina",
"Start a new chat to see it here.": "Avvia una nuova chat per vederla qui.",
"Summarize this page": "Riassumi questa pagina",
"Toggle AI Chat": "Attiva/disattiva Chat IA",
"Translate this page": "Traduci questa pagina",
"Try a different search term.": "Prova un termine di ricerca diverso.",
"Try again": "Riprova",
"Untitled chat": "Chat senza titolo",
"What can I help you with?": "Con cosa posso aiutarti?",
"Are you sure you want to revoke this {{credential}}": "Sei sicuro di voler revocare questa {{credential}}",
"Automatically provision users and groups from your identity provider via SCIM.": "Esegui automaticamente il provisioning di utenti e gruppi dal tuo provider di identità tramite SCIM.",
"Configure your identity provider with this URL to provision users and groups.": "Configura il tuo provider di identità con questo URL per eseguire il provisioning di utenti e gruppi.",
"Create {{credential}}": "Crea {{credential}}",
"{{credential}} created": "{{credential}} creata",
"{{credential}} created successfully": "{{credential}} creata con successo",
"Created by": "Creata da",
"Custom": "Personalizzato",
"Enable SCIM": "Abilita SCIM",
"Enter a descriptive name": "Inserisci un nome descrittivo",
"I've saved my {{credential}}": "Ho salvato la mia {{credential}}",
"Important": "Importante",
"Make sure to copy your {{credential}} now. You won't be able to see it again!": "Assicurati di copiare subito la tua {{credential}}. Non potrai più visualizzarla!",
"Never": "Mai",
"Revoke {{credential}}": "Revoca {{credential}}",
"SCIM endpoint URL": "URL dell'endpoint SCIM",
"SCIM provisioning": "Provisioning SCIM",
"SCIM takes precedence over SSO group sync while enabled.": "SCIM ha la precedenza sulla sincronizzazione dei gruppi SSO quando è abilitato.",
"You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "Hai raggiunto il numero massimo di {{max}} token SCIM. Elimina un token esistente per crearne uno nuovo.",
"SCIM token": "Token SCIM",
"SCIM tokens": "Token SCIM",
"This action cannot be undone. Your identity provider will stop syncing immediately.": "Questa azione non può essere annullata. Il tuo provider di identità smetterà di sincronizzarsi immediatamente.",
"Toggle SCIM provisioning": "Attiva/disattiva il provisioning SCIM",
"Token": "Token",
"Page menu": "Menu della pagina",
"Expand": "Espandi",
"Collapse": "Comprimi",
"Comment menu": "Menu dei commenti",
"Group menu": "Menu del gruppo",
"Show hidden breadcrumbs": "Mostra breadcrumb nascosti",
"Breadcrumbs": "Breadcrumb",
"Page actions": "Azioni della pagina",
"Pick emoji": "Scegli emoji",
"Template menu": "Menu del modello",
"Use": "Usa",
"Use template": "Usa modello",
"Preview template: {{title}}": "Anteprima modello: {{title}}",
"Use a template": "Usa un modello",
"Search templates...": "Cerca modelli...",
"Search spaces...": "Cerca spazi...",
"No templates found": "Nessun modello trovato",
"No spaces found": "Nessuno spazio trovato",
"Browse all templates": "Sfoglia tutti i modelli",
"This space": "Questo spazio",
"All templates": "Tutti i modelli",
"Global": "Globale",
"New template": "Nuovo modello",
"Edit template": "Modifica modello",
"Are you sure you want to delete this template?": "Sei sicuro di voler eliminare questo modello?",
"Template scope updated": "Ambito del modello aggiornato",
"Choose which space this template belongs to": "Scegli a quale spazio appartiene questo modello",
"Scope": "Ambito",
"Select scope": "Seleziona ambito",
"Title": "Titolo",
"Saving...": "Salvataggio...",
"Saved": "Salvato",
"Save failed. Retry": "Salvataggio non riuscito. Riprova",
"By {{name}}": "Di {{name}}",
"Updated {{time}}": "Aggiornato {{time}}",
"Choose destination": "Scegli destinazione",
"Search pages and spaces...": "Cerca pagine e spazi...",
"No results found": "Nessun risultato trovato",
"You don't have permission to create pages here": "Non hai l'autorizzazione per creare pagine qui",
"Chat menu": "Menu della chat",
"API key menu": "Menu della chiave API",
"Jump to comment selection": "Vai alla selezione dei commenti",
"Slash commands": "Comandi slash",
"Mention suggestions": "Suggerimenti di menzione",
"Link suggestions": "Suggerimenti di link",
"Diagram editor": "Editor di diagrammi",
"Add comment": "Aggiungi commento",
"Find and replace": "Trova e sostituisci",
"Main navigation": "Navigazione principale",
"Space navigation": "Navigazione dello spazio",
"Settings navigation": "Navigazione delle impostazioni",
"AI navigation": "Navigazione AI",
"Breadcrumb": "Percorso di navigazione",
"Synced block": "Blocco sincronizzato",
"Create a block that stays in sync across pages.": "Crea un blocco che rimanga sincronizzato tra le pagine.",
"Editing original": "Modifica originale",
"Copy synced block": "Copia blocco sincronizzato",
"Unsync": "Annulla sincronizzazione",
"Delete synced block": "Elimina blocco sincronizzato",
"Synced to {{count}} other page_one": "Synced to {{count}} other page",
"Synced to {{count}} other page_other": "Synced to {{count}} other pages",
"ORIGINAL": "ORIGINALE",
"THIS PAGE": "QUESTA PAGINA",
"No pages": "Nessuna pagina",
"The original synced block no longer exists": "Il blocco sincronizzato originale non esiste più",
"You don't have access to this synced block": "Non hai accesso a questo blocco sincronizzato",
"Failed to load this synced block": "Impossibile caricare questo blocco sincronizzato",
"Fixed editor toolbar": "Barra degli strumenti dell'editor fissa",
"Show a formatting toolbar above the editor with quick access to common actions.": "Mostra una barra degli strumenti di formattazione sopra l'editor con accesso rapido alle azioni comuni.",
"Toggle fixed editor toolbar": "Attiva/disattiva barra degli strumenti dell'editor fissa",
"Normal text": "Testo normale",
"More inline formatting": "Altra formattazione in linea",
"Subscript": "Pedice",
"Superscript": "Apice",
"Inline code": "Codice in linea",
"Insert media": "Inserisci contenuti multimediali",
"Mention": "Menzione",
"Emoji": "Emoji",
"Columns": "Colonne",
"More inserts": "Altri inserimenti",
"Embeds": "Incorporamenti",
"Diagrams": "Diagrammi",
"Advanced": "Avanzate",
"Utility": "Utilità",
"Decrease indent": "Riduci rientro",
"Increase indent": "Aumenta rientro",
"Clear formatting": "Cancella formattazione",
"Code block": "Blocco di codice",
"Experimental": "Sperimentale",
"Strikethrough": "Barrato",
"Undo": "Annulla",
"Redo": "Ripeti",
"Backlinks": "Backlink",
"Last updated by": "Ultimo aggiornamento di",
"Last updated": "Ultimo aggiornamento",
"Stats": "Statistiche",
"Word count": "Conteggio parole",
"Characters": "Caratteri",
"Incoming links": "Link in entrata",
"Outgoing links": "Link in uscita",
"Incoming links ({{count}})": "Link in entrata ({{count}})",
"Outgoing links ({{count}})": "Link in uscita ({{count}})",
"No pages link here yet.": "Nessuna pagina rimanda ancora qui.",
"This page doesn't link to other pages yet.": "Questa pagina non rimanda ancora ad altre pagine.",
"Verified until {{date}}": "Verificato fino al {{date}}",
"Labels": "Etichette",
"Add label": "Aggiungi etichetta",
"No labels yet": "Nessuna etichetta per ora",
"Already added": "Già aggiunto",
"Invalid label name": "Nome etichetta non valido",
"No matches": "Nessuna corrispondenza",
"Search or create…": "Cerca o crea…",
"Remove label {{name}}": "Rimuovi etichetta {{name}}",
"Failed to add label": "Impossibile aggiungere l'etichetta",
"Failed to remove label": "Impossibile rimuovere l'etichetta",
"No pages with this label": "Nessuna pagina con questa etichetta",
"Pages tagged with this label will appear here.": "Le pagine contrassegnate con questa etichetta appariranno qui.",
"No pages match your search.": "Nessuna pagina corrisponde alla tua ricerca.",
"Updated {{date}}": "Aggiornato il {{date}}",
"Cell actions": "Azioni cella",
"Column actions": "Azioni colonna",
"Row actions": "Azioni riga",
"Filter": "Filtro",
"Page title": "Titolo pagina",
"Page content": "Contenuto della pagina",
"Member actions": "Azioni membro",
"Toggle password visibility": "Attiva/disattiva visibilità password",
"Send comment": "Invia commento",
"Token actions": "Azioni token",
"Template settings": "Impostazioni modello",
"Edit diagram": "Modifica diagramma",
"Edit embed": "Modifica incorporamento",
"Edit drawing": "Modifica disegno",
"Delete equation": "Elimina equazione",
"Invite actions": "Azioni invito",
"Get started": "Inizia",
"* indicates required fields": "* indica i campi obbligatori",
"List of spaces in this workspace": "Elenco degli spazi in questo spazio di lavoro",
"Active sessions": "Sessioni attive",
"Add {{name}} to favorites": "Aggiungi {{name}} ai preferiti",
"Remove {{name}} from favorites": "Rimuovi {{name}} dai preferiti",
"Added to favorites": "Aggiunto ai preferiti",
"Removed from favorites": "Rimosso dai preferiti",
"Added {{name}} to favorites": "{{name}} aggiunto ai preferiti",
"Removed {{name}} from favorites": "{{name}} rimosso dai preferiti",
"Page menu for {{name}}": "Menu della pagina per {{name}}",
"Create subpage of {{name}}": "Crea sottopagina di {{name}}"
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+754 -32
View File
@@ -7,6 +7,7 @@
"Add members": "Adicionar membros", "Add members": "Adicionar membros",
"Add to groups": "Adicionar aos grupos", "Add to groups": "Adicionar aos grupos",
"Add space members": "Adicionar membros do espaço", "Add space members": "Adicionar membros do espaço",
"Add to favorites": "Adicionar aos favoritos",
"Admin": "Administrador", "Admin": "Administrador",
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Tem certeza de que deseja excluir este grupo? Os membros perderão acesso aos recursos que este grupo possui.", "Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Tem certeza de que deseja excluir este grupo? Os membros perderão acesso aos recursos que este grupo possui.",
"Are you sure you want to delete this page?": "Tem certeza de que deseja excluir esta página?", "Are you sure you want to delete this page?": "Tem certeza de que deseja excluir esta página?",
@@ -29,6 +30,7 @@
"Choose your preferred interface language.": "Escolha o idioma da interface.", "Choose your preferred interface language.": "Escolha o idioma da interface.",
"Choose your preferred page width.": "Escolha a largura preferida da página.", "Choose your preferred page width.": "Escolha a largura preferida da página.",
"Confirm": "Confirmar", "Confirm": "Confirmar",
"Copy as Markdown": "Copiar como Markdown",
"Copy link": "Copiar link", "Copy link": "Copiar link",
"Create": "Criar", "Create": "Criar",
"Create group": "Criar grupo", "Create group": "Criar grupo",
@@ -53,11 +55,12 @@
"e.g Space for product team": "ex.: Espaço para a equipe de produto", "e.g Space for product team": "ex.: Espaço para a equipe de produto",
"e.g Space for sales team to collaborate": "ex.: Espaço para a equipe de vendas colaborar", "e.g Space for sales team to collaborate": "ex.: Espaço para a equipe de vendas colaborar",
"Edit": "Editar", "Edit": "Editar",
"Read": "Ler",
"Edit group": "Editar grupo", "Edit group": "Editar grupo",
"Email": "Email", "Email": "Email",
"Enter a strong password": "Insira uma senha forte", "Enter a strong password": "Insira uma senha forte",
"Enter valid email addresses separated by comma or space max_50": "Insira endereços de email válidos separados por vírgula ou espaço [máx: 50]", "Enter valid email addresses separated by comma or space max_50": "Insira endereços de email válidos separados por vírgula ou espaço [máx: 50]",
"enter valid emails addresses": "insira endereços de email válidos", "enter valid emails addresses": "insira endereços de e-mail válidos",
"Enter your current password": "Insira sua senha atual", "Enter your current password": "Insira sua senha atual",
"enter your full name": "insira seu nome completo", "enter your full name": "insira seu nome completo",
"Enter your new password": "Insira sua nova senha", "Enter your new password": "Insira sua nova senha",
@@ -68,10 +71,14 @@
"Export": "Exportar", "Export": "Exportar",
"Failed to create page": "Falha ao criar página", "Failed to create page": "Falha ao criar página",
"Failed to delete page": "Falha ao excluir página", "Failed to delete page": "Falha ao excluir página",
"Failed to restore page": "Falha ao restaurar página",
"Failed to fetch recent pages": "Falha ao buscar páginas recentes", "Failed to fetch recent pages": "Falha ao buscar páginas recentes",
"Failed to import pages": "Falha ao importar páginas", "Failed to import pages": "Falha ao importar páginas",
"Failed to load page. An error occurred.": "Falha ao carregar página. Ocorreu um erro.", "Failed to load page. An error occurred.": "Falha ao carregar página. Ocorreu um erro.",
"Failed to update data": "Falha ao atualizar dados", "Failed to update data": "Falha ao atualizar dados",
"Favorite spaces": "Espaços favoritos",
"Favorite spaces appear here": "Os espaços favoritos aparecem aqui",
"Favorites": "Favoritos",
"Full access": "Acesso total", "Full access": "Acesso total",
"Full page width": "Usar largura total da página", "Full page width": "Usar largura total da página",
"Full width": "Largura total", "Full width": "Largura total",
@@ -90,6 +97,7 @@
"Invite by email": "Convidar por email", "Invite by email": "Convidar por email",
"Invite members": "Convidar membros", "Invite members": "Convidar membros",
"Invite new members": "Convidar novos membros", "Invite new members": "Convidar novos membros",
"Invite People": "Convidar pessoas",
"Invited members who are yet to accept their invitation will appear here.": "Membros convidados que ainda não aceitaram o convite aparecerão aqui.", "Invited members who are yet to accept their invitation will appear here.": "Membros convidados que ainda não aceitaram o convite aparecerão aqui.",
"Invited members will be granted access to spaces the groups can access": "Os membros convidados terão acesso aos espaços que os grupos podem acessar", "Invited members will be granted access to spaces the groups can access": "Os membros convidados terão acesso aos espaços que os grupos podem acessar",
"Join the workspace": "Entrar no workspace", "Join the workspace": "Entrar no workspace",
@@ -114,6 +122,7 @@
"No group found": "Nenhum grupo encontrado", "No group found": "Nenhum grupo encontrado",
"No page history saved yet.": "Nenhum histórico de página salvo ainda.", "No page history saved yet.": "Nenhum histórico de página salvo ainda.",
"No pages yet": "Nenhuma página ainda", "No pages yet": "Nenhuma página ainda",
"No shared pages": "Sem páginas compartilhadas",
"No results found...": "Nenhum resultado encontrado...", "No results found...": "Nenhum resultado encontrado...",
"No user found": "Nenhum usuário encontrado", "No user found": "Nenhum usuário encontrado",
"Overview": "Visão geral", "Overview": "Visão geral",
@@ -121,11 +130,14 @@
"page": "página", "page": "página",
"Page deleted successfully": "Página excluída com sucesso", "Page deleted successfully": "Página excluída com sucesso",
"Page history": "Histórico da página", "Page history": "Histórico da página",
"Select version": "Selecionar versão",
"Highlight changes": "Destacar alterações",
"Page import is in progress. Please do not close this tab.": "A importação da página está em andamento. Por favor, não feche esta aba.", "Page import is in progress. Please do not close this tab.": "A importação da página está em andamento. Por favor, não feche esta aba.",
"Pages": "Páginas", "Pages": "Páginas",
"pages": "páginas", "pages": "páginas",
"Password": "Senha", "Password": "Senha",
"Password changed successfully": "Senha alterada com sucesso", "Password changed successfully": "Senha alterada com sucesso",
"People": "Pessoas",
"Pending": "Pendente", "Pending": "Pendente",
"Please confirm your action": "Por favor, confirme sua ação", "Please confirm your action": "Por favor, confirme sua ação",
"Preferences": "Preferências", "Preferences": "Preferências",
@@ -133,6 +145,7 @@
"Profile": "Perfil", "Profile": "Perfil",
"Recently updated": "Atualizado recentemente", "Recently updated": "Atualizado recentemente",
"Remove": "Remover", "Remove": "Remover",
"Remove from favorites": "Remover dos favoritos",
"Remove group member": "Remover membro do grupo", "Remove group member": "Remover membro do grupo",
"Remove space member": "Remover membro do espaço", "Remove space member": "Remover membro do espaço",
"Restore": "Restaurar", "Restore": "Restaurar",
@@ -143,16 +156,16 @@
"Search for users": "Buscar usuários", "Search for users": "Buscar usuários",
"Search for users and groups": "Buscar usuários e grupos", "Search for users and groups": "Buscar usuários e grupos",
"Search...": "Buscar...", "Search...": "Buscar...",
"Select language": "Selecionar idioma", "Select language": "Selecione o idioma",
"Select role": "Selecionar função", "Select role": "Selecione a função",
"Select role to assign to all invited members": "Selecione a função para atribuir a todos os membros convidados", "Select role to assign to all invited members": "Selecione a função a ser atribuída a todos os membros convidados",
"Select theme": "Selecionar tema", "Select theme": "Selecione o tema",
"Send invitation": "Enviar convite", "Send invitation": "Enviar convite",
"Invitation sent": "Convite enviado", "Invitation sent": "Convite enviado",
"Settings": "Configurações", "Settings": "Configurações",
"Setup workspace": "Configurar workspace", "Setup workspace": "Configurar workspace",
"Sign In": "Entrar", "Sign In": "Entrar",
"Sign Up": "Registrar-se", "Sign Up": "Cadastrar-se",
"Slug": "Slug", "Slug": "Slug",
"Space": "Espaço", "Space": "Espaço",
"Space description": "Descrição do espaço", "Space description": "Descrição do espaço",
@@ -165,34 +178,35 @@
"No space found": "Nenhum espaço encontrado", "No space found": "Nenhum espaço encontrado",
"Search for spaces": "Pesquisar espaços", "Search for spaces": "Pesquisar espaços",
"Start typing to search...": "Comece a digitar para buscar...", "Start typing to search...": "Comece a digitar para buscar...",
"Status": "Estado", "Status": "Status",
"Successfully imported": "Importado com sucesso", "Successfully imported": "Importado com sucesso",
"Successfully restored": "Restaurado com sucesso", "Successfully restored": "Restaurado com sucesso",
"System settings": "Configurações do sistema", "System settings": "Configurações do sistema",
"Templates": "Modelos",
"Theme": "Tema", "Theme": "Tema",
"To change your email, you have to enter your password and new email.": "Para alterar seu email, você precisa inserir sua senha e o novo email.", "To change your email, you have to enter your password and new email.": "Para alterar seu email, você precisa inserir sua senha e o novo email.",
"Toggle full page width": "Alternar para largura total da página", "Toggle full page width": "Alternar largura total da página",
"Unable to import pages. Please try again.": "Não foi possível importar as páginas. Por favor, tente novamente.", "Unable to import pages. Please try again.": "Não foi possível importar as páginas. Por favor, tente novamente.",
"untitled": "sem título", "untitled": "sem título",
"Untitled": "Sem título", "Untitled": "Sem título",
"Updated successfully": "Atualizado com sucesso", "Updated successfully": "Atualizado com sucesso",
"User": "Usuário", "User": "Usuário",
"Workspace": "Espaço de Trabalho", "Workspace": "Workspace",
"Workspace Name": "Nome do Workspace", "Workspace Name": "Nome do workspace",
"Workspace settings": "Configurações do workspace", "Workspace settings": "Configurações do workspace",
"You can change your password here.": "Você pode alterar sua senha aqui.", "You can change your password here.": "Você pode alterar sua senha aqui.",
"Your Email": "Seu email", "Your Email": "Seu e-mail",
"Your import is complete.": "Sua importação está concluída.", "Your import is complete.": "Sua importação está concluída.",
"Your name": "Seu nome", "Your name": "Seu nome",
"Your Name": "Seu Nome", "Your Name": "Seu Nome",
"Your password": "Sua senha", "Your password": "Sua senha",
"Your password must be a minimum of 8 characters.": "Sua senha deve ter no mínimo 8 caracteres.", "Your password must be a minimum of 8 characters.": "Sua senha deve ter no mínimo 8 caracteres.",
"Sidebar toggle": "Interruptor do painel lateral", "Sidebar toggle": "Alternar barra lateral",
"Comments": "Comentários", "Comments": "Comentários",
"404 page not found": "Erro 404: Página não encontrada", "404 page not found": "404 página não encontrada",
"Sorry, we can't find the page you are looking for.": "Desculpe, não conseguimos encontrar a página que você está procurando.", "Sorry, we can't find the page you are looking for.": "Desculpe, não conseguimos encontrar a página que você está procurando.",
"Take me back to homepage": "Leve-me de volta para a página inicial", "Take me back to homepage": "Voltar para a página inicial",
"Forgot password": "Esqueci a senha", "Forgot password": "Esqueceu a senha",
"Forgot your password?": "Esqueceu sua senha?", "Forgot your password?": "Esqueceu sua senha?",
"A password reset link has been sent to your email. Please check your inbox.": "Um link de redefinição de senha foi enviado para o seu email. Por favor, verifique sua caixa de entrada.", "A password reset link has been sent to your email. Please check your inbox.": "Um link de redefinição de senha foi enviado para o seu email. Por favor, verifique sua caixa de entrada.",
"Send reset link": "Enviar link de recuperação", "Send reset link": "Enviar link de recuperação",
@@ -203,9 +217,14 @@
"Reply...": "Responder...", "Reply...": "Responder...",
"Error loading comments.": "Erro ao carregar comentários.", "Error loading comments.": "Erro ao carregar comentários.",
"No comments yet.": "Ainda sem comentários.", "No comments yet.": "Ainda sem comentários.",
"No open comments.": "Sem comentários em aberto.",
"No resolved comments.": "Sem comentários resolvidos.",
"Add a comment...": "Adicione um comentário...",
"Edit comment": "Editar comentário", "Edit comment": "Editar comentário",
"Delete comment": "Excluir comentário", "Delete comment": "Excluir comentário",
"Are you sure you want to delete this comment?": "Você tem certeza de que deseja excluir este comentário?", "Are you sure you want to delete this comment?": "Você tem certeza de que deseja excluir este comentário?",
"Delete chat": "Excluir chat",
"Are you sure you want to delete '{{title}}'? This action cannot be undone.": "Tem certeza de que deseja excluir '{{title}}'? Esta ação não pode ser desfeita.",
"Comment created successfully": "Comentário criado com sucesso", "Comment created successfully": "Comentário criado com sucesso",
"Error creating comment": "Erro ao criar comentário", "Error creating comment": "Erro ao criar comentário",
"Comment updated successfully": "Comentário atualizado com sucesso", "Comment updated successfully": "Comentário atualizado com sucesso",
@@ -213,7 +232,17 @@
"Comment deleted successfully": "Comentário excluído com sucesso", "Comment deleted successfully": "Comentário excluído com sucesso",
"Failed to delete comment": "Falha ao excluir comentário", "Failed to delete comment": "Falha ao excluir comentário",
"Comment resolved successfully": "Comentário resolvido com sucesso", "Comment resolved successfully": "Comentário resolvido com sucesso",
"Comment re-opened successfully": "Comentário reaberto com sucesso",
"Comment unresolved successfully": "Comentário marcado como não resolvido com sucesso",
"Failed to resolve comment": "Falha ao resolver comentário", "Failed to resolve comment": "Falha ao resolver comentário",
"Resolve comment": "Resolver comentário",
"Unresolve comment": "Marcar comentário como não resolvido",
"Resolve Comment Thread": "Resolver tópico de comentários",
"Unresolve Comment Thread": "Marcar tópico de comentários como não resolvido",
"Are you sure you want to resolve this comment thread? This will mark it as completed.": "Tem certeza de que deseja resolver este fio de comentários? Isso o marcará como concluído.",
"Are you sure you want to unresolve this comment thread?": "Tem certeza de que deseja não resolver este fio de comentários?",
"Resolved": "Resolvido",
"No active comments.": "Sem comentários ativos.",
"Revoke invitation": "Cancelar o convite", "Revoke invitation": "Cancelar o convite",
"Revoke": "Anular", "Revoke": "Anular",
"Don't": "Não", "Don't": "Não",
@@ -222,7 +251,9 @@
"Anyone with this link can join this workspace.": "Qualquer um com este link pode participar deste espaço de trabalho.", "Anyone with this link can join this workspace.": "Qualquer um com este link pode participar deste espaço de trabalho.",
"Invite link": "Link do convite", "Invite link": "Link do convite",
"Copy": "Copiar", "Copy": "Copiar",
"Copy to space": "Copiar para o espaço",
"Copied": "Copiado", "Copied": "Copiado",
"Duplicate": "Duplicar",
"Select a user": "Selecione um usuário", "Select a user": "Selecione um usuário",
"Select a group": "Selecione um grupo", "Select a group": "Selecione um grupo",
"Export all pages and attachments in this space.": "Exportar todas as páginas e anexos deste espaço.", "Export all pages and attachments in this space.": "Exportar todas as páginas e anexos deste espaço.",
@@ -230,7 +261,7 @@
"Are you sure you want to delete this space?": "Tem certeza de que deseja excluir este espaço?", "Are you sure you want to delete this space?": "Tem certeza de que deseja excluir este espaço?",
"Delete this space with all its pages and data.": "Excluir este espaço com todas as suas páginas e dados.", "Delete this space with all its pages and data.": "Excluir este espaço com todas as suas páginas e dados.",
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Todas as páginas, comentários, anexos e permissões neste espaço serão excluídos de forma irreversível.", "All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Todas as páginas, comentários, anexos e permissões neste espaço serão excluídos de forma irreversível.",
"Confirm space name": "Confirme o nome do espaço", "Confirm space name": "Confirmar nome do espaço",
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Digite o nome do espaço <b>{{spaceName}}</b> para confirmar sua ação.", "Type the space name <b>{{spaceName}}</b> to confirm your action.": "Digite o nome do espaço <b>{{spaceName}}</b> para confirmar sua ação.",
"Format": "Formato", "Format": "Formato",
"Include subpages": "Incluir subpáginas", "Include subpages": "Incluir subpáginas",
@@ -239,12 +270,16 @@
"Export failed:": "Falha ao exportar:", "Export failed:": "Falha ao exportar:",
"export error": "erro de exportação", "export error": "erro de exportação",
"Export page": "Exportar página", "Export page": "Exportar página",
"Export successful": "Exportação bem-sucedida",
"Export space": "Exportar espaço", "Export space": "Exportar espaço",
"Export {{type}}": "Exportar para {{type}}", "Export {{type}}": "Exportar para {{type}}",
"File exceeds the {{limit}} attachment limit": "O arquivo excede o limite de anexos {{limit}}", "File exceeds the {{limit}} attachment limit": "O arquivo excede o limite de anexos {{limit}}",
"Align left": "Alinhar à esquerda", "Align left": "Alinhar à esquerda",
"Align right": "Alinhar à direita", "Align right": "Alinhar à direita",
"Align center": "Alinhar ao centro", "Align center": "Alinhar ao centro",
"Alt text": "Texto alternativo",
"Describe this for accessibility.": "Descreva isto para acessibilidade.",
"Add a description": "Adicionar uma descrição",
"Justify": "Justificar", "Justify": "Justificar",
"Merge cells": "Mesclar células", "Merge cells": "Mesclar células",
"Split cell": "Dividir célula", "Split cell": "Dividir célula",
@@ -255,7 +290,21 @@
"Add row above": "Adicionar linha acima", "Add row above": "Adicionar linha acima",
"Add row below": "Adicionar linha abaixo", "Add row below": "Adicionar linha abaixo",
"Delete table": "Excluir tabela", "Delete table": "Excluir tabela",
"Add column left": "Adicionar coluna à esquerda",
"Add column right": "Adicionar coluna à direita",
"Clear cell": "Limpar célula",
"Clear cells": "Limpar células",
"Toggle header cell": "Alternar célula de cabeçalho",
"Toggle header column": "Alternar coluna de cabeçalho",
"Toggle header row": "Alternar linha de cabeçalho",
"Move column left": "Mover coluna para a esquerda",
"Move column right": "Mover coluna para a direita",
"Move row down": "Mover linha para baixo",
"Move row up": "Mover linha para cima",
"Sort A → Z": "Ordenar A → Z",
"Sort Z → A": "Ordenar Z → A",
"Info": "Informação", "Info": "Informação",
"Note": "Observação",
"Success": "Sucesso", "Success": "Sucesso",
"Warning": "Aviso", "Warning": "Aviso",
"Danger": "Perigo", "Danger": "Perigo",
@@ -266,6 +315,11 @@
"Save & Exit": "Salvar e Sair", "Save & Exit": "Salvar e Sair",
"Double-click to edit Excalidraw diagram": "Clique duas vezes para editar o diagrama Excalidraw", "Double-click to edit Excalidraw diagram": "Clique duas vezes para editar o diagrama Excalidraw",
"Paste link": "Colar link", "Paste link": "Colar link",
"Paste link or search pages": "Cole o link ou pesquise páginas",
"Link to web page": "Link para página da web",
"Recents": "Recentes",
"Page or URL": "Página ou URL",
"Link title": "Título do link",
"Edit link": "Editar link", "Edit link": "Editar link",
"Remove link": "Remover link", "Remove link": "Remover link",
"Add link": "Adicionar link", "Add link": "Adicionar link",
@@ -284,7 +338,7 @@
"Pink": "Rosa", "Pink": "Rosa",
"Gray": "Cinza", "Gray": "Cinza",
"Embed link": "Link embutido", "Embed link": "Link embutido",
"Invalid {{provider}} embed link": "Link de incorporação {{provider}} inválido", "Invalid {{provider}} embed link": "Link de incorporação do {{provider}} inválido",
"Embed {{provider}}": "Incorporar {{provider}}", "Embed {{provider}}": "Incorporar {{provider}}",
"Enter {{provider}} link to embed": "Digite o link do {{provider}} para incorporar", "Enter {{provider}} link to embed": "Digite o link do {{provider}} para incorporar",
"Bold": "Negrito", "Bold": "Negrito",
@@ -311,9 +365,14 @@
"Create block quote.": "Crie uma citação em bloco.", "Create block quote.": "Crie uma citação em bloco.",
"Insert code snippet.": "Insira um trecho de código.", "Insert code snippet.": "Insira um trecho de código.",
"Insert horizontal rule divider": "Insira um divisor horizontal", "Insert horizontal rule divider": "Insira um divisor horizontal",
"Page break": "Quebra de página",
"Insert a page break for printing.": "Insira uma quebra de página para impressão.",
"Upload any image from your device.": "Envie qualquer imagem do seu dispositivo.", "Upload any image from your device.": "Envie qualquer imagem do seu dispositivo.",
"Upload any video from your device.": "Envie qualquer vídeo do seu dispositivo.", "Upload any video from your device.": "Envie qualquer vídeo do seu dispositivo.",
"Upload any audio from your device.": "Envie qualquer áudio do seu dispositivo.",
"Upload any file from your device.": "Envie qualquer arquivo do seu dispositivo.", "Upload any file from your device.": "Envie qualquer arquivo do seu dispositivo.",
"Uploading {{name}}": "Enviando {{name}}",
"Uploading file": "Enviando arquivo",
"Table": "Tabela", "Table": "Tabela",
"Insert a table.": "Insira uma tabela.", "Insert a table.": "Insira uma tabela.",
"Insert collapsible block.": "Insira um bloco colapsável.", "Insert collapsible block.": "Insira um bloco colapsável.",
@@ -321,24 +380,48 @@
"Divider": "Divisor", "Divider": "Divisor",
"Quote": "Citação", "Quote": "Citação",
"Image": "Imagem", "Image": "Imagem",
"Audio": "Áudio",
"Embed PDF": "Incorporar PDF",
"Upload and embed a PDF file.": "Envie e incorpore um arquivo PDF.",
"Embed as PDF": "Incorporar como PDF",
"Failed to load PDF": "Falha ao carregar PDF",
"Convert to attachment": "Converter em anexo",
"File attachment": "Anexo de arquivo", "File attachment": "Anexo de arquivo",
"Toggle block": "Bloco colapsável", "Toggle block": "Bloco recolvel",
"Callout": "Aviso", "Callout": "Chamada",
"Insert callout notice.": "Insira um aviso.", "Insert callout notice.": "Insira um aviso.",
"Math inline": "Matemática inline", "Math inline": "Matemática em linha",
"Insert inline math equation.": "Insira uma equação matemática inline.", "Insert inline math equation.": "Insira uma equação matemática inline.",
"Math block": "Bloco de matemática", "Math block": "Bloco matemático",
"Insert math equation": "Insira uma equação matemática", "Insert math equation": "Inserir equação matemática",
"Mermaid diagram": "Diagrama Mermaid", "Mermaid diagram": "Diagrama Mermaid",
"Insert mermaid diagram": "Insira um diagrama Mermaid", "Insert mermaid diagram": "Inserir diagrama Mermaid",
"Insert and design Drawio diagrams": "Insira e projete diagramas Drawio", "Insert and design Drawio diagrams": "Inserir e criar diagramas Drawio",
"Insert current date": "Insira a data atual", "Insert current date": "Inserir data atual",
"Draw and sketch excalidraw diagrams": "Desenhe e esboce diagramas Excalidraw", "Draw and sketch excalidraw diagrams": "Desenhar e esboçar diagramas Excalidraw",
"Multiple": "Múltiplo", "Multiple": "Múltiplo",
"Turn into": "Transformar em",
"Text align": "Alinhar texto",
"This page may have been deleted, moved, or you may not have access.": "Esta página pode ter sido excluída, movida ou você pode não ter acesso a ela.",
"Go to homepage": "Ir para a página inicial",
"Pages you create will show up here.": "As páginas que você criar aparecerão aqui.",
"Heading {{level}}": "Título {{level}}", "Heading {{level}}": "Título {{level}}",
"Toggle title": "Alternar título", "Toggle title": "Título do bloco recolhível",
"Write anything. Enter \"/\" for commands": "Escreva qualquer coisa. Digite \"/\" para comandos", "Write anything. Enter \"/\" for commands": "Escreva qualquer coisa. Digite \"/\" para ver os comandos",
"Names do not match": "Os nomes não coincidem", "Write...": "Escreva...",
"Column count": "Número de colunas",
"{{count}} Columns": "{{count}} colunas",
"{{count}} command available_one": "1 command available",
"{{count}} command available_other": "{{count}} commands available",
"{{count}} result available_one": "1 result available",
"{{count}} result available_other": "{{count}} results available",
"Equal columns": "Colunas iguais",
"Left sidebar": "Barra lateral esquerda",
"Right sidebar": "Barra lateral direita",
"Wide center": "Centro largo",
"Left wide": "Largo à esquerda",
"Right wide": "Largo à direita",
"Names do not match": "Os nomes não correspondem",
"Today, {{time}}": "Hoje, {{time}}", "Today, {{time}}": "Hoje, {{time}}",
"Yesterday, {{time}}": "Ontem, {{time}}", "Yesterday, {{time}}": "Ontem, {{time}}",
"Space created successfully": "Espaço criado com sucesso", "Space created successfully": "Espaço criado com sucesso",
@@ -354,13 +437,652 @@
"Character count: {{characterCount}}": "Contagem de caracteres: {{characterCount}}", "Character count: {{characterCount}}": "Contagem de caracteres: {{characterCount}}",
"New update": "Nova atualização", "New update": "Nova atualização",
"{{latestVersion}} is available": "{{latestVersion}} está disponível", "{{latestVersion}} is available": "{{latestVersion}} está disponível",
"Default page edit mode": "Modo padrão de edição da página",
"Choose your preferred page edit mode. Avoid accidental edits.": "Escolha o modo de edição de página preferido. Evite edições acidentais.",
"Choose {{format}} file": "Escolher arquivo {{format}}",
"Reading": "Leitura",
"Delete member": "Excluir membro", "Delete member": "Excluir membro",
"Member deleted successfully": "Membro removido com sucesso", "Member deleted successfully": "Membro excluído com sucesso",
"Are you sure you want to delete this workspace member? This action is irreversible.": "Você tem certeza que deseja deletar este membro do workspace? Esta ação é irreversível.", "Are you sure you want to delete this workspace member? This action is irreversible.": "Você tem certeza que deseja deletar este membro do workspace? Esta ação é irreversível.",
"Deactivate member": "Desativar membro",
"Activate member": "Ativar membro",
"Are you sure you want to deactivate this workspace member? They will no longer be able to access this workspace.": "Tem certeza de que deseja desativar este membro do espaço de trabalho? Ele não poderá mais acessar este espaço de trabalho.",
"Are you sure you want to activate this workspace member?": "Tem certeza de que deseja ativar este membro do espaço de trabalho?",
"Deactivate": "Desativar",
"Activate": "Ativar",
"Deactivated": "Desativado",
"Move": "Mover", "Move": "Mover",
"Move page": "Mover página", "Move page": "Mover página",
"Move page to a different space.": "Mover página para um espaço diferente.", "Move page to a different space.": "Mover página para um espaço diferente.",
"Real-time editor connection lost. Retrying...": "Conexão do editor em tempo real perdida. Tentando novamente...", "Real-time editor connection lost. Retrying...": "Conexão do editor em tempo real perdida. Tentando novamente...",
"Table of contents": "Tabela de conteúdos", "Table of contents": "Sumário",
"Add headings (H1, H2, H3) to generate a table of contents.": "Adicionar títulos (H1, H2, H3) para gerar uma tabela de conteúdo." "Add headings (H1, H2, H3) to generate a table of contents.": "Adicionar títulos (H1, H2, H3) para gerar uma tabela de conteúdo.",
"Share": "Compartilhar",
"Public sharing": "Compartilhamento público",
"Shared by": "Compartilhado por",
"Shared at": "Compartilhado em",
"Inherits public sharing from": "Herda o compartilhamento público de",
"Share to web": "Compartilhar na web",
"Shared to web": "Compartilhado na web",
"Anyone with the link can view this page": "Qualquer pessoa com o link pode visualizar esta página",
"Make this page publicly accessible": "Tornar esta página acessível publicamente",
"Include sub-pages": "Incluir subpáginas",
"Make sub-pages public too": "Tornar as subpáginas públicas também",
"Allow search engines to index page": "Permitir que mecanismos de busca indexem a página",
"Open page": "Abrir página",
"Page": "Página",
"Delete public share link": "Excluir link de compartilhamento público",
"Delete share": "Excluir compartilhamento",
"Are you sure you want to delete this shared link?": "Tem certeza de que deseja excluir este link compartilhado?",
"Publicly shared pages from spaces you are a member of will appear here": "Páginas compartilhadas publicamente dos espaços dos quais você é membro aparecerão aqui",
"Share deleted successfully": "Compartilhamento excluído com sucesso",
"Share not found": "Compartilhamento não encontrado",
"Failed to share page": "Falha ao compartilhar a página",
"Disable public sharing": "Desativar compartilhamento público",
"Prevent members from sharing pages publicly.": "Impedir que os membros compartilhem páginas publicamente.",
"Toggle public sharing": "Alternar compartilhamento público",
"Toggle space public sharing": "Alternar compartilhamento público do espaço",
"Allow viewers to comment": "Permitir que os visualizadores comentem",
"Allow viewers to add comments on pages in this space.": "Permitir que os visualizadores adicionem comentários em páginas deste espaço.",
"Toggle viewer comments": "Ativar/desativar comentários de visualizadores",
"Public sharing is disabled at the workspace level": "O compartilhamento público está desativado no nível do espaço de trabalho",
"Prevent pages in this space from being shared publicly.": "Impedir que as páginas neste espaço sejam compartilhadas publicamente.",
"Page permissions": "Permissões da página},{",
"Control who can view and edit individual pages. Available with an enterprise license.": "Controle quem pode visualizar e editar páginas individuais. Disponível com licença empresarial.",
"Enable public sharing": "Ativar compartilhamento público",
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "Tem certeza de que deseja ativar o compartilhamento público? Os membros poderão compartilhar páginas publicamente.",
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "Tem certeza de que deseja desativar o compartilhamento público? Todos os links compartilhados existentes neste espaço de trabalho serão excluídos.",
"Are you sure you want to enable public sharing for this space?": "Tem certeza de que deseja ativar o compartilhamento público para este espaço?",
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "Tem certeza de que deseja desativar o compartilhamento público? Todos os links compartilhados existentes neste espaço serão excluídos.",
"Public sharing is disabled": "Compartilhamento público está desativado",
"Public sharing has been disabled at the workspace level.": "O compartilhamento público foi desativado no nível do espaço de trabalho.",
"Public sharing has been disabled for this space.": "O compartilhamento público foi desativado para este espaço.",
"Copy page": "Copiar página",
"Copy page to a different space.": "Copiar página para um espaço diferente.",
"Page copied successfully": "Página copiada com sucesso",
"Page duplicated successfully": "Página duplicada com sucesso",
"Find": "Localizar",
"Not found": "Não encontrado",
"Previous Match (Shift+Enter)": "Correspondência anterior (Shift+Enter)",
"Next match (Enter)": "Próxima correspondência (Enter)",
"Match case (Alt+C)": "Diferenciar maiúsculas de minúsculas (Alt+C)",
"Replace": "Substituir",
"Close (Escape)": "Fechar (Escape)",
"Replace (Enter)": "Substituir (Enter)",
"Replace all (Ctrl+Alt+Enter)": "Substituir tudo (Ctrl+Alt+Enter)",
"Replace all": "Substituir tudo",
"View all": "Ver tudo",
"View all spaces": "Ver todos os espaços",
"Error": "Erro",
"Failed to disable MFA": "Falha ao desativar a MFA",
"Disable two-factor authentication": "Desativar autenticação de dois fatores",
"Disabling two-factor authentication will make your account less secure. You'll only need your password to sign in.": "Desativar a autenticação de dois fatores tornará sua conta menos segura. Você só precisará de sua senha para entrar.",
"Please enter your password to disable two-factor authentication:": "Por favor, insira sua senha para desativar a autenticação de dois fatores:",
"Two-factor authentication has been enabled": "A autenticação de dois fatores foi ativada",
"Two-factor authentication has been disabled": "A autenticação de dois fatores foi desativada",
"2-step verification": "Verificação em 2 etapas",
"Protect your account with an additional verification layer when signing in.": "Proteja sua conta com uma camada adicional de verificação ao entrar.",
"Two-factor authentication is active on your account.": "Autenticação de dois fatores está ativa na sua conta.",
"Add 2FA method": "Adicionar método de 2FA",
"Backup codes": "Códigos de backup",
"Disable": "Desativar",
"Invalid verification code": "Código de verificação inválido",
"New backup codes have been generated": "Novos códigos de backup foram gerados",
"Failed to regenerate backup codes": "Falha ao regenerar os códigos de backup",
"About backup codes": "Sobre os códigos de backup",
"Backup codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "Códigos de backup podem ser usados para acessar sua conta se perder acesso ao aplicativo autenticador. Cada código só pode ser usado uma vez.",
"You can regenerate new backup codes at any time. This will invalidate all existing codes.": "Você pode regenerar novos códigos de backup a qualquer momento. Isso invalidará todos os códigos existentes.",
"Confirm password": "Confirmar senha",
"Generate new backup codes": "Gerar novos códigos de backup",
"Save your new backup codes": "Salve seus novos códigos de backup",
"Make sure to save these codes in a secure place. Your old backup codes are no longer valid.": "Certifique-se de salvar esses códigos em um local seguro. Seus códigos de backup antigos não são mais válidos.",
"Your new backup codes": "Seus novos códigos de backup",
"I've saved my backup codes": "Salvei meus códigos de backup",
"Failed to setup MFA": "Falha ao configurar a MFA",
"Setup & Verify": "Configurar e verificar",
"Add to authenticator": "Adicionar ao autenticador",
"1. Scan this QR code with your authenticator app": "1. Escaneie este código QR com seu aplicativo autenticador",
"Can't scan the code?": "Não consegue escanear o código?",
"Enter this code manually in your authenticator app:": "Digite este código manualmente em seu aplicativo autenticador:",
"2. Enter the 6-digit code from your authenticator": "2. Insira o código de 6 dígitos do seu autenticador",
"Verify and enable": "Verificar e ativar",
"Failed to generate QR code. Please try again.": "Falha ao gerar código QR. Por favor, tente novamente.",
"Backup": "Backup",
"Save codes": "Salvar códigos",
"Save your backup codes": "Salve seus códigos de backup",
"These codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "Esses códigos podem ser usados para acessar sua conta se você perder o acesso ao aplicativo autenticador. Cada código só pode ser usado uma vez.",
"Print": "Imprimir",
"Two-factor authentication has been set up. Please log in again.": "A autenticação de dois fatores foi configurada. Por favor, faça login novamente.",
"Two-Factor authentication required": "Autenticação de dois fatores obrigatória",
"Your workspace requires two-factor authentication for all users": "Seu workspace exige autenticação de dois fatores para todos os usuários",
"To continue accessing your workspace, you must set up two-factor authentication. This adds an extra layer of security to your account.": "Para continuar acessando seu espaço de trabalho, você deve configurar a autenticação de dois fatores. Isso adiciona uma camada extra de segurança à sua conta.",
"Set up two-factor authentication": "Configurar autenticação de dois fatores",
"Cancel and logout": "Cancelar e sair",
"Your workspace requires two-factor authentication. Please set it up to continue.": "Seu espaço de trabalho requer autenticação de dois fatores. Por favor, configure para continuar.",
"This adds an extra layer of security to your account by requiring a verification code from your authenticator app.": "Isso adiciona uma camada extra de segurança à sua conta, exigindo um código de verificação de seu aplicativo autenticador.",
"Password is required": "A senha é obrigatória",
"Password must be at least 8 characters": "A senha deve ter pelo menos 8 caracteres",
"Please enter a 6-digit code": "Insira um código de 6 dígitos",
"Code must be exactly 6 digits": "O código deve ter exatamente 6 dígitos",
"Enter the 6-digit code found in your authenticator app": "Insira o código de 6 dígitos encontrado no seu aplicativo autenticador",
"Need help authenticating?": "Precisa de ajuda para autenticar?",
"MFA QR Code": "Código QR de MFA",
"Account created successfully. Please log in to set up two-factor authentication.": "Conta criada com sucesso. Por favor, faça login para configurar a autenticação de dois fatores.",
"Password reset successful. Please log in with your new password and complete two-factor authentication.": "Redefinição de senha bem-sucedida. Por favor, faça login com sua nova senha e complete a autenticação de dois fatores.",
"Password reset successful. Please log in with your new password to set up two-factor authentication.": "Redefinição de senha bem-sucedida. Por favor, faça login com sua nova senha para configurar a autenticação de dois fatores.",
"Password reset was successful. Please log in with your new password.": "Redefinição de senha foi bem-sucedida. Por favor, faça login com sua nova senha.",
"Two-factor authentication": "Autenticação de dois fatores",
"Use authenticator app instead": "Usar aplicativo autenticador em vez disso",
"Verify backup code": "Verificar código de backup",
"Use backup code": "Usar código de backup",
"Enter one of your backup codes": "Insira um dos seus códigos de backup",
"Backup code": "Código de backup",
"Enter one of your backup codes. Each backup code can only be used once.": "Digite um de seus códigos de backup. Cada código de backup só pode ser usado uma vez.",
"Verify": "Verificar",
"Trash": "Lixeira",
"Pages in trash will be permanently deleted after {{count}} days.": "{count, plural, one {A página na lixeira será excluída permanentemente após # dia.} other {As páginas na lixeira serão excluídas permanentemente após # dias.}}",
"Deleted": "Excluído",
"No pages in trash": "Nenhuma página na lixeira",
"Permanently delete page?": "Excluir página permanentemente?",
"Are you sure you want to permanently delete '{{title}}'? This action cannot be undone.": "Tem certeza de que deseja excluir permanentemente '{{title}}'? Esta ação não pode ser desfeita.",
"Restore '{{title}}' and its sub-pages?": "Restaurar '{{title}}' e suas subpáginas?",
"Move to trash": "Mover para a lixeira",
"Move this page to trash?": "Mover esta página para a lixeira?",
"Restore page": "Restaurar página",
"Permanently delete": "Excluir permanentemente",
"<b>{{name}}</b> moved this page to Trash {{time}}.": "<b>{{name}}</b> moveu esta página para a Lixeira {{time}}.",
"Page moved to trash": "Página movida para a lixeira",
"Page restored successfully": "Página restaurada com sucesso",
"Deleted by": "Excluído por",
"Deleted at": "Excluído em",
"Preview": "Visualização",
"Subpages": "Subpáginas",
"Failed to load subpages": "Falha ao carregar as subpáginas",
"No subpages": "Nenhuma subpágina",
"Subpages (Child pages)": "Subpáginas (páginas filhas)",
"List all subpages of the current page": "Listar todas as subpáginas da página atual",
"Attachments": "Anexos",
"All spaces": "Todos os espaços",
"Unknown": "Desconhecido",
"Find a space": "Encontrar um espaço",
"Search in all your spaces": "Pesquisar em todos os seus espaços",
"Type": "Tipo",
"Enterprise": "Enterprise",
"Download attachment": "Baixar anexo",
"Allowed email domains": "Domínios de e-mail permitidos",
"Only users with email addresses from these domains can signup via SSO.": "Somente usuários com endereços de e-mail desses domínios podem se cadastrar via SSO.",
"Enter valid domain names separated by comma or space": "Insira nomes de domínio válidos separados por vírgula ou espaço",
"Enforce two-factor authentication": "Exigir autenticação de dois fatores",
"Once enforced, all members must enable two-factor authentication to access the workspace.": "Uma vez imposto, todos os membros devem habilitar a autenticação de dois fatores para acessar o espaço de trabalho.",
"Toggle MFA enforcement": "Alternar exigência de MFA",
"Display name": "Nome de exibição",
"Allow signup": "Permitir cadastro",
"Enabled": "Ativado",
"Advanced Settings": "Configurações avançadas",
"Enable TLS/SSL": "Ativar TLS/SSL",
"Use secure connection to LDAP server": "Usar conexão segura com o servidor LDAP",
"Group sync": "Sincronização de grupos",
"No SSO providers found.": "Nenhum provedor de SSO encontrado.",
"Delete SSO provider": "Excluir provedor de SSO",
"Are you sure you want to delete this SSO provider?": "Tem certeza de que deseja excluir este provedor de SSO?",
"Action": "Ação",
"{{ssoProviderType}} configuration": "Configuração de {{ssoProviderType}}",
"Icon": "Ícone",
"Upload image": "Enviar imagem",
"Remove image": "Remover imagem",
"Failed to remove image": "Falha ao remover imagem",
"Image exceeds 10MB limit.": "A imagem excede o limite de 10MB.",
"Image removed successfully": "Imagem removida com sucesso",
"API key": "Chave API",
"API keys": "Chaves API",
"API management": "Gestão de API",
"Custom expiration date": "Data de expiração personalizada",
"Enter a descriptive token name": "Insira um nome descritivo para o token",
"Expiration": "Expiração",
"Expired": "Expirado",
"Expires": "Expira",
"Last use": "Último uso",
"No API keys found": "Nenhuma chave API encontrada",
"No expiration": "Sem expiração",
"Revoked successfully": "Revogada com sucesso",
"Select expiration date": "Selecionar data de expiração",
"This action cannot be undone. Any applications using this API key will stop working.": "Esta ação não pode ser desfeita. Qualquer aplicação usando esta chave API deixará de funcionar.",
"Update": "Atualizar",
"Update {{credential}}": "Atualizar {{credential}}",
"Manage API keys for all users in the workspace": "Gerenciar chaves API para todos os usuários no espaço de trabalho",
"Restrict API key creation to admins": "Restringir a criação de chave de API aos administradores",
"Only admins and owners can create new API keys. Existing member keys will continue to work.": "Somente administradores e proprietários podem criar novas chaves de API. As chaves de membros já existentes continuarão funcionando.",
"Toggle restrict API keys to admins": "Alternar restrição de chaves de API para administradores",
"API key creation is restricted to admins by your workspace administrator.": "A criação de chaves de API foi restringida aos administradores pelo administrador do seu workspace.",
"AI settings": "Configurações de IA",
"AI search": "Pesquisa IA",
"AI Answer": "Resposta de IA",
"Ask AI": "Pergunte à IA",
"AI is thinking...": "IA está pensando...",
"Thinking": "Pensando",
"Ask a question...": "Faça uma pergunta...",
"AI Answers": "Respostas de IA",
"AI-powered search (AI Answers)": "Pesquisa com IA (Respostas de IA)",
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "A pesquisa IA usa vetores de incorporação para fornecer capacidades de pesquisa semântica em todo o conteúdo do seu espaço de trabalho.",
"Toggle AI search": "Alternar pesquisa de IA",
"Generative AI (Ask AI)": "IA generativa (Perguntar à IA)",
"Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.": "Habilitar geração de conteúdo com IA no editor. Permite aos usuários gerar, melhorar, traduzir e transformar texto.",
"Toggle generative AI": "Alternar IA generativa",
"Upgrade your plan": "Faça upgrade do seu plano",
"Available with a paid license": "Disponível com uma licença paga",
"Upgrade your license tier.": "Faça upgrade do seu nível de licença.",
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "A IA está disponível apenas na edição empresarial do Docmost. Contate sales@docmost.com.",
"AI & MCP": "IA e MCP",
"AI": "IA",
"MCP": "MCP",
"Model Context Protocol (MCP)": "Protocolo de Contexto de Modelo (MCP)",
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Ative o servidor MCP para permitir que assistentes de IA e ferramentas interajam com o conteúdo do seu espaço de trabalho.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "O MCP está disponível apenas na edição empresarial do Docmost. Contate sales@docmost.com.",
"MCP Server URL": "URL do servidor MCP",
"Use your API key for authentication. You can manage API keys in your account settings.": "Use sua chave de API para autenticação. Você pode gerenciar chaves de API nas configurações da sua conta.",
"Supported tools": "Ferramentas compatíveis",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "Seu espaço de trabalho tem MCP habilitado. Use sua chave de API para conectar assistentes de IA.",
"MCP server URL:": "URL do servidor MCP:",
"Learn more": "Saiba mais",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Gerencie as chaves de API de todos os usuários do workspace. Veja a <anchor>documentação da API</anchor> para detalhes de uso.",
"View the <anchor>API documentation</anchor> for usage details.": "Veja a <anchor>documentação da API</anchor> para detalhes de uso.",
"View the <anchor>MCP documentation</anchor>.": "Veja a <anchor>documentação MCP</anchor>.",
"Sources": "Fontes",
"AI Answers not available for attachments": "Respostas de IA não disponíveis para anexos",
"No answer available": "Nenhuma resposta disponível",
"Background color": "Cor de fundo",
"Highlight color": "Cor de destaque",
"Remove color": "Remover cor",
"Notifications": "Notificações",
"No notifications": "Sem notificações",
"No unread notifications": "Sem notificações não lidas",
"All notifications": "Todas as notificações",
"Unread only": "Somente não lidas",
"Mark all as read": "Marcar todas como lidas",
"Mark as read": "Marcar como lida",
"More options": "Mais opções",
"<bold>{{name}}</bold> mentioned you in a comment": "<bold>{{name}}</bold> mencionou você em um comentário",
"<bold>{{name}}</bold> commented on a page": "<bold>{{name}}</bold> comentou em uma página",
"<bold>{{name}}</bold> resolved a comment": "<bold>{{name}}</bold> resolveu um comentário",
"<bold>{{name}}</bold> mentioned you on a page": "<bold>{{name}}</bold> mencionou você em uma página",
"<bold>{{name}}</bold> gave you edit access to a page": "<bold>{{name}}</bold> concedeu a você acesso de edição a uma página",
"<bold>{{name}}</bold> gave you view access to a page": "<bold>{{name}}</bold> concedeu a você acesso de visualização a uma página",
"<bold>{{name}}</bold> updated a page": "<bold>{{name}}</bold> atualizou uma página",
"Watch page": "Acompanhar página",
"Stop watching": "Parar de acompanhar",
"Watch space": "Acompanhar espaço",
"Stop watching space": "Parar de acompanhar espaço",
"Email notifications": "Notificações por e-mail",
"Page updates": "Atualizações da página",
"Get notified when pages you watch are updated.": "Receba notificações quando as páginas que você observa forem atualizadas.",
"Page mentions": "Menções na página",
"Get notified when someone mentions you on a page.": "Receba notificações quando alguém mencionar você em uma página.",
"Comment mentions": "Menções em comentários",
"Get notified when someone mentions you in a comment.": "Receba notificações quando alguém mencionar você em um comentário.",
"New comments": "Novos comentários",
"Get notified about new comments on threads you participate in.": "Receba notificações sobre novos comentários nas discussões em que você participa.",
"Resolved comments": "Comentários resolvidos",
"Get notified when your comment is resolved.": "Receba notificações quando seu comentário for resolvido.",
"You are now watching this page": "Agora você está observando esta página",
"You are no longer watching this page": "Você não está mais observando esta página",
"You are now watching this space": "Agora você está acompanhando este espaço",
"You are no longer watching this space": "Você não está mais acompanhando este espaço",
"Direct": "Direto",
"Updates": "Atualizações",
"Today": "Hoje",
"Yesterday": "Ontem",
"This week": "Esta semana",
"Older": "Mais antigo",
"Restricted page": "Página restrita",
"Restricted pages cannot be shared publicly.": "Páginas restritas não podem ser compartilhadas publicamente.",
"Restricted by parent": "Restrita pela página pai",
"Restricted": "Restrito",
"Open": "Aberto",
"Inherits restrictions from ancestor page": "Herda restrições da página ancestral",
"Only people listed below can access this page": "Apenas as pessoas listadas abaixo podem acessar esta página",
"Everyone in this space can access": "Todos neste espaço podem acessar",
"No additional restrictions on this page": "Sem restrições adicionais nesta página",
"Only specific people can access": "Apenas pessoas específicas podem acessar",
"Use only inherited restrictions": "Usar apenas restrições herdadas",
"Add restrictions on top of inherited": "Adicionar restrições além das herdadas",
"Inherited restriction": "Restrição herdada",
"Access limited by": "Acesso limitado por",
"Restrict access to control who can view and edit this page": "Restringir o acesso para controlar quem pode visualizar e editar esta página",
"Add additional restrictions specific to this page": "Adicionar restrições adicionais específicas para esta página",
"Access": "Acesso",
"People with access": "Pessoas com acesso",
"Remove all": "Remover tudo",
"Remove access": "Remover acesso",
"Remove all access": "Remover todo o acesso",
"Are you sure you want to remove this member's access to the page?": "Tem certeza de que deseja remover o acesso deste membro à página?",
"Are you sure you want to remove all specific access? This will make the page open to everyone in the space.": "Tem certeza de que deseja remover todo o acesso específico? Isso fará com que a página fique aberta para todos no espaço.",
"Trash retention": "Retenção da lixeira",
"Pages in trash will be permanently deleted after this period.": "As páginas na lixeira serão excluídas permanentemente após este período.",
"Trash retention updated": "Retenção da lixeira atualizada",
"Failed to update trash retention": "Falha ao atualizar a retenção da lixeira",
"Removed page restriction": "Restrição de página removida",
"Added page permission": "Permissão de página adicionada",
"Removed page permission": "Permissão de página removida",
"day": "dia",
"days": "dias",
"week": "semana",
"weeks": "semanas",
"month": "mês",
"months": "meses",
"year": "ano",
"years": "anos",
"Period": "Período",
"Fixed date": "Data fixa",
"Indefinitely": "Indefinidamente",
"Days": "Dias",
"Weeks": "Semanas",
"Months": "Meses",
"Years": "Anos",
"Pick a date": "Escolha uma data",
"Maximum is {{max}} {{unit}} for this unit": "O máximo é {{max}} {{unit}} para esta unidade",
"Never expires. Verifiers can re-verify at any time.": "Nunca expira. Os verificadores podem verificar novamente a qualquer momento.",
"Verified": "Verificado",
"Review needed": "Revisão necessária",
"Verification expired": "A verificação expirou",
"Draft": "Rascunho",
"In Approval": "Em aprovação",
"In approval": "Em aprovação",
"Approved": "Aprovado",
"Obsolete": "Obsoleto",
"Expiring": "Expirando",
"Set up verification": "Configurar verificação",
"Verify page": "Verificar página",
"Page verification": "Verificação da página",
"Add verification": "Adicionar verificação",
"Edit verification": "Editar verificação",
"Search by title": "Pesquisar por título",
"Choose how this page should stay accurate.": "Escolha como esta página deve permanecer precisa.",
"Recurring verification": "Verificação recorrente",
"Verifiers re-confirm this page on a schedule.": "Os verificadores confirmam novamente esta página em uma programação definida.",
"Re-verify on a schedule (e.g every 30 days )": "Verificar novamente em uma programação definida (ex.: a cada 30 dias)",
"Page stays editable at all times": "A página permanece editável o tempo todo",
"Best for runbooks, FAQs, living documentation": "Ideal para runbooks, FAQs e documentação viva",
"Approval workflow": "Fluxo de aprovação",
"Formal document lifecycle with named approvers.": "Ciclo de vida formal do documento com aprovadores nomeados.",
"Draft → In approval → Approved → Obsolete": "Rascunho → Em aprovação → Aprovado → Obsoleto",
"Locked once approved, with full history": "Bloqueado após a aprovação, com histórico completo",
"Designed for ISO 9001, ISO 13485, and FDA": "Desenvolvido para ISO 9001, ISO 13485 e FDA",
"Best for SOPs and controlled documents": "Ideal para POPs e documentos controlados",
"Back": "Voltar",
"Quality management": "Gestão da qualidade",
"Recurring": "Recorrente",
"Pages move through draft, approval, and approved stages.": "As páginas passam pelos estágios de rascunho, aprovação e aprovado.",
"Verifiers": "Verificadores",
"Add verifier": "Adicionar verificador",
"I've reviewed this page for accuracy": "Revisei esta página quanto à precisão",
"Set up": "Configurar",
"Remove verification": "Remover verificação",
"Are you sure you want to remove verification from this page?": "Tem certeza de que deseja remover a verificação desta página?",
"Assigned verifiers must periodically re-verify this page.": "Os verificadores atribuídos devem verificar novamente esta página periodicamente.",
"Last verified by {{name}} {{time}} (expired)": "Verificado pela última vez por {{name}} {{time}} (expirado)",
"The fixed expiration date has passed.": "A data fixa de expiração já passou.",
"Verified by {{name}} {{time}}": "Verificado por {{name}} {{time}}",
"Expires {{date}}": "Expira em {{date}}",
"Expired {{date}}": "Expirou em {{date}}",
"Mark as obsolete": "Marcar como obsoleto",
"Mark obsolete": "Marcar como obsoleto",
"Returned by {{name}} {{time}}": "Devolvido por {{name}} {{time}}",
"No approval has been requested yet.": "Nenhuma aprovação foi solicitada ainda.",
"Submitted by {{name}} {{time}}": "Enviado por {{name}} {{time}}",
"Someone": "Alguém",
"Approved by {{name}} {{time}}": "Aprovado por {{name}} {{time}}",
"This document has been marked as obsolete.": "Este documento foi marcado como obsoleto.",
"Rejection comment": "Comentário de rejeição",
"Reason for returning this document...": "Motivo para devolver este documento...",
"Confirm rejection": "Confirmar rejeição",
"Submit for approval": "Enviar para aprovação",
"Reject": "Rejeitar",
"Approve": "Aprovar",
"Re-submit for approval": "Reenviar para aprovação",
"Verified until": "Verificado até",
"QMS": "SGQ",
"Verified pages": "Páginas verificadas",
"Search pages...": "Pesquisar páginas...",
"Filter by space": "Filtrar por espaço",
"Filter by type": "Filtrar por tipo",
"<bold>{{name}}</bold> verified a page": "<bold>{{name}}</bold> verificou uma página",
"<bold>{{name}}</bold> submitted a page for your approval": "<bold>{{name}}</bold> enviou uma página para sua aprovação",
"<bold>{{name}}</bold> returned a page for revision": "<bold>{{name}}</bold> devolveu uma página para revisão",
"Page verification expires soon": "A verificação da página expirará em breve",
"Page verification has expired": "A verificação da página expirou",
"Verifying your email": "Verificando seu e-mail",
"Please wait...": "Por favor, aguarde...",
"Verification failed. The link may have expired.": "Falha na verificação. O link pode ter expirado.",
"Check your email": "Verifique seu e-mail",
"We sent a verification link to {{email}}.": "Enviamos um link de verificação para {{email}}.",
"We sent a verification link to your email.": "Enviamos um link de verificação para seu e-mail.",
"Click the link to verify your email and access your workspace.": "Clique no link para verificar seu e-mail e acessar seu workspace.",
"Resend verification email": "Reenviar e-mail de verificação",
"Verification email sent. Please check your inbox.": "E-mail de verificação enviado. Por favor, verifique sua caixa de entrada.",
"Failed to resend verification email. Please try again.": "Falha ao reenviar o e-mail de verificação. Por favor, tente novamente.",
"We've sent you an email with your associated workspaces.": "Enviamos um e-mail para você com seus workspaces associados.",
"Load more": "Carregar mais",
"Log out of all devices": "Sair de todos os dispositivos",
"Log out of all sessions except this device": "Sair de todas as sessões, exceto deste dispositivo",
"This Device": "Este dispositivo",
"Unknown device": "Dispositivo desconhecido",
"No active sessions": "Nenhuma sessão ativa",
"Session revoked": "Sessão revogada",
"All other sessions revoked": "Todas as outras sessões foram revogadas",
"Last used": "Último uso",
"Created": "Criado",
"Rename": "Renomear",
"Publish": "Publicar",
"Security": "Segurança",
"Enforce SSO": "Exigir SSO",
"Once enforced, members will not be able to login with email and password.": "Depois de exigido, os membros não poderão fazer login com e-mail e senha.",
"AI-generated content may not be accurate.": "O conteúdo gerado por IA pode não ser preciso.",
"AI Chat": "Chat com IA",
"Analyze for insights": "Analisar para obter insights",
"Ask anything...": "Pergunte qualquer coisa...",
"Assistant said:": "O assistente disse:",
"Chat history": "Histórico de chats",
"Chat name": "Nome do chat",
"Chat transcript": "Transcrição do chat",
"Close": "Fechar",
"Copy assistant response": "Copiar resposta do assistente",
"Docmost AI": "Docmost AI",
"Failed to load chat. An error occurred.": "Falha ao carregar o chat. Ocorreu um erro.",
"Failed to render this message.": "Falha ao renderizar esta mensagem.",
"How can I help you today?": "Como posso ajudar você hoje?",
"New chat": "Novo chat",
"No chat history": "Nenhum histórico de chat",
"No chats found": "Nenhum chat encontrado",
"No conversations yet": "Ainda não há conversas",
"Open full page": "Abrir página inteira",
"Scroll to bottom": "Rolar até o fim",
"You said:": "Você disse:",
"Previous 7 days": "Últimos 7 dias",
"Previous 30 days": "Últimos 30 dias",
"Search chats...": "Pesquisar chats...",
"Search chats": "Pesquisar chats",
"Ask anything... Use @ to mention pages": "Pergunte qualquer coisa... Use @ para mencionar páginas",
"Ask anything or search your workspace": "Pergunte qualquer coisa ou pesquise no seu workspace",
"Welcome to {{name}}": "Boas-vindas a {{name}}",
"Add files": "Adicionar arquivos",
"Mention a page": "Mencionar uma página",
"Start a new chat to see it here.": "Inicie um novo chat para vê-lo aqui.",
"Summarize this page": "Resumir esta página",
"Toggle AI Chat": "Alternar chat com IA",
"Translate this page": "Traduzir esta página",
"Try a different search term.": "Tente um termo de pesquisa diferente.",
"Try again": "Tentar novamente",
"Untitled chat": "Chat sem título",
"What can I help you with?": "Com o que posso ajudar você?",
"Are you sure you want to revoke this {{credential}}": "Tem certeza de que deseja revogar esta {{credential}}",
"Automatically provision users and groups from your identity provider via SCIM.": "Provisione automaticamente usuários e grupos do seu provedor de identidade via SCIM.",
"Configure your identity provider with this URL to provision users and groups.": "Configure seu provedor de identidade com esta URL para provisionar usuários e grupos.",
"Create {{credential}}": "Criar {{credential}}",
"{{credential}} created": "{{credential}} criada",
"{{credential}} created successfully": "{{credential}} criada com sucesso",
"Created by": "Criado por",
"Custom": "Personalizado",
"Enable SCIM": "Ativar SCIM",
"Enter a descriptive name": "Insira um nome descritivo",
"I've saved my {{credential}}": "Salvei minha {{credential}}",
"Important": "Importante",
"Make sure to copy your {{credential}} now. You won't be able to see it again!": "Copie sua {{credential}} agora. Você não poderá vê-la novamente!",
"Never": "Nunca",
"Revoke {{credential}}": "Revogar {{credential}}",
"SCIM endpoint URL": "URL do endpoint SCIM",
"SCIM provisioning": "Provisionamento SCIM",
"SCIM takes precedence over SSO group sync while enabled.": "O SCIM tem precedência sobre a sincronização de grupos por SSO enquanto estiver ativado.",
"You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "Você atingiu o máximo de {{max}} tokens SCIM. Exclua um token existente para criar um novo.",
"SCIM token": "Token SCIM",
"SCIM tokens": "Tokens SCIM",
"This action cannot be undone. Your identity provider will stop syncing immediately.": "Esta ação não pode ser desfeita. Seu provedor de identidade deixará de sincronizar imediatamente.",
"Toggle SCIM provisioning": "Alternar provisionamento SCIM",
"Token": "Token",
"Page menu": "Menu da página",
"Expand": "Expandir",
"Collapse": "Recolher",
"Comment menu": "Menu de comentários",
"Group menu": "Menu do grupo",
"Show hidden breadcrumbs": "Mostrar breadcrumbs ocultos",
"Breadcrumbs": "Trilhas de navegação",
"Page actions": "Ações da página",
"Pick emoji": "Escolher emoji",
"Template menu": "Menu do modelo",
"Use": "Usar",
"Use template": "Usar modelo",
"Preview template: {{title}}": "Visualizar modelo: {{title}}",
"Use a template": "Usar um modelo",
"Search templates...": "Pesquisar modelos...",
"Search spaces...": "Pesquisar espaços...",
"No templates found": "Nenhum modelo encontrado",
"No spaces found": "Nenhum espaço encontrado",
"Browse all templates": "Ver todos os modelos",
"This space": "Este espaço",
"All templates": "Todos os modelos",
"Global": "Global",
"New template": "Novo modelo",
"Edit template": "Editar modelo",
"Are you sure you want to delete this template?": "Tem certeza de que deseja excluir este modelo?",
"Template scope updated": "Escopo do modelo atualizado",
"Choose which space this template belongs to": "Escolha a qual espaço este modelo pertence",
"Scope": "Escopo",
"Select scope": "Selecionar escopo",
"Title": "Título",
"Saving...": "Salvando...",
"Saved": "Salvo",
"Save failed. Retry": "Falha ao salvar. Tentar novamente",
"By {{name}}": "Por {{name}}",
"Updated {{time}}": "Atualizado {{time}}",
"Choose destination": "Escolher destino",
"Search pages and spaces...": "Pesquisar páginas e espaços...",
"No results found": "Nenhum resultado encontrado",
"You don't have permission to create pages here": "Você não tem permissão para criar páginas aqui",
"Chat menu": "Menu do chat",
"API key menu": "Menu da chave de API",
"Jump to comment selection": "Ir para a seleção de comentários",
"Slash commands": "Comandos de barra",
"Mention suggestions": "Sugestões de menção",
"Link suggestions": "Sugestões de links",
"Diagram editor": "Editor de diagramas",
"Add comment": "Adicionar comentário",
"Find and replace": "Localizar e substituir",
"Main navigation": "Navegação principal",
"Space navigation": "Navegação do espaço",
"Settings navigation": "Navegação de configurações",
"AI navigation": "Navegação de IA",
"Breadcrumb": "Trilha de navegação",
"Synced block": "Bloco sincronizado",
"Create a block that stays in sync across pages.": "Crie um bloco que permaneça sincronizado entre páginas.",
"Editing original": "Editando original",
"Copy synced block": "Copiar bloco sincronizado",
"Unsync": "Desfazer sincronização",
"Delete synced block": "Excluir bloco sincronizado",
"Synced to {{count}} other page_one": "Synced to {{count}} other page",
"Synced to {{count}} other page_other": "Synced to {{count}} other pages",
"ORIGINAL": "ORIGINAL",
"THIS PAGE": "ESTA PÁGINA",
"No pages": "Nenhuma página",
"The original synced block no longer exists": "O bloco sincronizado original não existe mais",
"You don't have access to this synced block": "Você não tem acesso a este bloco sincronizado",
"Failed to load this synced block": "Falha ao carregar este bloco sincronizado",
"Fixed editor toolbar": "Barra de ferramentas fixa do editor",
"Show a formatting toolbar above the editor with quick access to common actions.": "Mostre uma barra de ferramentas de formatação acima do editor com acesso rápido a ações comuns.",
"Toggle fixed editor toolbar": "Alternar barra de ferramentas fixa do editor",
"Normal text": "Texto normal",
"More inline formatting": "Mais formatação em linha",
"Subscript": "Subscrito",
"Superscript": "Sobrescrito",
"Inline code": "Código em linha",
"Insert media": "Inserir mídia",
"Mention": "Menção",
"Emoji": "Emoji",
"Columns": "Colunas",
"More inserts": "Mais inserções",
"Embeds": "Incorporações",
"Diagrams": "Diagramas",
"Advanced": "Avançado",
"Utility": "Utilitário",
"Decrease indent": "Diminuir recuo",
"Increase indent": "Aumentar recuo",
"Clear formatting": "Limpar formatação",
"Code block": "Bloco de código",
"Experimental": "Experimental",
"Strikethrough": "Tachado",
"Undo": "Desfazer",
"Redo": "Refazer",
"Backlinks": "Links de retorno",
"Last updated by": "Última atualização por",
"Last updated": "Última atualização",
"Stats": "Estatísticas",
"Word count": "Contagem de palavras",
"Characters": "Caracteres",
"Incoming links": "Links recebidos",
"Outgoing links": "Links de saída",
"Incoming links ({{count}})": "Links recebidos ({{count}})",
"Outgoing links ({{count}})": "Links de saída ({{count}})",
"No pages link here yet.": "Nenhuma página tem link para cá ainda.",
"This page doesn't link to other pages yet.": "Esta página ainda não tem links para outras páginas.",
"Verified until {{date}}": "Verificado até {{date}}",
"Labels": "Rótulos",
"Add label": "Adicionar rótulo",
"No labels yet": "Ainda não há rótulos",
"Already added": "Já adicionado",
"Invalid label name": "Nome de rótulo inválido",
"No matches": "Sem correspondências",
"Search or create…": "Pesquisar ou criar…",
"Remove label {{name}}": "Remover rótulo {{name}}",
"Failed to add label": "Falha ao adicionar rótulo",
"Failed to remove label": "Falha ao remover rótulo",
"No pages with this label": "Nenhuma página com este rótulo",
"Pages tagged with this label will appear here.": "As páginas marcadas com este rótulo aparecerão aqui.",
"No pages match your search.": "Nenhuma página corresponde à sua pesquisa.",
"Updated {{date}}": "Atualizado em {{date}}",
"Cell actions": "Ações da célula",
"Column actions": "Ações da coluna",
"Row actions": "Ações da linha",
"Filter": "Filtrar",
"Page title": "Título da página",
"Page content": "Conteúdo da página",
"Member actions": "Ações do membro",
"Toggle password visibility": "Alternar visibilidade da senha",
"Send comment": "Enviar comentário",
"Token actions": "Ações do token",
"Template settings": "Configurações do modelo",
"Edit diagram": "Editar diagrama",
"Edit embed": "Editar incorporação",
"Edit drawing": "Editar desenho",
"Delete equation": "Excluir equação",
"Invite actions": "Ações do convite",
"Get started": "Começar",
"* indicates required fields": "* indica campos obrigatórios",
"List of spaces in this workspace": "Lista de espaços neste workspace",
"Active sessions": "Sessões ativas",
"Add {{name}} to favorites": "Adicionar {{name}} aos favoritos",
"Remove {{name}} from favorites": "Remover {{name}} dos favoritos",
"Added to favorites": "Adicionado aos favoritos",
"Removed from favorites": "Removido dos favoritos",
"Added {{name}} to favorites": "{{name}} adicionado aos favoritos",
"Removed {{name}} from favorites": "{{name}} removido dos favoritos",
"Page menu for {{name}}": "Menu da página de {{name}}",
"Create subpage of {{name}}": "Criar subpágina de {{name}}"
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+758 -36
View File
@@ -7,6 +7,7 @@
"Add members": "添加成员", "Add members": "添加成员",
"Add to groups": "添加到群组", "Add to groups": "添加到群组",
"Add space members": "添加空间成员", "Add space members": "添加空间成员",
"Add to favorites": "添加到收藏",
"Admin": "管理员", "Admin": "管理员",
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "您确定要删除这个群组吗?成员将失去对该群组可访问资源的访问权限。", "Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "您确定要删除这个群组吗?成员将失去对该群组可访问资源的访问权限。",
"Are you sure you want to delete this page?": "您确定要删除这个页面吗?", "Are you sure you want to delete this page?": "您确定要删除这个页面吗?",
@@ -29,6 +30,7 @@
"Choose your preferred interface language.": "选择您喜欢的界面语言。", "Choose your preferred interface language.": "选择您喜欢的界面语言。",
"Choose your preferred page width.": "选择您喜欢的页面宽度。", "Choose your preferred page width.": "选择您喜欢的页面宽度。",
"Confirm": "确认", "Confirm": "确认",
"Copy as Markdown": "复制为Markdown",
"Copy link": "复制链接", "Copy link": "复制链接",
"Create": "创建", "Create": "创建",
"Create group": "创建群组", "Create group": "创建群组",
@@ -43,23 +45,24 @@
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "您确定要删除这个页面吗?这将删除其子页面和页面历史记录。此操作不可逆。", "Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "您确定要删除这个页面吗?这将删除其子页面和页面历史记录。此操作不可逆。",
"Description": "描述", "Description": "描述",
"Details": "详情", "Details": "详情",
"e.g ACME": "例如ACME", "e.g ACME": "例如 ACME",
"e.g ACME Inc": "例如ACME Inc", "e.g ACME Inc": "例如 ACME Inc",
"e.g Developers": "例如:开发人员", "e.g Developers": "例如 Developers",
"e.g Group for developers": "例如开发人员群组", "e.g Group for developers": "例如 开发者小组",
"e.g product": "例如product", "e.g product": "例如 product",
"e.g Product Team": "例如产品团队", "e.g Product Team": "例如 产品团队",
"e.g Sales": "例如销售", "e.g Sales": "例如 销售",
"e.g Space for product team": "例如产品团队空间", "e.g Space for product team": "例如 产品团队空间",
"e.g Space for sales team to collaborate": "例如销售团队协作的空间", "e.g Space for sales team to collaborate": "例如销售团队协作的空间",
"Edit": "编辑", "Edit": "编辑",
"Read": "读取",
"Edit group": "编辑群组", "Edit group": "编辑群组",
"Email": "电子邮箱", "Email": "电子邮箱",
"Enter a strong password": "输入一个强密码", "Enter a strong password": "输入一个强密码",
"Enter valid email addresses separated by comma or space max_50": "输入有效的电子邮箱地址,用逗号或空格分隔 [最多:50个]", "Enter valid email addresses separated by comma or space max_50": "输入有效的电子邮箱地址,用逗号或空格分隔 [最多:50个]",
"enter valid emails addresses": "输入有效的电子邮箱地址", "enter valid emails addresses": "输入有效的电子邮箱地址",
"Enter your current password": "输入您的当前密码", "Enter your current password": "输入您的当前密码",
"enter your full name": "输入您的全名", "enter your full name": "输入您的全名",
"Enter your new password": "输入您的新密码", "Enter your new password": "输入您的新密码",
"Enter your new preferred email": "输入您新的首选电子邮箱", "Enter your new preferred email": "输入您新的首选电子邮箱",
"Enter your password": "输入您的密码", "Enter your password": "输入您的密码",
@@ -68,10 +71,14 @@
"Export": "导出", "Export": "导出",
"Failed to create page": "创建页面失败", "Failed to create page": "创建页面失败",
"Failed to delete page": "删除页面失败", "Failed to delete page": "删除页面失败",
"Failed to restore page": "恢复页面失败",
"Failed to fetch recent pages": "获取最近页面失败", "Failed to fetch recent pages": "获取最近页面失败",
"Failed to import pages": "导入页面失败", "Failed to import pages": "导入页面失败",
"Failed to load page. An error occurred.": "页面加载失败。发生了一个错误。", "Failed to load page. An error occurred.": "页面加载失败。发生了一个错误。",
"Failed to update data": "数据更新失败", "Failed to update data": "数据更新失败",
"Favorite spaces": "收藏的空间",
"Favorite spaces appear here": "收藏的空间会显示在这里",
"Favorites": "收藏",
"Full access": "完全访问", "Full access": "完全访问",
"Full page width": "全页宽度", "Full page width": "全页宽度",
"Full width": "全宽", "Full width": "全宽",
@@ -90,6 +97,7 @@
"Invite by email": "通过电子邮箱邀请", "Invite by email": "通过电子邮箱邀请",
"Invite members": "邀请成员", "Invite members": "邀请成员",
"Invite new members": "邀请新成员", "Invite new members": "邀请新成员",
"Invite People": "邀请成员",
"Invited members who are yet to accept their invitation will appear here.": "尚未接受邀请的成员将显示在这里。", "Invited members who are yet to accept their invitation will appear here.": "尚未接受邀请的成员将显示在这里。",
"Invited members will be granted access to spaces the groups can access": "被邀请的成员将被授予访问群组可以访问的空间的权限", "Invited members will be granted access to spaces the groups can access": "被邀请的成员将被授予访问群组可以访问的空间的权限",
"Join the workspace": "加入工作空间", "Join the workspace": "加入工作空间",
@@ -114,6 +122,7 @@
"No group found": "未找到群组", "No group found": "未找到群组",
"No page history saved yet.": "尚未保存页面历史。", "No page history saved yet.": "尚未保存页面历史。",
"No pages yet": "暂无页面", "No pages yet": "暂无页面",
"No shared pages": "没有共享页面",
"No results found...": "未找到结果...", "No results found...": "未找到结果...",
"No user found": "未找到用户", "No user found": "未找到用户",
"Overview": "概览", "Overview": "概览",
@@ -121,11 +130,14 @@
"page": "个页面", "page": "个页面",
"Page deleted successfully": "页面已成功删除", "Page deleted successfully": "页面已成功删除",
"Page history": "页面历史", "Page history": "页面历史",
"Select version": "选择版本",
"Highlight changes": "突出显示更改",
"Page import is in progress. Please do not close this tab.": "页面导入正在进行中。请不要关闭此标签页。", "Page import is in progress. Please do not close this tab.": "页面导入正在进行中。请不要关闭此标签页。",
"Pages": "页面", "Pages": "页面",
"pages": "个页面", "pages": "个页面",
"Password": "密码", "Password": "密码",
"Password changed successfully": "密码更改成功", "Password changed successfully": "密码更改成功",
"People": "人员",
"Pending": "待定", "Pending": "待定",
"Please confirm your action": "请确认您的操作", "Please confirm your action": "请确认您的操作",
"Preferences": "偏好设置", "Preferences": "偏好设置",
@@ -133,6 +145,7 @@
"Profile": "个人资料", "Profile": "个人资料",
"Recently updated": "最近更新", "Recently updated": "最近更新",
"Remove": "移除", "Remove": "移除",
"Remove from favorites": "从收藏中移除",
"Remove group member": "移除群组成员", "Remove group member": "移除群组成员",
"Remove space member": "移除空间成员", "Remove space member": "移除空间成员",
"Restore": "恢复", "Restore": "恢复",
@@ -145,53 +158,54 @@
"Search...": "搜索...", "Search...": "搜索...",
"Select language": "选择语言", "Select language": "选择语言",
"Select role": "选择角色", "Select role": "选择角色",
"Select role to assign to all invited members": "选择要分配给所有被邀请成员的角色", "Select role to assign to all invited members": "选择要分配给所有受邀成员的角色",
"Select theme": "选择主题", "Select theme": "选择主题",
"Send invitation": "发送邀请", "Send invitation": "发送邀请",
"Invitation sent": "邀请邮件已发送", "Invitation sent": "邀请已发送",
"Settings": "设置", "Settings": "设置",
"Setup workspace": "设置工作空间", "Setup workspace": "设置工作",
"Sign In": "登录", "Sign In": "登录",
"Sign Up": "注册", "Sign Up": "注册",
"Slug": "短链接", "Slug": "标识符",
"Space": "空间", "Space": "空间",
"Space description": "空间描述", "Space description": "空间描述",
"Space menu": "空间菜单", "Space menu": "空间菜单",
"Space name": "空间名称", "Space name": "空间名称",
"Space settings": "空间设置", "Space settings": "空间设置",
"Space slug": "空间短链接", "Space slug": "空间标识符",
"Spaces": "空间", "Spaces": "空间",
"Spaces you belong to": "您所属的空间", "Spaces you belong to": "您所属的空间",
"No space found": "未找到空间", "No space found": "未找到空间",
"Search for spaces": "搜索空间", "Search for spaces": "搜索空间",
"Start typing to search...": "开始输入以搜索...", "Start typing to search...": "开始输入以搜索...",
"Status": "状态", "Status": "状态",
"Successfully imported": "成功导入", "Successfully imported": "导入成功",
"Successfully restored": "恢复成功", "Successfully restored": "恢复成功",
"System settings": "系统设置", "System settings": "系统设置",
"Templates": "模板",
"Theme": "主题", "Theme": "主题",
"To change your email, you have to enter your password and new email.": "要更改您的电子邮箱,您需要输入密码和新的电子邮箱地址。", "To change your email, you have to enter your password and new email.": "要更改您的电子邮箱,您需要输入密码和新的电子邮箱地址。",
"Toggle full page width": "切换页宽度", "Toggle full page width": "切换页宽度",
"Unable to import pages. Please try again.": "无法导入页面。请重试。", "Unable to import pages. Please try again.": "无法导入页面。请重试。",
"untitled": "无标题", "untitled": "未命名",
"Untitled": "无标题", "Untitled": "未命名",
"Updated successfully": "更新成功", "Updated successfully": "更新成功",
"User": "用户", "User": "用户",
"Workspace": "工作区", "Workspace": "工作区",
"Workspace Name": "工作空间名称", "Workspace Name": "工作名称",
"Workspace settings": "工作区设置", "Workspace settings": "工作区设置",
"You can change your password here.": "您可以在这里更改密码。", "You can change your password here.": "您可以在这里更改密码。",
"Your Email": "您的电子邮箱", "Your Email": "您的邮箱",
"Your import is complete.": "导入已完成。", "Your import is complete.": "导入已完成。",
"Your name": "您的姓名", "Your name": "您的姓名",
"Your Name": "您的姓名", "Your Name": "您的姓名",
"Your password": "您的密码", "Your password": "您的密码",
"Your password must be a minimum of 8 characters.": "您的密码必须至少包含8个字符。", "Your password must be a minimum of 8 characters.": "您的密码必须至少包含8个字符。",
"Sidebar toggle": "切换侧边栏", "Sidebar toggle": "侧边栏切换",
"Comments": "评论", "Comments": "评论",
"404 page not found": "404 页面未找到", "404 page not found": "404 页面未找到",
"Sorry, we can't find the page you are looking for.": "抱歉,我们无法找到你所需要的页面", "Sorry, we can't find the page you are looking for.": "抱歉,我们无法找到你所需要的页面",
"Take me back to homepage": "回到主页", "Take me back to homepage": "返回首页",
"Forgot password": "忘记密码", "Forgot password": "忘记密码",
"Forgot your password?": "忘记密码了吗?", "Forgot your password?": "忘记密码了吗?",
"A password reset link has been sent to your email. Please check your inbox.": "密码重置链接已经发送到您的邮箱,请检查收件箱", "A password reset link has been sent to your email. Please check your inbox.": "密码重置链接已经发送到您的邮箱,请检查收件箱",
@@ -203,9 +217,14 @@
"Reply...": "回复...", "Reply...": "回复...",
"Error loading comments.": "加载评论时出错", "Error loading comments.": "加载评论时出错",
"No comments yet.": "目前还没有评论", "No comments yet.": "目前还没有评论",
"No open comments.": "没有未解决的评论。",
"No resolved comments.": "没有已解决的评论。",
"Add a comment...": "添加评论...",
"Edit comment": "编辑评论", "Edit comment": "编辑评论",
"Delete comment": "删除评论", "Delete comment": "删除评论",
"Are you sure you want to delete this comment?": "你确定要删除这条评论吗?", "Are you sure you want to delete this comment?": "你确定要删除这条评论吗?",
"Delete chat": "删除聊天",
"Are you sure you want to delete '{{title}}'? This action cannot be undone.": "您确定要删除「{{title}}」吗?此操作无法撤销。",
"Comment created successfully": "成功创建评论", "Comment created successfully": "成功创建评论",
"Error creating comment": "创建评论时出错", "Error creating comment": "创建评论时出错",
"Comment updated successfully": "评论更新成功", "Comment updated successfully": "评论更新成功",
@@ -213,7 +232,17 @@
"Comment deleted successfully": "成功删除评论", "Comment deleted successfully": "成功删除评论",
"Failed to delete comment": "删除评论失败", "Failed to delete comment": "删除评论失败",
"Comment resolved successfully": "成功标记评论为解决", "Comment resolved successfully": "成功标记评论为解决",
"Comment re-opened successfully": "评论重新打开成功",
"Comment unresolved successfully": "评论已成功取消解决",
"Failed to resolve comment": "标记评论为解决失败", "Failed to resolve comment": "标记评论为解决失败",
"Resolve comment": "解决评论",
"Unresolve comment": "取消解决评论",
"Resolve Comment Thread": "解决评论线程",
"Unresolve Comment Thread": "取消解决评论线程",
"Are you sure you want to resolve this comment thread? This will mark it as completed.": "确定要解决此评论线程吗?这将标记为已完成。",
"Are you sure you want to unresolve this comment thread?": "确定要取消解决此评论线程吗?",
"Resolved": "已解决",
"No active comments.": "没有活跃的评论。",
"Revoke invitation": "撤回邀请", "Revoke invitation": "撤回邀请",
"Revoke": "撤销", "Revoke": "撤销",
"Don't": "不要", "Don't": "不要",
@@ -222,7 +251,9 @@
"Anyone with this link can join this workspace.": "任何拥有此连接的人都可以加入此工作区", "Anyone with this link can join this workspace.": "任何拥有此连接的人都可以加入此工作区",
"Invite link": "邀请链接", "Invite link": "邀请链接",
"Copy": "复制", "Copy": "复制",
"Copy to space": "复制到空间",
"Copied": "已复制", "Copied": "已复制",
"Duplicate": "复制",
"Select a user": "选择一个用户", "Select a user": "选择一个用户",
"Select a group": "选择一个组", "Select a group": "选择一个组",
"Export all pages and attachments in this space.": "导出当前空间的所有页面和附件", "Export all pages and attachments in this space.": "导出当前空间的所有页面和附件",
@@ -239,12 +270,16 @@
"Export failed:": "导出失败:", "Export failed:": "导出失败:",
"export error": "导出出错", "export error": "导出出错",
"Export page": "导出页面", "Export page": "导出页面",
"Export successful": "导出成功",
"Export space": "导出空间", "Export space": "导出空间",
"Export {{type}}": "导出为 {{type}}", "Export {{type}}": "导出为 {{type}}",
"File exceeds the {{limit}} attachment limit": "文件超出了 {{limit}} 类型附件限制", "File exceeds the {{limit}} attachment limit": "文件超出了 {{limit}} 类型附件限制",
"Align left": "靠左对齐", "Align left": "靠左对齐",
"Align right": "靠右对齐", "Align right": "靠右对齐",
"Align center": "居中对齐", "Align center": "居中对齐",
"Alt text": "替代文本",
"Describe this for accessibility.": "为无障碍访问添加描述。",
"Add a description": "添加描述",
"Justify": "两端对齐", "Justify": "两端对齐",
"Merge cells": "合并单元格", "Merge cells": "合并单元格",
"Split cell": "分割单元格", "Split cell": "分割单元格",
@@ -255,7 +290,21 @@
"Add row above": "在上方添加行", "Add row above": "在上方添加行",
"Add row below": "在下方插入行", "Add row below": "在下方插入行",
"Delete table": "删除表格", "Delete table": "删除表格",
"Add column left": "在左侧添加列",
"Add column right": "在右侧添加列",
"Clear cell": "清空单元格",
"Clear cells": "清空单元格",
"Toggle header cell": "切换标题单元格",
"Toggle header column": "切换标题列",
"Toggle header row": "切换标题行",
"Move column left": "左移列",
"Move column right": "右移列",
"Move row down": "下移行",
"Move row up": "上移行",
"Sort A → Z": "按 A → Z 排序",
"Sort Z → A": "按 Z → A 排序",
"Info": "信息", "Info": "信息",
"Note": "注意",
"Success": "成功", "Success": "成功",
"Warning": "警告", "Warning": "警告",
"Danger": "危险", "Danger": "危险",
@@ -266,6 +315,11 @@
"Save & Exit": "保存并退出", "Save & Exit": "保存并退出",
"Double-click to edit Excalidraw diagram": "双击以编辑 Excalidraw 图表", "Double-click to edit Excalidraw diagram": "双击以编辑 Excalidraw 图表",
"Paste link": "粘贴链接", "Paste link": "粘贴链接",
"Paste link or search pages": "粘贴链接或搜索页面",
"Link to web page": "链接到网页",
"Recents": "最近使用",
"Page or URL": "页面或网址",
"Link title": "链接标题",
"Edit link": "编辑链接", "Edit link": "编辑链接",
"Remove link": "移除链接", "Remove link": "移除链接",
"Add link": "添加链接", "Add link": "添加链接",
@@ -298,7 +352,7 @@
"Heading 2": "2 级标题", "Heading 2": "2 级标题",
"Heading 3": "3 级标题", "Heading 3": "3 级标题",
"To-do List": "代办列表", "To-do List": "代办列表",
"Bullet List": "无列表", "Bullet List": "无列表",
"Numbered List": "有序列表", "Numbered List": "有序列表",
"Blockquote": "引用块", "Blockquote": "引用块",
"Just start typing with plain text.": "只需开始键入纯文本", "Just start typing with plain text.": "只需开始键入纯文本",
@@ -311,19 +365,30 @@
"Create block quote.": "创建引用块", "Create block quote.": "创建引用块",
"Insert code snippet.": "插入代码片段", "Insert code snippet.": "插入代码片段",
"Insert horizontal rule divider": "插入水平分割线", "Insert horizontal rule divider": "插入水平分割线",
"Page break": "分页符",
"Insert a page break for printing.": "插入一个用于打印的分页符。",
"Upload any image from your device.": "从设备上传任何图像", "Upload any image from your device.": "从设备上传任何图像",
"Upload any video from your device.": "从设备上传任何视频", "Upload any video from your device.": "从设备上传任何视频",
"Upload any audio from your device.": "从您的设备上传任意音频文件。",
"Upload any file from your device.": "从设备上传任何文件", "Upload any file from your device.": "从设备上传任何文件",
"Uploading {{name}}": "正在上传{{name}}",
"Uploading file": "正在上传文件",
"Table": "表格", "Table": "表格",
"Insert a table.": "插入一个表格", "Insert a table.": "插入一个表格",
"Insert collapsible block.": "插入一个折叠块", "Insert collapsible block.": "插入一个折叠块",
"Video": "视频", "Video": "视频",
"Divider": "分割线", "Divider": "分割线",
"Quote": "引用", "Quote": "引用",
"Image": "图", "Image": "图",
"Audio": "音频",
"Embed PDF": "嵌入 PDF",
"Upload and embed a PDF file.": "上传并嵌入 PDF 文件。",
"Embed as PDF": "作为 PDF 嵌入",
"Failed to load PDF": "加载 PDF 失败",
"Convert to attachment": "转换为附件",
"File attachment": "文件附件", "File attachment": "文件附件",
"Toggle block": "切换块", "Toggle block": "折叠块",
"Callout": "标注块", "Callout": "提示块",
"Insert callout notice.": "插入标注提示块", "Insert callout notice.": "插入标注提示块",
"Math inline": "行内公式", "Math inline": "行内公式",
"Insert inline math equation.": "插入行内公式", "Insert inline math equation.": "插入行内公式",
@@ -331,19 +396,37 @@
"Insert math equation": "插入数学公式", "Insert math equation": "插入数学公式",
"Mermaid diagram": "Mermaid 图表", "Mermaid diagram": "Mermaid 图表",
"Insert mermaid diagram": "插入 Mermaid 图表", "Insert mermaid diagram": "插入 Mermaid 图表",
"Insert and design Drawio diagrams": "插入并设计 Draw.io 图表", "Insert and design Drawio diagrams": "插入并设计 Drawio 图表",
"Insert current date": "插入当前日期", "Insert current date": "插入当前日期",
"Draw and sketch excalidraw diagrams": "绘制 Excalidraw 图表", "Draw and sketch excalidraw diagrams": "绘制和草绘 Excalidraw 图表",
"Multiple": "多个", "Multiple": "多个",
"Heading {{level}}": "{{level}} 级标题", "Turn into": "变成",
"Toggle title": "切换标题", "Text align": "文本对齐",
"Write anything. Enter \"/\" for commands": "开始编写内容,输入 \"/\" 以使用指令", "This page may have been deleted, moved, or you may not have access.": "此页面可能已被删除、移动,或者您可能无权访问。{",
"Go to homepage": "前往首页",
"Pages you create will show up here.": "您创建的页面将显示在此处。",
"Heading {{level}}": "标题 {{level}}",
"Toggle title": "折叠标题",
"Write anything. Enter \"/\" for commands": "输入任意内容。输入“/”查看命令",
"Write...": "写点内容...",
"Column count": "列数",
"{{count}} Columns": "{{count}} 列",
"{{count}} command available_one": "有 1 个可用命令",
"{{count}} command available_other": "有 {{count}} 个可用命令",
"{{count}} result available_one": "有 1 个可用结果",
"{{count}} result available_other": "有 {{count}} 个可用结果",
"Equal columns": "等宽列",
"Left sidebar": "左侧边栏",
"Right sidebar": "右侧边栏",
"Wide center": "中间加宽",
"Left wide": "左侧加宽",
"Right wide": "右侧加宽",
"Names do not match": "名称不匹配", "Names do not match": "名称不匹配",
"Today, {{time}}": "今天,{{time}}", "Today, {{time}}": "今天,{{time}}",
"Yesterday, {{time}}": "昨天,{{time}}", "Yesterday, {{time}}": "昨天,{{time}}",
"Space created successfully": "空间创建成功", "Space created successfully": "空间创建成功",
"Space updated successfully": "空间更新成功", "Space updated successfully": "空间更新成功",
"Space deleted successfully": "空间已成功删除", "Space deleted successfully": "空间删除成功",
"Members added successfully": "成员添加成功", "Members added successfully": "成员添加成功",
"Member removed successfully": "成员移除成功", "Member removed successfully": "成员移除成功",
"Member role updated successfully": "成员角色更新成功", "Member role updated successfully": "成员角色更新成功",
@@ -353,14 +436,653 @@
"Word count: {{wordCount}}": "字数:{{wordCount}}", "Word count: {{wordCount}}": "字数:{{wordCount}}",
"Character count: {{characterCount}}": "字符数:{{characterCount}}", "Character count: {{characterCount}}": "字符数:{{characterCount}}",
"New update": "新更新", "New update": "新更新",
"{{latestVersion}} is available": "{{latestVersion}} 已经可以使用", "{{latestVersion}} is available": "{{latestVersion}} 用",
"Default page edit mode": "默认页面编辑模式",
"Choose your preferred page edit mode. Avoid accidental edits.": "选择您偏好的页面编辑模式。避免意外编辑。",
"Choose {{format}} file": "选择 {{format}} 文件",
"Reading": "阅读",
"Delete member": "删除成员", "Delete member": "删除成员",
"Member deleted successfully": "成员删除成功", "Member deleted successfully": "成员删除成功",
"Are you sure you want to delete this workspace member? This action is irreversible.": "您确定要删除此工作区成员吗?此操作不可逆。", "Are you sure you want to delete this workspace member? This action is irreversible.": "您确定要删除此工作区成员吗?此操作不可逆。",
"Deactivate member": "停用成员",
"Activate member": "激活成员",
"Are you sure you want to deactivate this workspace member? They will no longer be able to access this workspace.": "您确定要停用此工作区成员吗?该成员将无法再访问此工作区。",
"Are you sure you want to activate this workspace member?": "您确定要激活此工作区成员吗?",
"Deactivate": "停用",
"Activate": "激活",
"Deactivated": "已停用",
"Move": "移动", "Move": "移动",
"Move page": "移动页面", "Move page": "移动页面",
"Move page to a different space.": "将页面移动到不同的空间。", "Move page to a different space.": "将页面移动到不同的空间。",
"Real-time editor connection lost. Retrying...": "实时编辑器连接丢失。重试中……", "Real-time editor connection lost. Retrying...": "实时编辑器连接丢失。重试中……",
"Table of contents": "目录", "Table of contents": "目录",
"Add headings (H1, H2, H3) to generate a table of contents.": "添加标题(H1H2H3)以生成目录。" "Add headings (H1, H2, H3) to generate a table of contents.": "添加标题(H1H2H3)以生成目录。",
"Share": "分享",
"Public sharing": "公开分享",
"Shared by": "分享者",
"Shared at": "分享于",
"Inherits public sharing from": "继承公开分享自",
"Share to web": "分享到网页",
"Shared to web": "已分享到网页",
"Anyone with the link can view this page": "任何拥有链接的人都可以查看此页面",
"Make this page publicly accessible": "将此页面设为公开可访问",
"Include sub-pages": "包含子页面",
"Make sub-pages public too": "同时将子页面设为公开",
"Allow search engines to index page": "允许搜索引擎索引页面",
"Open page": "打开页面",
"Page": "页面",
"Delete public share link": "删除公开分享链接",
"Delete share": "删除分享",
"Are you sure you want to delete this shared link?": "您确定要删除此分享链接吗?",
"Publicly shared pages from spaces you are a member of will appear here": "您所属空间中公开分享的页面将显示在这里",
"Share deleted successfully": "分享删除成功",
"Share not found": "未找到分享",
"Failed to share page": "页面分享失败",
"Disable public sharing": "禁用公开分享",
"Prevent members from sharing pages publicly.": "阻止成员公开分享页面。",
"Toggle public sharing": "切换公开分享",
"Toggle space public sharing": "切换空间公开分享",
"Allow viewers to comment": "允许观众评论",
"Allow viewers to add comments on pages in this space.": "允许观众在此空间的页面上添加评论。",
"Toggle viewer comments": "切换观众评论",
"Public sharing is disabled at the workspace level": "公开分享在工作区级别被禁用",
"Prevent pages in this space from being shared publicly.": "阻止此空间中的页面被公开分享。",
"Page permissions": "页面权限},{",
"Control who can view and edit individual pages. Available with an enterprise license.": "控制谁可以查看和编辑单个页面。此功能在企业版许可下可用。",
"Enable public sharing": "启用公开分享",
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "您确定要启用公开分享吗?成员将能够公开分享页面。",
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "您确定要禁用公开分享吗?此工作区中的所有现有共享链接都将被删除。",
"Are you sure you want to enable public sharing for this space?": "您确定要为此空间启用公开分享吗?",
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "您确定要禁用公开分享吗?此空间中的所有现有共享链接都将被删除。",
"Public sharing is disabled": "公开分享已被禁用",
"Public sharing has been disabled at the workspace level.": "公开分享已在工作区级别被禁用。",
"Public sharing has been disabled for this space.": "此空间的公开分享已被禁用。",
"Copy page": "复制页面",
"Copy page to a different space.": "将页面复制到不同的空间。",
"Page copied successfully": "页面复制成功",
"Page duplicated successfully": "页面副本创建成功",
"Find": "查找",
"Not found": "未找到",
"Previous Match (Shift+Enter)": "上一个匹配项(Shift+Enter",
"Next match (Enter)": "下一个匹配项(Enter",
"Match case (Alt+C)": "区分大小写(Alt+C",
"Replace": "替换",
"Close (Escape)": "关闭(Escape",
"Replace (Enter)": "替换(Enter",
"Replace all (Ctrl+Alt+Enter)": "全部替换(Ctrl+Alt+Enter",
"Replace all": "全部替换",
"View all": "查看全部",
"View all spaces": "查看所有空间",
"Error": "错误",
"Failed to disable MFA": "禁用 MFA 失败",
"Disable two-factor authentication": "禁用双重身份验证",
"Disabling two-factor authentication will make your account less secure. You'll only need your password to sign in.": "停用双因素认证会降低账户安全性。您只需密码即可登录。",
"Please enter your password to disable two-factor authentication:": "请输入您的密码以停用双因素认证:",
"Two-factor authentication has been enabled": "双重身份验证已启用",
"Two-factor authentication has been disabled": "双重身份验证已禁用",
"2-step verification": "两步验证",
"Protect your account with an additional verification layer when signing in.": "通过额外的验证层保护您的账户安全。",
"Two-factor authentication is active on your account.": "您的账户已激活双因素认证。",
"Add 2FA method": "添加 2FA 方式",
"Backup codes": "备用代码",
"Disable": "禁用",
"Invalid verification code": "无效的验证码",
"New backup codes have been generated": "新的备用代码已生成",
"Failed to regenerate backup codes": "重新生成备用代码失败",
"About backup codes": "关于备用代码",
"Backup codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "如果您无法访问身份验证器应用,可使用备份代码访问账户。每个代码仅可使用一次。",
"You can regenerate new backup codes at any time. This will invalidate all existing codes.": "您可以随时重新生成新的备份代码。这将使所有现有代码失效。",
"Confirm password": "确认密码",
"Generate new backup codes": "生成新的备用代码",
"Save your new backup codes": "保存您的新备用代码",
"Make sure to save these codes in a secure place. Your old backup codes are no longer valid.": "请确保将这些代码保存在安全的地方。您的旧备份代码不再有效。",
"Your new backup codes": "您的新备用代码",
"I've saved my backup codes": "我已保存备用代码",
"Failed to setup MFA": "设置 MFA 失败",
"Setup & Verify": "设置并验证",
"Add to authenticator": "添加到身份验证器",
"1. Scan this QR code with your authenticator app": "1. 使用您的身份验证器应用扫描此二维码",
"Can't scan the code?": "无法扫描代码?",
"Enter this code manually in your authenticator app:": "在您的身份验证器应用中手动输入此代码:",
"2. Enter the 6-digit code from your authenticator": "2. 输入您的身份验证器中的 6 位代码",
"Verify and enable": "验证并启用",
"Failed to generate QR code. Please try again.": "生成二维码失败。请重试。",
"Backup": "备用",
"Save codes": "保存代码",
"Save your backup codes": "保存您的备用代码",
"These codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "如果无法访问身份验证器应用,可以使用这些代码访问账户。每个代码仅可使用一次。",
"Print": "打印",
"Two-factor authentication has been set up. Please log in again.": "双因素认证已设置。请重新登录。",
"Two-Factor authentication required": "需要双重身份验证",
"Your workspace requires two-factor authentication for all users": "您的工作区要求所有用户启用双重身份验证",
"To continue accessing your workspace, you must set up two-factor authentication. This adds an extra layer of security to your account.": "要继续访问工作区,必须设置双因素认证。此操作为您的账户添加一层额外的安全保障。",
"Set up two-factor authentication": "设置双重身份验证",
"Cancel and logout": "取消并退出登录",
"Your workspace requires two-factor authentication. Please set it up to continue.": "您的工作区需要双因素认证。请设置以继续。",
"This adds an extra layer of security to your account by requiring a verification code from your authenticator app.": "通过要求您的身份验证器应用提供验证码,此操作为您的账户增加了一层额外的安全保障。",
"Password is required": "密码为必填项",
"Password must be at least 8 characters": "密码长度至少为 8 个字符",
"Please enter a 6-digit code": "请输入 6 位代码",
"Code must be exactly 6 digits": "代码必须正好为 6 位",
"Enter the 6-digit code found in your authenticator app": "输入您身份验证器应用中的 6 位代码",
"Need help authenticating?": "需要帮助进行身份验证吗?",
"MFA QR Code": "MFA 二维码",
"Account created successfully. Please log in to set up two-factor authentication.": "账户创建成功。请登录以设置双因素认证。",
"Password reset successful. Please log in with your new password and complete two-factor authentication.": "密码重置成功。请使用新密码登录并完成双因素认证。",
"Password reset successful. Please log in with your new password to set up two-factor authentication.": "密码重置成功。请使用新密码登录以设置双因素认证。",
"Password reset was successful. Please log in with your new password.": "密码重置成功。请使用新密码登录。",
"Two-factor authentication": "双重身份验证",
"Use authenticator app instead": "改用身份验证器应用",
"Verify backup code": "验证备用代码",
"Use backup code": "使用备用代码",
"Enter one of your backup codes": "输入您的一个备用代码",
"Backup code": "备用代码",
"Enter one of your backup codes. Each backup code can only be used once.": "输入您的一个备份代码。每个备份代码只能使用一次。",
"Verify": "验证",
"Trash": "回收站",
"Pages in trash will be permanently deleted after {{count}} days.": "垃圾箱中的页面将在{{count}}天后被永久删除。",
"Deleted": "已删除",
"No pages in trash": "回收站中没有页面",
"Permanently delete page?": "永久删除页面?",
"Are you sure you want to permanently delete '{{title}}'? This action cannot be undone.": "确定要永久删除“{{title}}”吗?此操作无法撤销。",
"Restore '{{title}}' and its sub-pages?": "恢复“{{title}}”及其子页面?",
"Move to trash": "移至回收站",
"Move this page to trash?": "将此页面移至垃圾箱?",
"Restore page": "恢复页面",
"Permanently delete": "永久删除",
"<b>{{name}}</b> moved this page to Trash {{time}}.": "<b>{{name}}</b> 于 {{time}} 将此页面移至回收站。",
"Page moved to trash": "页面已移至回收站",
"Page restored successfully": "页面恢复成功",
"Deleted by": "删除者",
"Deleted at": "删除于",
"Preview": "预览",
"Subpages": "子页面",
"Failed to load subpages": "加载子页面失败",
"No subpages": "没有子页面",
"Subpages (Child pages)": "子页面(下级页面)",
"List all subpages of the current page": "列出当前页面的所有子页面",
"Attachments": "附件",
"All spaces": "所有空间",
"Unknown": "未知",
"Find a space": "查找空间",
"Search in all your spaces": "在您的所有空间中搜索",
"Type": "类型",
"Enterprise": "企业版",
"Download attachment": "下载附件",
"Allowed email domains": "允许的邮箱域名",
"Only users with email addresses from these domains can signup via SSO.": "只有使用这些域名邮箱地址的用户才能通过 SSO 注册。",
"Enter valid domain names separated by comma or space": "请输入有效的域名,并用逗号或空格分隔",
"Enforce two-factor authentication": "强制启用双重身份验证",
"Once enforced, all members must enable two-factor authentication to access the workspace.": "一旦实施,所有成员必须启用双因素认证才能访问工作区。",
"Toggle MFA enforcement": "切换 MFA 强制执行",
"Display name": "显示名称",
"Allow signup": "允许注册",
"Enabled": "已启用",
"Advanced Settings": "高级设置",
"Enable TLS/SSL": "启用 TLS/SSL",
"Use secure connection to LDAP server": "使用安全连接访问 LDAP 服务器",
"Group sync": "群组同步",
"No SSO providers found.": "未找到SSO提供商。",
"Delete SSO provider": "删除 SSO 提供商",
"Are you sure you want to delete this SSO provider?": "您确定要删除此SSO提供商吗?",
"Action": "操作",
"{{ssoProviderType}} configuration": "{{ssoProviderType}} 配置",
"Icon": "图标",
"Upload image": "上传图片",
"Remove image": "删除图片",
"Failed to remove image": "无法删除图片",
"Image exceeds 10MB limit.": "图片超过10MB限制。",
"Image removed successfully": "图片删除成功",
"API key": "API密钥",
"API keys": "API密钥",
"API management": "API管理",
"Custom expiration date": "自定义到期日期",
"Enter a descriptive token name": "输入描述性令牌名称",
"Expiration": "到期",
"Expired": "已过期",
"Expires": "到期",
"Last use": "上次使用",
"No API keys found": "找不到API密钥",
"No expiration": "无到期",
"Revoked successfully": "撤销成功",
"Select expiration date": "选择到期日期",
"This action cannot be undone. Any applications using this API key will stop working.": "此操作无法撤销。使用此API密钥的任何应用程序将停止工作。",
"Update": "更新",
"Update {{credential}}": "更新{{credential}}",
"Manage API keys for all users in the workspace": "管理工作空间中所有用户的API密钥",
"Restrict API key creation to admins": "仅限管理员创建 API 密钥。",
"Only admins and owners can create new API keys. Existing member keys will continue to work.": "只有管理员和所有者可以创建新的 API 密钥。现有成员密钥将继续有效。",
"Toggle restrict API keys to admins": "切换仅限管理员创建 API 密钥",
"API key creation is restricted to admins by your workspace administrator.": "API 密钥的创建已被您的工作区管理员限制为仅管理员可用。",
"AI settings": "AI设置",
"AI search": "AI搜索",
"AI Answer": "AI回答",
"Ask AI": "询问AI",
"AI is thinking...": "AI正在思考...",
"Thinking": "思考中",
"Ask a question...": "提问...",
"AI Answers": "AI答案",
"AI-powered search (AI Answers)": "AI驱动的搜索 (AI答案)",
"AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "AI搜索使用向量嵌入提供跨工作空间内容的语义搜索功能。",
"Toggle AI search": "切换AI搜索",
"Generative AI (Ask AI)": "生成型AI (询问AI)",
"Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.": "在编辑器中启用AI驱动的内容生成。允许用户生成、改进、翻译和转换文本。",
"Toggle generative AI": "切换生成型AI",
"Upgrade your plan": "升级您的方案",
"Available with a paid license": "需付费许可才可用",
"Upgrade your license tier.": "升级您的许可等级。",
"AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "AI 仅在 Docmost 企业版中提供。请联系 sales@docmost.com。",
"AI & MCP": "AI 与 MCP",
"AI": "AI",
"MCP": "MCP",
"Model Context Protocol (MCP)": "模型上下文协议(MCP",
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "启用 MCP 服务器以允许 AI 助手和工具与您的工作区内容交互。",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP 仅在 Docmost 企业版中提供。请联系 sales@docmost.com。",
"MCP Server URL": "MCP 服务器 URL",
"Use your API key for authentication. You can manage API keys in your account settings.": "使用您的 API 密钥进行身份验证。您可以在账户设置中管理 API 密钥。",
"Supported tools": "支持的工具",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "您的工作区已启用 MCP。使用您的 API 密钥连接 AI 助手。",
"MCP server URL:": "MCP 服务器 URL",
"Learn more": "了解更多",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "为工作区内所有用户管理 API 密钥。有关使用详情,请查阅<anchor>API 文档</anchor>。",
"View the <anchor>API documentation</anchor> for usage details.": "有关使用详情,请查阅<anchor>API 文档</anchor>。",
"View the <anchor>MCP documentation</anchor>.": "查看<anchor>MCP 文档</anchor>。",
"Sources": "来源",
"AI Answers not available for attachments": "AI答案不适用于附件",
"No answer available": "无可用答案",
"Background color": "背景颜色",
"Highlight color": "突出显示颜色",
"Remove color": "移除颜色",
"Notifications": "通知",
"No notifications": "没有通知",
"No unread notifications": "没有未读通知",
"All notifications": "所有通知",
"Unread only": "仅未读",
"Mark all as read": "标记所有为已读",
"Mark as read": "标记为已读",
"More options": "更多选项",
"<bold>{{name}}</bold> mentioned you in a comment": "<bold>{{name}}</bold>在评论中提到你",
"<bold>{{name}}</bold> commented on a page": "<bold>{{name}}</bold>在页面上评论了",
"<bold>{{name}}</bold> resolved a comment": "<bold>{{name}}</bold> 解决了一条评论",
"<bold>{{name}}</bold> mentioned you on a page": "<bold>{{name}}</bold> 在某个页面中提到了您",
"<bold>{{name}}</bold> gave you edit access to a page": "<bold>{{name}}</bold> 授予您某个页面的编辑权限",
"<bold>{{name}}</bold> gave you view access to a page": "<bold>{{name}}</bold> 授予您某个页面的查看权限",
"<bold>{{name}}</bold> updated a page": "<bold>{{name}}</bold> 更新了某个页面",
"Watch page": "关注页面",
"Stop watching": "取消关注",
"Watch space": "关注空间",
"Stop watching space": "取消关注空间",
"Email notifications": "邮件通知",
"Page updates": "页面更新",
"Get notified when pages you watch are updated.": "当你关注的页面有更新时收到通知。",
"Page mentions": "页面提及",
"Get notified when someone mentions you on a page.": "当有人在页面上提到你时收到通知。",
"Comment mentions": "评论提及",
"Get notified when someone mentions you in a comment.": "当有人在评论中提到你时收到通知。",
"New comments": "新评论",
"Get notified about new comments on threads you participate in.": "当你参与的讨论有新评论时收到通知。",
"Resolved comments": "已解决的评论",
"Get notified when your comment is resolved.": "当你的评论被解决时收到通知。",
"You are now watching this page": "你现在正在关注此页面",
"You are no longer watching this page": "你已取消关注此页面",
"You are now watching this space": "您现在正在关注此空间",
"You are no longer watching this space": "您已不再关注此空间",
"Direct": "直接",
"Updates": "更新",
"Today": "今天",
"Yesterday": "昨天",
"This week": "本周",
"Older": "较早",
"Restricted page": "受限页面",
"Restricted pages cannot be shared publicly.": "受限页面不能公开共享。",
"Restricted by parent": "受父页面限制",
"Restricted": "受限",
"Open": "公开",
"Inherits restrictions from ancestor page": "继承自上级页面的限制",
"Only people listed below can access this page": "只有下面列出的人可以访问此页面",
"Everyone in this space can access": "此空间中的所有人均可访问",
"No additional restrictions on this page": "此页面无额外限制",
"Only specific people can access": "仅特定人员可访问",
"Use only inherited restrictions": "仅使用继承的限制",
"Add restrictions on top of inherited": "在继承的限制之上添加限制",
"Inherited restriction": "继承的限制",
"Access limited by": "访问受限于",
"Restrict access to control who can view and edit this page": "限制访问以控制谁可以查看和编辑此页面",
"Add additional restrictions specific to this page": "为此页面添加额外的特定限制",
"Access": "访问",
"People with access": "有访问权限的人员",
"Remove all": "全部移除",
"Remove access": "移除访问权限",
"Remove all access": "移除所有访问权限",
"Are you sure you want to remove this member's access to the page?": "您确定要移除此成员对该页面的访问权限吗?",
"Are you sure you want to remove all specific access? This will make the page open to everyone in the space.": "您确定要删除所有特定访问权限吗?这将使该页面对该空间中的所有人开放。",
"Trash retention": "垃圾箱保留期",
"Pages in trash will be permanently deleted after this period.": "该期限结束后,垃圾箱中的页面将被永久删除。",
"Trash retention updated": "垃圾箱保留期已更新",
"Failed to update trash retention": "更新垃圾箱保留期失败",
"Removed page restriction": "已移除页面限制",
"Added page permission": "已添加页面权限",
"Removed page permission": "已移除页面权限",
"day": "天",
"days": "天",
"week": "周",
"weeks": "周",
"month": "个月",
"months": "个月",
"year": "年",
"years": "年",
"Period": "周期",
"Fixed date": "固定日期",
"Indefinitely": "无限期",
"Days": "天",
"Weeks": "周",
"Months": "个月",
"Years": "年",
"Pick a date": "选择日期",
"Maximum is {{max}} {{unit}} for this unit": "此单位的最大值为 {{max}} {{unit}}",
"Never expires. Verifiers can re-verify at any time.": "永不过期。验证者可随时重新验证。",
"Verified": "已验证",
"Review needed": "需要审核",
"Verification expired": "验证已过期",
"Draft": "草稿",
"In Approval": "审批中",
"In approval": "审批中",
"Approved": "已批准",
"Obsolete": "已作废",
"Expiring": "即将过期",
"Set up verification": "设置验证",
"Verify page": "验证页面",
"Page verification": "页面验证",
"Add verification": "添加验证",
"Edit verification": "编辑验证",
"Search by title": "按标题搜索",
"Choose how this page should stay accurate.": "选择此页面保持准确的方式。",
"Recurring verification": "定期验证",
"Verifiers re-confirm this page on a schedule.": "验证者按计划重新确认此页面。",
"Re-verify on a schedule (e.g every 30 days )": "按计划重新验证(例如每 30 天一次)",
"Page stays editable at all times": "页面始终可编辑",
"Best for runbooks, FAQs, living documentation": "最适合运行手册、常见问题和动态文档",
"Approval workflow": "审批工作流",
"Formal document lifecycle with named approvers.": "具有指定审批人的正式文档生命周期。",
"Draft → In approval → Approved → Obsolete": "草稿 → 审批中 → 已批准 → 已作废",
"Locked once approved, with full history": "批准后锁定,并保留完整历史记录",
"Designed for ISO 9001, ISO 13485, and FDA": "专为 ISO 9001、ISO 13485 和 FDA 设计",
"Best for SOPs and controlled documents": "最适合 SOP 和受控文档",
"Back": "返回",
"Quality management": "质量管理",
"Recurring": "定期",
"Pages move through draft, approval, and approved stages.": "页面会经历草稿、审批中和已批准阶段。",
"Verifiers": "验证者",
"Add verifier": "添加验证者",
"I've reviewed this page for accuracy": "我已审核此页面的准确性",
"Set up": "设置",
"Remove verification": "移除验证",
"Are you sure you want to remove verification from this page?": "确定要移除此页面的验证吗?",
"Assigned verifiers must periodically re-verify this page.": "指定的验证者必须定期重新验证此页面。",
"Last verified by {{name}} {{time}} (expired)": "最后由 {{name}} 于 {{time}} 验证(已过期)",
"The fixed expiration date has passed.": "固定到期日已过。",
"Verified by {{name}} {{time}}": "由 {{name}} 于 {{time}} 验证",
"Expires {{date}}": "于 {{date}} 到期",
"Expired {{date}}": "已于 {{date}} 过期",
"Mark as obsolete": "标记为作废",
"Mark obsolete": "标记作废",
"Returned by {{name}} {{time}}": "由 {{name}} 于 {{time}} 退回",
"No approval has been requested yet.": "尚未请求审批。",
"Submitted by {{name}} {{time}}": "由 {{name}} 于 {{time}} 提交",
"Someone": "某人",
"Approved by {{name}} {{time}}": "由 {{name}} 于 {{time}} 批准",
"This document has been marked as obsolete.": "此文档已被标记为作废。",
"Rejection comment": "退回意见",
"Reason for returning this document...": "退回此文档的原因...",
"Confirm rejection": "确认退回",
"Submit for approval": "提交审批",
"Reject": "退回",
"Approve": "批准",
"Re-submit for approval": "重新提交审批",
"Verified until": "验证有效期至",
"QMS": "QMS",
"Verified pages": "已验证页面",
"Search pages...": "搜索页面...",
"Filter by space": "按空间筛选",
"Filter by type": "按类型筛选",
"<bold>{{name}}</bold> verified a page": "<bold>{{name}}</bold> 验证了一个页面",
"<bold>{{name}}</bold> submitted a page for your approval": "<bold>{{name}}</bold> 提交了一个页面供您审批",
"<bold>{{name}}</bold> returned a page for revision": "<bold>{{name}}</bold> 退回了一个页面以供修改",
"Page verification expires soon": "页面验证即将过期",
"Page verification has expired": "页面验证已过期",
"Verifying your email": "正在验证您的邮箱",
"Please wait...": "请稍候……",
"Verification failed. The link may have expired.": "验证失败。该链接可能已过期。",
"Check your email": "检查您的邮箱",
"We sent a verification link to {{email}}.": "我们已向{{email}}发送了一封验证邮件。",
"We sent a verification link to your email.": "我们已向您的邮箱发送了一封验证邮件。",
"Click the link to verify your email and access your workspace.": "请点击链接以验证邮箱并访问您的工作区。",
"Resend verification email": "重新发送验证邮件",
"Verification email sent. Please check your inbox.": "验证邮件已发送。请检查您的收件箱。",
"Failed to resend verification email. Please try again.": "重新发送验证邮件失败。请重试。",
"We've sent you an email with your associated workspaces.": "我们已向您发送包含关联工作区的邮件。",
"Load more": "加载更多",
"Log out of all devices": "退出所有设备登录",
"Log out of all sessions except this device": "退出除当前设备外的所有会话",
"This Device": "此设备",
"Unknown device": "未知设备",
"No active sessions": "没有活动会话",
"Session revoked": "会话已撤销",
"All other sessions revoked": "所有其他会话已撤销",
"Last used": "上次使用",
"Created": "创建时间",
"Rename": "重命名",
"Publish": "发布",
"Security": "安全",
"Enforce SSO": "强制使用 SSO",
"Once enforced, members will not be able to login with email and password.": "启用后,成员将无法使用邮箱和密码登录。",
"AI-generated content may not be accurate.": "AI 生成的内容可能并不准确。",
"AI Chat": "AI 聊天",
"Analyze for insights": "分析并获取洞察",
"Ask anything...": "随便问点什么...",
"Assistant said:": "助手说:",
"Chat history": "聊天记录",
"Chat name": "聊天名称",
"Chat transcript": "聊天记录",
"Close": "关闭",
"Copy assistant response": "复制助手回复",
"Docmost AI": "Docmost AI",
"Failed to load chat. An error occurred.": "加载聊天失败。发生错误。",
"Failed to render this message.": "渲染此消息失败。",
"How can I help you today?": "今天我可以如何帮助您?",
"New chat": "新聊天",
"No chat history": "没有聊天记录",
"No chats found": "未找到聊天",
"No conversations yet": "暂无对话",
"Open full page": "打开完整页面",
"Scroll to bottom": "滚动到底部",
"You said:": "你说:",
"Previous 7 days": "前 7 天",
"Previous 30 days": "前 30 天",
"Search chats...": "搜索聊天...",
"Search chats": "搜索聊天",
"Ask anything... Use @ to mention pages": "询问任何内容……使用 @ 提及页面",
"Ask anything or search your workspace": "询问任何问题或搜索你的工作区",
"Welcome to {{name}}": "欢迎使用 {{name}}",
"Add files": "添加文件",
"Mention a page": "提及页面",
"Start a new chat to see it here.": "开始新的聊天后会显示在这里。",
"Summarize this page": "总结此页面",
"Toggle AI Chat": "切换 AI 聊天",
"Translate this page": "翻译此页面",
"Try a different search term.": "请尝试其他搜索词。",
"Try again": "重试",
"Untitled chat": "未命名聊天",
"What can I help you with?": "我能帮您做什么?",
"Are you sure you want to revoke this {{credential}}": "确定要撤销此{{credential}}吗",
"Automatically provision users and groups from your identity provider via SCIM.": "通过 SCIM 从您的身份提供商自动预配用户和群组。",
"Configure your identity provider with this URL to provision users and groups.": "使用此 URL 配置您的身份提供商以预配用户和群组。",
"Create {{credential}}": "创建{{credential}}",
"{{credential}} created": "已创建{{credential}}",
"{{credential}} created successfully": "已成功创建{{credential}}",
"Created by": "创建者",
"Custom": "自定义",
"Enable SCIM": "启用 SCIM",
"Enter a descriptive name": "输入描述性名称",
"I've saved my {{credential}}": "我已保存我的{{credential}}",
"Important": "重要",
"Make sure to copy your {{credential}} now. You won't be able to see it again!": "请务必立即复制您的{{credential}}。之后您将无法再次查看!",
"Never": "从不",
"Revoke {{credential}}": "撤销{{credential}}",
"SCIM endpoint URL": "SCIM 端点 URL",
"SCIM provisioning": "SCIM 预配",
"SCIM takes precedence over SSO group sync while enabled.": "启用后,SCIM 的优先级高于 SSO 群组同步。",
"You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "您已达到 {{max}} 个 SCIM 令牌的上限。请删除一个现有令牌以创建新令牌。",
"SCIM token": "SCIM 令牌",
"SCIM tokens": "SCIM 令牌",
"This action cannot be undone. Your identity provider will stop syncing immediately.": "此操作无法撤销。您的身份提供商将立即停止同步。",
"Toggle SCIM provisioning": "切换 SCIM 预配",
"Token": "令牌",
"Page menu": "页面菜单",
"Expand": "展开",
"Collapse": "折叠",
"Comment menu": "评论菜单",
"Group menu": "群组菜单",
"Show hidden breadcrumbs": "显示隐藏的面包屑",
"Breadcrumbs": "面包屑",
"Page actions": "页面操作",
"Pick emoji": "选择表情符号",
"Template menu": "模板菜单",
"Use": "使用",
"Use template": "使用模板",
"Preview template: {{title}}": "预览模板:{{title}}",
"Use a template": "使用模板",
"Search templates...": "搜索模板……",
"Search spaces...": "搜索空间……",
"No templates found": "未找到模板",
"No spaces found": "未找到空间",
"Browse all templates": "浏览所有模板",
"This space": "此空间",
"All templates": "所有模板",
"Global": "全局",
"New template": "新建模板",
"Edit template": "编辑模板",
"Are you sure you want to delete this template?": "你确定要删除此模板吗?",
"Template scope updated": "模板范围已更新",
"Choose which space this template belongs to": "选择此模板所属的空间",
"Scope": "范围",
"Select scope": "选择范围",
"Title": "标题",
"Saving...": "正在保存……",
"Saved": "已保存",
"Save failed. Retry": "保存失败。重试",
"By {{name}}": "作者:{{name}}",
"Updated {{time}}": "更新于 {{time}}",
"Choose destination": "选择目标位置",
"Search pages and spaces...": "搜索页面和空间……",
"No results found": "未找到结果",
"You don't have permission to create pages here": "你无权在此处创建页面",
"Chat menu": "聊天菜单",
"API key menu": "API 密钥菜单",
"Jump to comment selection": "跳转到评论选择",
"Slash commands": "斜杠命令",
"Mention suggestions": "提及建议",
"Link suggestions": "链接建议",
"Diagram editor": "图表编辑器",
"Add comment": "添加评论",
"Find and replace": "查找和替换",
"Main navigation": "主导航",
"Space navigation": "空间导航",
"Settings navigation": "设置导航",
"AI navigation": "AI 导航",
"Breadcrumb": "面包屑",
"Synced block": "同步块",
"Create a block that stays in sync across pages.": "创建一个可在多个页面间保持同步的块。",
"Editing original": "正在编辑原始内容",
"Copy synced block": "复制同步块",
"Unsync": "取消同步",
"Delete synced block": "删除同步块",
"Synced to {{count}} other page_one": "已与另外 {{count}} 个页面同步",
"Synced to {{count}} other page_other": "已与另外 {{count}} 个页面同步",
"ORIGINAL": "原始内容",
"THIS PAGE": "此页面",
"No pages": "没有页面",
"The original synced block no longer exists": "原始同步块已不存在",
"You don't have access to this synced block": "你无权访问此同步块",
"Failed to load this synced block": "加载此同步块失败",
"Fixed editor toolbar": "固定编辑器工具栏",
"Show a formatting toolbar above the editor with quick access to common actions.": "在编辑器上方显示格式工具栏,便于快速访问常用操作。",
"Toggle fixed editor toolbar": "切换固定编辑器工具栏",
"Normal text": "普通文本",
"More inline formatting": "更多内联格式",
"Subscript": "下标",
"Superscript": "上标",
"Inline code": "行内代码",
"Insert media": "插入媒体",
"Mention": "提及",
"Emoji": "表情符号",
"Columns": "分栏",
"More inserts": "更多插入项",
"Embeds": "嵌入内容",
"Diagrams": "图表",
"Advanced": "高级",
"Utility": "实用工具",
"Decrease indent": "减少缩进",
"Increase indent": "增加缩进",
"Clear formatting": "清除格式",
"Code block": "代码块",
"Experimental": "实验性",
"Strikethrough": "删除线",
"Undo": "撤销",
"Redo": "重做",
"Backlinks": "反向链接",
"Last updated by": "最后更新者",
"Last updated": "最后更新",
"Stats": "统计",
"Word count": "字数",
"Characters": "字符数",
"Incoming links": "传入链接",
"Outgoing links": "传出链接",
"Incoming links ({{count}})": "传入链接({{count}}",
"Outgoing links ({{count}})": "传出链接({{count}}",
"No pages link here yet.": "还没有页面链接到这里。",
"This page doesn't link to other pages yet.": "此页面尚未链接到其他页面。",
"Verified until {{date}}": "验证有效期至 {{date}}",
"Labels": "标签",
"Add label": "添加标签",
"No labels yet": "还没有标签",
"Already added": "已添加",
"Invalid label name": "标签名称无效",
"No matches": "无匹配结果",
"Search or create…": "搜索或创建…",
"Remove label {{name}}": "移除标签 {{name}}",
"Failed to add label": "添加标签失败",
"Failed to remove label": "移除标签失败",
"No pages with this label": "没有带有此标签的页面",
"Pages tagged with this label will appear here.": "带有此标签的页面将显示在这里。",
"No pages match your search.": "没有页面匹配你的搜索。",
"Updated {{date}}": "更新于 {{date}}",
"Cell actions": "单元格操作",
"Column actions": "列操作",
"Row actions": "行操作",
"Filter": "筛选",
"Page title": "页面标题",
"Page content": "页面内容",
"Member actions": "成员操作",
"Toggle password visibility": "切换密码可见性",
"Send comment": "发送评论",
"Token actions": "令牌操作",
"Template settings": "模板设置",
"Edit diagram": "编辑图表",
"Edit embed": "编辑嵌入内容",
"Edit drawing": "编辑绘图",
"Delete equation": "删除公式",
"Invite actions": "邀请操作",
"Get started": "开始使用",
"* indicates required fields": "* 表示必填字段",
"List of spaces in this workspace": "此工作区中的空间列表",
"Active sessions": "活动会话",
"Add {{name}} to favorites": "将 {{name}} 添加到收藏",
"Remove {{name}} from favorites": "将 {{name}} 从收藏中移除",
"Added to favorites": "已添加到收藏",
"Removed from favorites": "已从收藏中移除",
"Added {{name}} to favorites": "已将 {{name}} 添加到收藏",
"Removed {{name}} from favorites": "已将 {{name}} 从收藏中移除",
"Page menu for {{name}}": "{{name}} 的页面菜单",
"Create subpage of {{name}}": "创建 {{name}} 的子页面"
} }
+30
View File
@@ -0,0 +1,30 @@
{
"name": "Docmost",
"short_name": "Docmost",
"start_url": "/",
"display": "standalone",
"background_color": "#222",
"theme_color": "#222",
"icons": [
{
"src": "icons/favicon-16x16.png",
"type": "image/png",
"sizes": "16x16"
},
{
"src": "icons/favicon-32x32.png",
"type": "image/png",
"sizes": "32x32"
},
{
"src": "icons/app-icon-192x192.png",
"type": "image/png",
"sizes": "180x180 192x192"
},
{
"src": "icons/app-icon-512x512.png",
"type": "image/png",
"sizes": "512x512"
}
]
}
+54 -8
View File
@@ -14,7 +14,6 @@ import AccountPreferences from "@/pages/settings/account/account-preferences.tsx
import SpaceHome from "@/pages/space/space-home.tsx"; import SpaceHome from "@/pages/space/space-home.tsx";
import PageRedirect from "@/pages/page/page-redirect.tsx"; import PageRedirect from "@/pages/page/page-redirect.tsx";
import Layout from "@/components/layouts/global/layout.tsx"; import Layout from "@/components/layouts/global/layout.tsx";
import { ErrorBoundary } from "react-error-boundary";
import InviteSignup from "@/pages/auth/invite-signup.tsx"; import InviteSignup from "@/pages/auth/invite-signup.tsx";
import ForgotPassword from "@/pages/auth/forgot-password.tsx"; import ForgotPassword from "@/pages/auth/forgot-password.tsx";
import PasswordReset from "./pages/auth/password-reset"; import PasswordReset from "./pages/auth/password-reset";
@@ -26,10 +25,32 @@ import { useTranslation } from "react-i18next";
import Security from "@/ee/security/pages/security.tsx"; import Security from "@/ee/security/pages/security.tsx";
import License from "@/ee/licence/pages/license.tsx"; import License from "@/ee/licence/pages/license.tsx";
import { useRedirectToCloudSelect } from "@/ee/hooks/use-redirect-to-cloud-select.tsx"; import { useRedirectToCloudSelect } from "@/ee/hooks/use-redirect-to-cloud-select.tsx";
import SharedPage from "@/pages/share/shared-page.tsx";
import PdfRenderPage from "@/ee/pdf-export/pdf-render-page.tsx";
import Shares from "@/pages/settings/shares/shares.tsx";
import ShareLayout from "@/features/share/components/share-layout.tsx";
import ShareRedirect from "@/pages/share/share-redirect.tsx";
import { useTrackOrigin } from "@/hooks/use-track-origin";
import SpacesPage from "@/pages/spaces/spaces.tsx";
import { MfaChallengePage } from "@/ee/mfa/pages/mfa-challenge-page";
import { MfaSetupRequiredPage } from "@/ee/mfa/pages/mfa-setup-required-page";
import SpaceTrash from "@/pages/space/space-trash.tsx";
import UserApiKeys from "@/ee/api-key/pages/user-api-keys";
import WorkspaceApiKeys from "@/ee/api-key/pages/workspace-api-keys";
import AiSettings from "@/ee/ai/pages/ai-settings.tsx";
import AuditLogs from "@/ee/audit/pages/audit-logs.tsx";
import VerifiedPages from "@/ee/page-verification/pages/verified-pages.tsx";
import TemplateList from "@/ee/template/pages/template-list";
import TemplateEditor from "@/ee/template/pages/template-editor";
import FavoritesPage from "@/pages/favorites/favorites-page";
import AiChat from "@/ee/ai-chat/pages/ai-chat.tsx";
import VerifyEmail from "@/ee/pages/verify-email.tsx";
import LabelPage from "@/pages/label/label-page";
export default function App() { export default function App() {
const { t } = useTranslation(); const { t } = useTranslation();
useRedirectToCloudSelect(); useRedirectToCloudSelect();
useTrackOrigin();
return ( return (
<> <>
@@ -39,6 +60,8 @@ export default function App() {
<Route path={"/invites/:invitationId"} element={<InviteSignup />} /> <Route path={"/invites/:invitationId"} element={<InviteSignup />} />
<Route path={"/forgot-password"} element={<ForgotPassword />} /> <Route path={"/forgot-password"} element={<ForgotPassword />} />
<Route path={"/password-reset"} element={<PasswordReset />} /> <Route path={"/password-reset"} element={<PasswordReset />} />
<Route path={"/login/mfa"} element={<MfaChallengePage />} />
<Route path={"/login/mfa/setup"} element={<MfaSetupRequiredPage />} />
{!isCloud() && ( {!isCloud() && (
<Route path={"/setup/register"} element={<SetupWorkspace />} /> <Route path={"/setup/register"} element={<SetupWorkspace />} />
@@ -48,23 +71,39 @@ export default function App() {
<> <>
<Route path={"/create"} element={<CreateWorkspace />} /> <Route path={"/create"} element={<CreateWorkspace />} />
<Route path={"/select"} element={<CloudLogin />} /> <Route path={"/select"} element={<CloudLogin />} />
<Route path={"/verify-email"} element={<VerifyEmail />} />
</> </>
)} )}
<Route element={<ShareLayout />}>
<Route
path={"/share/:shareId/p/:pageSlug"}
element={<SharedPage />}
/>
<Route path={"/share/p/:pageSlug"} element={<SharedPage />} />
</Route>
<Route path={"/pdf-render/:pageId"} element={<PdfRenderPage />} />
<Route path={"/share/:shareId"} element={<ShareRedirect />} />
<Route path={"/p/:pageSlug"} element={<PageRedirect />} /> <Route path={"/p/:pageSlug"} element={<PageRedirect />} />
<Route element={<Layout />}> <Route element={<Layout />}>
<Route path={"/home"} element={<Home />} /> <Route path={"/home"} element={<Home />} />
<Route path={"/ai"} element={<AiChat />} />
<Route path={"/ai/chat/:chatId"} element={<AiChat />} />
<Route path={"/spaces"} element={<SpacesPage />} />
<Route path={"/favorites"} element={<FavoritesPage />} />
<Route path={"/labels/:labelName"} element={<LabelPage />} />
<Route path={"/templates"} element={<TemplateList />} />
<Route
path={"/templates/:templateId"}
element={<TemplateEditor />}
/>
<Route path={"/s/:spaceSlug"} element={<SpaceHome />} /> <Route path={"/s/:spaceSlug"} element={<SpaceHome />} />
<Route path={"/s/:spaceSlug/trash"} element={<SpaceTrash />} />
<Route <Route
path={"/s/:spaceSlug/p/:pageSlug"} path={"/s/:spaceSlug/p/:pageSlug"}
element={ element={<Page />}
<ErrorBoundary
fallback={<>{t("Failed to load page. An error occurred.")}</>}
>
<Page />
</ErrorBoundary>
}
/> />
<Route path={"/settings"}> <Route path={"/settings"}>
@@ -73,12 +112,19 @@ export default function App() {
path={"account/preferences"} path={"account/preferences"}
element={<AccountPreferences />} element={<AccountPreferences />}
/> />
<Route path={"account/api-keys"} element={<UserApiKeys />} />
<Route path={"workspace"} element={<WorkspaceSettings />} /> <Route path={"workspace"} element={<WorkspaceSettings />} />
<Route path={"members"} element={<WorkspaceMembers />} /> <Route path={"members"} element={<WorkspaceMembers />} />
<Route path={"api-keys"} element={<WorkspaceApiKeys />} />
<Route path={"groups"} element={<Groups />} /> <Route path={"groups"} element={<Groups />} />
<Route path={"groups/:groupId"} element={<GroupInfo />} /> <Route path={"groups/:groupId"} element={<GroupInfo />} />
<Route path={"spaces"} element={<Spaces />} /> <Route path={"spaces"} element={<Spaces />} />
<Route path={"sharing"} element={<Shares />} />
<Route path={"security"} element={<Security />} /> <Route path={"security"} element={<Security />} />
<Route path={"ai"} element={<AiSettings />} />
<Route path={"ai/mcp"} element={<AiSettings />} />
<Route path={"audit"} element={<AuditLogs />} />
<Route path={"verifications"} element={<VerifiedPages />} />
{!isCloud() && <Route path={"license"} element={<License />} />} {!isCloud() && <Route path={"license"} element={<License />} />}
{isCloud() && <Route path={"billing"} element={<Billing />} />} {isCloud() && <Route path={"billing"} element={<Billing />} />}
</Route> </Route>
@@ -0,0 +1,183 @@
import React, { useRef } from "react";
import { Menu, Box, Loader } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { IconTrash, IconUpload } from "@tabler/icons-react";
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
import { notifications } from "@mantine/notifications";
interface AvatarUploaderProps {
currentImageUrl?: string | null;
fallbackName?: string;
radius?: string | number;
size?: string | number;
variant?: string;
type: AvatarIconType;
onUpload: (file: File) => Promise<void>;
onRemove: () => Promise<void>;
isLoading?: boolean;
disabled?: boolean;
}
export default function AvatarUploader({
currentImageUrl,
fallbackName,
radius,
variant,
size,
type,
onUpload,
onRemove,
isLoading = false,
disabled = false,
}: AvatarUploaderProps) {
const { t } = useTranslation();
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileInputChange = async (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const file = event.target.files?.[0];
if (!file || disabled) {
return;
}
// Validate file size (max 10MB)
const maxSizeInBytes = 10 * 1024 * 1024;
if (file.size > maxSizeInBytes) {
notifications.show({
message: t("Image exceeds 10MB limit."),
color: "red",
});
// Reset the input
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
return;
}
try {
await onUpload(file);
} catch (error) {
console.error(error);
notifications.show({
message: t("Failed to upload image"),
color: "red",
});
}
// Reset the input so the same file can be selected again
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
const handleUploadClick = () => {
if (fileInputRef.current) {
fileInputRef.current.click();
} else {
console.error("File input ref is null!");
}
};
const actionLabel = {
[AvatarIconType.AVATAR]: t("Change avatar"),
[AvatarIconType.SPACE_ICON]: t("Change space icon"),
[AvatarIconType.WORKSPACE_ICON]: t("Change workspace icon"),
}[type];
// Per WCAG 2.5.3 (Label in Name), the accessible name must include the
// visible text. When no image is set, the avatar renders the name's
// initials, so prepend the name to the action label.
const ariaLabel =
!currentImageUrl && fallbackName
? `${fallbackName} ${actionLabel}`
: actionLabel;
const handleRemove = async () => {
if (disabled) return;
try {
await onRemove();
notifications.show({
message: t("Image removed successfully"),
});
} catch (error) {
console.error(error);
notifications.show({
message: t("Failed to remove image"),
color: "red",
});
}
};
return (
<Box>
<input
type="file"
ref={fileInputRef}
onChange={handleFileInputChange}
accept="image/png,image/jpeg,image/jpg"
aria-label={ariaLabel}
tabIndex={-1}
style={{ display: "none" }}
/>
<Menu shadow="md" width={200} withArrow disabled={disabled || isLoading}>
<Menu.Target>
<Box style={{ position: "relative", display: "inline-block" }}>
<CustomAvatar
component="button"
size={size}
avatarUrl={currentImageUrl}
name={fallbackName}
aria-label={ariaLabel}
aria-haspopup="menu"
style={{
cursor: disabled || isLoading ? "default" : "pointer",
opacity: isLoading ? 0.6 : 1,
}}
radius={radius}
variant={variant}
type={type}
/>
{isLoading && (
<Box
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: 200,
}}
>
<Loader size="sm" />
</Box>
)}
</Box>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
leftSection={<IconUpload size={16} />}
disabled={isLoading || disabled}
onClick={handleUploadClick}
>
{t("Upload image")}
</Menu.Item>
{currentImageUrl && (
<Menu.Item
leftSection={<IconTrash size={16} />}
color="red"
onClick={handleRemove}
disabled={isLoading || disabled}
>
{t("Remove image")}
</Menu.Item>
)}
</Menu.Dropdown>
</Menu>
</Box>
);
}
@@ -0,0 +1,33 @@
// Source: https://github.com/mantinedev/mantine/blob/master/packages/@mantine/core/src/components/CopyButton/CopyButton.tsx - MIT
// modified to use the polyfilled clipboard api
import React from "react";
import { useClipboard } from "@/hooks/use-clipboard";
import { useProps } from "@mantine/core";
interface CopyButtonProps {
/** Children callback, provides current status and copy function as an argument */
children: (payload: { copied: boolean; copy: () => void }) => React.ReactNode;
/** Value that is copied to the clipboard when the button is clicked */
value: string;
/** Copied status timeout in ms @default `1000` */
timeout?: number;
}
const defaultProps = {
timeout: 1000,
} satisfies Partial<CopyButtonProps>;
export function CopyButton(props: CopyButtonProps) {
const { children, timeout, value, ...others } = useProps(
"CopyButton",
defaultProps,
props,
);
const clipboard = useClipboard({ timeout });
const copy = () => clipboard.copy(value);
return <>{children({ copy, copied: clipboard.copied, ...others })}</>;
}
CopyButton.displayName = "@mantine/core/CopyButton";
+12 -3
View File
@@ -1,19 +1,26 @@
import { ActionIcon, CopyButton, Tooltip } from "@mantine/core"; import { ActionIcon, MantineColor, MantineSize, Tooltip } from "@mantine/core";
import { CopyButton } from "@/components/common/copy-button";
import { IconCheck, IconCopy } from "@tabler/icons-react"; import { IconCheck, IconCopy } from "@tabler/icons-react";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
interface CopyProps { interface CopyProps {
text: string; text: string;
size?: MantineSize;
color?: MantineColor;
/** Override the accessible name (and tooltip) when not yet copied. Lets callers disambiguate adjacent copy buttons for screen readers. */
label?: string;
} }
export default function CopyTextButton({ text }: CopyProps) { export default function CopyTextButton({ text, size, label }: CopyProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const copyLabel = label ?? t("Copy");
return ( return (
<CopyButton value={text} timeout={2000}> <CopyButton value={text} timeout={2000}>
{({ copied, copy }) => ( {({ copied, copy }) => (
<Tooltip <Tooltip
label={copied ? t("Copied") : t("Copy")} label={copied ? t("Copied") : copyLabel}
withArrow withArrow
position="right" position="right"
> >
@@ -21,6 +28,8 @@ export default function CopyTextButton({ text }: CopyProps) {
color={copied ? "teal" : "gray"} color={copied ? "teal" : "gray"}
variant="subtle" variant="subtle"
onClick={copy} onClick={copy}
size={size}
aria-label={copied ? t("Copied") : copyLabel}
> >
{copied ? <IconCheck size={16} /> : <IconCopy size={16} />} {copied ? <IconCheck size={16} /> : <IconCopy size={16} />}
</ActionIcon> </ActionIcon>
@@ -29,19 +29,27 @@ export default function ExportModal({
}: ExportModalProps) { }: ExportModalProps) {
const [format, setFormat] = useState<ExportFormat>(ExportFormat.Markdown); const [format, setFormat] = useState<ExportFormat>(ExportFormat.Markdown);
const [includeChildren, setIncludeChildren] = useState<boolean>(false); const [includeChildren, setIncludeChildren] = useState<boolean>(false);
const [includeAttachments, setIncludeAttachments] = useState<boolean>(true); const [includeAttachments, setIncludeAttachments] = useState<boolean>(false);
const [isExporting, setIsExporting] = useState<boolean>(false);
const { t } = useTranslation(); const { t } = useTranslation();
const handleExport = async () => { const handleExport = async () => {
setIsExporting(true);
try { try {
if (type === "page") { if (type === "page") {
await exportPage({ pageId: id, format, includeChildren }); await exportPage({
pageId: id,
format,
includeChildren,
includeAttachments,
});
} }
if (type === "space") { if (type === "space") {
await exportSpace({ spaceId: id, format, includeAttachments }); await exportSpace({ spaceId: id, format, includeAttachments });
} }
setIncludeChildren(false); notifications.show({
setIncludeAttachments(true); message: t("Export successful"),
});
onClose(); onClose();
} catch (err) { } catch (err) {
notifications.show({ notifications.show({
@@ -49,6 +57,8 @@ export default function ExportModal({
color: "red", color: "red",
}); });
console.error("export error", err); console.error("export error", err);
} finally {
setIsExporting(false);
} }
}; };
@@ -71,7 +81,7 @@ export default function ExportModal({
<Modal.Content style={{ overflow: "hidden" }}> <Modal.Content style={{ overflow: "hidden" }}>
<Modal.Header py={0}> <Modal.Header py={0}>
<Modal.Title fw={500}>{t(`Export ${type}`)}</Modal.Title> <Modal.Title fw={500}>{t(`Export ${type}`)}</Modal.Title>
<Modal.CloseButton /> <Modal.CloseButton aria-label={t("Close")} />
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>
<Group justify="space-between" wrap="nowrap"> <Group justify="space-between" wrap="nowrap">
@@ -96,6 +106,18 @@ export default function ExportModal({
checked={includeChildren} checked={includeChildren}
/> />
</Group> </Group>
<Group justify="space-between" wrap="nowrap" mt="md">
<div>
<Text size="md">{t("Include attachments")}</Text>
</div>
<Switch
onChange={(event) =>
setIncludeAttachments(event.currentTarget.checked)
}
checked={includeAttachments}
/>
</Group>
</> </>
)} )}
@@ -121,7 +143,7 @@ export default function ExportModal({
<Button onClick={onClose} variant="default"> <Button onClick={onClose} variant="default">
{t("Cancel")} {t("Cancel")}
</Button> </Button>
<Button onClick={handleExport}>{t("Export")}</Button> <Button onClick={handleExport} loading={isExporting}>{t("Export")}</Button>
</Group> </Group>
</Modal.Body> </Modal.Body>
</Modal.Content> </Modal.Content>
@@ -4,14 +4,15 @@ import { useTranslation } from "react-i18next";
interface NoTableResultsProps { interface NoTableResultsProps {
colSpan: number; colSpan: number;
text?: string;
} }
export default function NoTableResults({ colSpan }: NoTableResultsProps) { export default function NoTableResults({ colSpan, text }: NoTableResultsProps) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Table.Tr> <Table.Tr>
<Table.Td colSpan={colSpan}> <Table.Td colSpan={colSpan}>
<Text fw={500} c="dimmed" ta="center"> <Text fw={500} c="dimmed" ta="center">
{t("No results found...")} {text || t("No results found...")}
</Text> </Text>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
@@ -2,17 +2,17 @@ import { Button, Group } from "@mantine/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
export interface PagePaginationProps { export interface PagePaginationProps {
currentPage: number;
hasPrevPage: boolean; hasPrevPage: boolean;
hasNextPage: boolean; hasNextPage: boolean;
onPageChange: (newPage: number) => void; onPrev: () => void;
onNext: () => void;
} }
export default function Paginate({ export default function Paginate({
currentPage,
hasPrevPage, hasPrevPage,
hasNextPage, hasNextPage,
onPageChange, onPrev,
onNext,
}: PagePaginationProps) { }: PagePaginationProps) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -25,7 +25,7 @@ export default function Paginate({
<Button <Button
variant="default" variant="default"
size="compact-sm" size="compact-sm"
onClick={() => onPageChange(currentPage - 1)} onClick={onPrev}
disabled={!hasPrevPage} disabled={!hasPrevPage}
> >
{t("Prev")} {t("Prev")}
@@ -34,7 +34,7 @@ export default function Paginate({
<Button <Button
variant="default" variant="default"
size="compact-sm" size="compact-sm"
onClick={() => onPageChange(currentPage + 1)} onClick={onNext}
disabled={!hasNextPage} disabled={!hasNextPage}
> >
{t("Next")} {t("Next")}
@@ -4,16 +4,20 @@ import {
UnstyledButton, UnstyledButton,
Badge, Badge,
Table, Table,
ActionIcon, ThemeIcon,
} from '@mantine/core'; Button,
import {Link} from 'react-router-dom'; } from "@mantine/core";
import PageListSkeleton from '@/components/ui/page-list-skeleton.tsx'; import { Link } from "react-router-dom";
import { buildPageUrl } from '@/features/page/page.utils.ts'; import PageListSkeleton from "@/components/ui/page-list-skeleton.tsx";
import { formattedDate } from '@/lib/time.ts'; import { buildPageUrl } from "@/features/page/page.utils.ts";
import { useRecentChangesQuery } from '@/features/page/queries/page-query.ts'; import { formattedDate } from "@/lib/time.ts";
import { IconFileDescription } from '@tabler/icons-react'; import { useRecentChangesQuery } from "@/features/page/queries/page-query.ts";
import { getSpaceUrl } from '@/lib/config.ts'; import { IconFileDescription, IconFiles } from "@tabler/icons-react";
import { EmptyState } from "@/components/ui/empty-state.tsx";
import { getSpaceUrl } from "@/lib/config.ts";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { getInitialsColor } from "@/lib/get-initials-color.ts";
import rowClasses from "@/components/ui/clickable-table-row.module.css";
interface Props { interface Props {
spaceId?: string; spaceId?: string;
@@ -21,7 +25,8 @@ interface Props {
export default function RecentChanges({ spaceId }: Props) { export default function RecentChanges({ spaceId }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const {data: pages, isLoading, isError} = useRecentChangesQuery(spaceId); const { data, isLoading, isError, hasNextPage, fetchNextPage, isFetchingNextPage } = useRecentChangesQuery(spaceId);
const pages = data?.pages.flatMap((p) => p.items) ?? [];
if (isLoading) { if (isLoading) {
return <PageListSkeleton />; return <PageListSkeleton />;
@@ -31,22 +36,24 @@ export default function RecentChanges({spaceId}: Props) {
return <Text>{t("Failed to fetch recent pages")}</Text>; return <Text>{t("Failed to fetch recent pages")}</Text>;
} }
return pages && pages.items.length > 0 ? ( return pages.length > 0 ? (
<>
<Table.ScrollContainer minWidth={500}> <Table.ScrollContainer minWidth={500}>
<Table highlightOnHover verticalSpacing="sm"> <Table highlightOnHover verticalSpacing="sm">
<Table.Tbody> <Table.Tbody>
{pages.items.map((page) => ( {pages.map((page) => (
<Table.Tr key={page.id}> <Table.Tr key={page.id} className={rowClasses.row}>
<Table.Td> <Table.Td>
<UnstyledButton <UnstyledButton
className={rowClasses.link}
component={Link} component={Link}
to={buildPageUrl(page?.space.slug, page.slugId, page.title)} to={buildPageUrl(page?.space.slug, page.slugId, page.title)}
> >
<Group wrap="nowrap"> <Group wrap="nowrap">
{page.icon || ( {page.icon || (
<ActionIcon variant='transparent' color='gray' size={18}> <ThemeIcon variant="transparent" color="gray" size={18}>
<IconFileDescription size={18} /> <IconFileDescription size={18} />
</ActionIcon> </ThemeIcon>
)} )}
<Text fw={500} size="md" lineClamp={1}> <Text fw={500} size="md" lineClamp={1}>
@@ -58,18 +65,23 @@ export default function RecentChanges({spaceId}: Props) {
{!spaceId && ( {!spaceId && (
<Table.Td> <Table.Td>
<Badge <Badge
color="blue" color={getInitialsColor(page?.space.name)}
variant="light" variant="light"
component={Link} component={Link}
to={getSpaceUrl(page?.space.slug)} to={getSpaceUrl(page?.space.slug)}
style={{cursor: 'pointer'}} style={{ cursor: "pointer" }}
> >
{page?.space.name} {page?.space.name}
</Badge> </Badge>
</Table.Td> </Table.Td>
)} )}
<Table.Td> <Table.Td>
<Text c="dimmed" style={{whiteSpace: 'nowrap'}} size="xs" fw={500}> <Text
c="dimmed"
style={{ whiteSpace: "nowrap" }}
size="xs"
fw={500}
>
{formattedDate(page.updatedAt)} {formattedDate(page.updatedAt)}
</Text> </Text>
</Table.Td> </Table.Td>
@@ -78,9 +90,24 @@ export default function RecentChanges({spaceId}: Props) {
</Table.Tbody> </Table.Tbody>
</Table> </Table>
</Table.ScrollContainer> </Table.ScrollContainer>
{hasNextPage && (
<Button
variant="subtle"
fullWidth
mt="sm"
mb="xl"
onClick={() => fetchNextPage()}
loading={isFetchingNextPage}
>
{t("Load more")}
</Button>
)}
</>
) : ( ) : (
<Text size="md" ta="center"> <EmptyState
{t("No pages yet")} icon={IconFiles}
</Text> title={t("No pages yet")}
description={t("Pages you create will show up here.")}
/>
); );
} }
@@ -6,12 +6,14 @@ import { useTranslation } from "react-i18next";
export interface SearchInputProps { export interface SearchInputProps {
placeholder?: string; placeholder?: string;
ariaLabel?: string;
debounceDelay?: number; debounceDelay?: number;
onSearch: (value: string) => void; onSearch: (value: string) => void;
} }
export function SearchInput({ export function SearchInput({
placeholder, placeholder,
ariaLabel,
debounceDelay = 500, debounceDelay = 500,
onSearch, onSearch,
}: SearchInputProps) { }: SearchInputProps) {
@@ -28,6 +30,7 @@ export function SearchInput({
<TextInput <TextInput
size="sm" size="sm"
placeholder={placeholder || t("Search...")} placeholder={placeholder || t("Search...")}
aria-label={ariaLabel || placeholder || t("Search")}
leftSection={<IconSearch size={16} />} leftSection={<IconSearch size={16} />}
value={value} value={value}
onChange={(e) => setValue(e.currentTarget.value)} onChange={(e) => setValue(e.currentTarget.value)}
@@ -0,0 +1,24 @@
import { Group, Text } from "@mantine/core";
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
import React from "react";
import { IUser } from '@/features/user/types/user.types.ts';
interface UserInfoProps {
user: Partial<IUser>;
size?: string;
}
export function UserInfo({ user, size }: UserInfoProps) {
return (
<Group gap="sm" wrap="nowrap">
<CustomAvatar avatarUrl={user?.avatarUrl} name={user?.name} size={size} />
<div>
<Text fz="sm" fw={500} lineClamp={1}>
{user?.name}
</Text>
<Text fz="xs" c="dimmed">
{user?.email}
</Text>
</div>
</Group>
);
}
@@ -0,0 +1,20 @@
import { rem } from "@mantine/core";
interface Props {
size?: number | string;
}
export function ConfluenceIcon({ size }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
style={{ width: rem(size), height: rem(size) }}
>
<path d="M.87 18.257c-.248.382-.53.875-.763 1.245a.764.764 0 0 0 .255 1.04l4.965 3.054a.764.764 0 0 0 1.058-.26c.199-.332.454-.763.733-1.221 1.967-3.247 3.945-2.853 7.508-1.146l4.957 2.337a.764.764 0 0 0 1.028-.382l2.364-5.346a.764.764 0 0 0-.382-1 599.851 599.851 0 0 1-4.965-2.361C10.911 10.97 5.224 11.185.87 18.257zM23.131 5.743c.249-.405.531-.875.764-1.25a.764.764 0 0 0-.256-1.034L18.675.404a.764.764 0 0 0-1.058.26c-.195.335-.451.763-.734 1.225-1.966 3.246-3.945 2.85-7.508 1.146L4.437.694a.764.764 0 0 0-1.027.382L1.046 6.422a.764.764 0 0 0 .382 1c1.039.49 3.105 1.467 4.965 2.361 6.698 3.246 12.392 3.029 16.738-4.04z" />
</svg>
);
}
@@ -0,0 +1,27 @@
import { rem } from "@mantine/core";
type Props = {
size?: number | string;
stroke?: number;
};
export function IconColumns4({ size = 24, stroke = 2 }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={rem(size)}
height={rem(size)}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={stroke}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3 4a1 1 0 0 1 1 -1h16a1 1 0 0 1 1 1v16a1 1 0 0 1 -1 1h-16a1 1 0 0 1 -1 -1v-16" />
<path d="M7.5 3v18" />
<path d="M12 3v18" />
<path d="M16.5 3v18" />
</svg>
);
}
@@ -0,0 +1,28 @@
import { rem } from "@mantine/core";
type Props = {
size?: number | string;
stroke?: number;
};
export function IconColumns5({ size = 24, stroke = 2 }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={rem(size)}
height={rem(size)}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={stroke}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3 4a1 1 0 0 1 1 -1h16a1 1 0 0 1 1 1v16a1 1 0 0 1 -1 1h-16a1 1 0 0 1 -1 -1v-16" />
<path d="M6.6 3v18" />
<path d="M10.2 3v18" />
<path d="M13.8 3v18" />
<path d="M17.4 3v18" />
</svg>
);
}
@@ -1,11 +1,11 @@
import { ActionIcon, rem } from "@mantine/core"; import { ThemeIcon } from "@mantine/core";
import React from "react"; import React from "react";
import { IconUsersGroup } from "@tabler/icons-react"; import { IconUsersGroup } from "@tabler/icons-react";
export function IconGroupCircle() { export function IconGroupCircle() {
return ( return (
<ActionIcon variant="light" size="lg" color="gray" radius="xl"> <ThemeIcon variant="light" size="lg" color="gray" radius="xl">
<IconUsersGroup stroke={1.5} /> <IconUsersGroup stroke={1.5} />
</ActionIcon> </ThemeIcon>
); );
} }
@@ -7,6 +7,19 @@
padding-right: var(--mantine-spacing-md); padding-right: var(--mantine-spacing-md);
} }
.brand {
display: flex;
align-items: center;
text-decoration: none;
color: inherit;
cursor: pointer;
}
.brandIcon {
display: flex;
align-items: center;
}
.link { .link {
display: block; display: block;
line-height: 1; line-height: 1;
@@ -16,6 +29,9 @@
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0)); color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
font-size: var(--mantine-font-size-sm); font-size: var(--mantine-font-size-sm);
font-weight: 500; font-weight: 500;
user-select: none;
white-space: nowrap;
flex-shrink: 0;
@mixin hover { @mixin hover {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
@@ -1,8 +1,18 @@
import { Badge, Group, Text, Tooltip } from "@mantine/core"; import {
ActionIcon,
Badge,
Box,
Group,
Text,
Tooltip,
UnstyledButton,
} from "@mantine/core";
import classes from "./app-header.module.css"; import classes from "./app-header.module.css";
import React from "react"; import React from "react";
import TopMenu from "@/components/layouts/global/top-menu.tsx"; import TopMenu from "@/components/layouts/global/top-menu.tsx";
import { Link } from "react-router-dom"; import { Link, useLocation } from "react-router-dom";
import { IconSparkles } from "@tabler/icons-react";
import useToggleAside from "@/hooks/use-toggle-aside.tsx";
import APP_ROUTE from "@/lib/app-route.ts"; import APP_ROUTE from "@/lib/app-route.ts";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { import {
@@ -14,8 +24,20 @@ import SidebarToggle from "@/components/ui/sidebar-toggle-button.tsx";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import useTrial from "@/ee/hooks/use-trial.tsx"; import useTrial from "@/ee/hooks/use-trial.tsx";
import { isCloud } from "@/lib/config.ts"; import { isCloud } from "@/lib/config.ts";
import {
SearchControl,
SearchMobileControl,
} from "@/features/search/components/search-control.tsx";
import {
searchSpotlight,
shareSearchSpotlight,
} from "@/features/search/constants.ts";
import { NotificationPopover } from "@/features/notification/components/notification-popover.tsx";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
const links = [{ link: APP_ROUTE.HOME, label: "Home" }]; const links = [
{ link: APP_ROUTE.HOME, label: "Home" },
];
export function AppHeader() { export function AppHeader() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -25,8 +47,12 @@ export function AppHeader() {
const [desktopOpened] = useAtom(desktopSidebarAtom); const [desktopOpened] = useAtom(desktopSidebarAtom);
const toggleDesktop = useToggleSidebar(desktopSidebarAtom); const toggleDesktop = useToggleSidebar(desktopSidebarAtom);
const { isTrial, trialDaysLeft } = useTrial(); const { isTrial, trialDaysLeft } = useTrial();
const location = useLocation();
const toggleAside = useToggleAside();
const [workspace] = useAtom(workspaceAtom);
const aiChatEnabled = workspace?.settings?.ai?.chat === true;
const isHomeRoute = location.pathname.startsWith("/home"); const isPageRoute = location.pathname.includes("/p/");
const items = links.map((link) => ( const items = links.map((link) => (
<Link key={link.label} to={link.link} className={classes.link}> <Link key={link.label} to={link.link} className={classes.link}>
@@ -38,8 +64,6 @@ export function AppHeader() {
<> <>
<Group h="100%" px="md" justify="space-between" wrap={"nowrap"}> <Group h="100%" px="md" justify="space-between" wrap={"nowrap"}>
<Group wrap="nowrap"> <Group wrap="nowrap">
{!isHomeRoute && (
<>
<Tooltip label={t("Sidebar toggle")}> <Tooltip label={t("Sidebar toggle")}>
<SidebarToggle <SidebarToggle
aria-label={t("Sidebar toggle")} aria-label={t("Sidebar toggle")}
@@ -59,25 +83,85 @@ export function AppHeader() {
size="sm" size="sm"
/> />
</Tooltip> </Tooltip>
</>
)}
<Link to="/home" className={classes.brand} aria-label="Docmost">
<Box hiddenFrom="sm" className={classes.brandIcon}>
<img
src="/icons/favicon-32x32.png"
alt="Docmost"
width={22}
height={22}
/>
</Box>
<Text <Text
size="lg" size="lg"
fw={600} fw={600}
style={{ cursor: "pointer", userSelect: "none" }} style={{ userSelect: "none" }}
component={Link} visibleFrom="sm"
to="/home"
> >
Docmost Docmost
</Text> </Text>
</Link>
<Group ml={50} gap={5} className={classes.links} visibleFrom="sm"> <Group ml={50} gap={5} className={classes.links} visibleFrom="sm">
{items} {items}
</Group> </Group>
</Group> </Group>
<div>
<Group visibleFrom="sm">
<SearchControl onClick={searchSpotlight.open} />
</Group>
<Group hiddenFrom="sm">
<SearchMobileControl onSearch={searchSpotlight.open} />
</Group>
</div>
<Group px={"xl"} wrap="nowrap"> <Group px={"xl"} wrap="nowrap">
{aiChatEnabled && (
<>
<UnstyledButton
component={Link}
to="/ai"
className={classes.link}
visibleFrom="sm"
onClick={(e: React.MouseEvent) => {
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) {
return;
}
if (isPageRoute) {
e.preventDefault();
toggleAside("chat");
}
}}
>
{t("AI Chat")}
</UnstyledButton>
<Tooltip label={t("AI Chat")} openDelay={250} withArrow>
<ActionIcon
component={Link}
to="/ai"
variant="subtle"
color="dark"
size="sm"
hiddenFrom="sm"
aria-label={t("AI Chat")}
onClick={(e: React.MouseEvent) => {
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) {
return;
}
if (isPageRoute) {
e.preventDefault();
toggleAside("chat");
}
}}
>
<IconSparkles size={20} stroke={2} />
</ActionIcon>
</Tooltip>
</>
)}
<NotificationPopover />
{isCloud() && isTrial && trialDaysLeft !== 0 && ( {isCloud() && isTrial && trialDaysLeft !== 0 && (
<Badge <Badge
variant="light" variant="light"
@@ -27,5 +27,3 @@
background: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-5)) background: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-5))
} }
} }
@@ -1,43 +1,76 @@
import { Box, ScrollArea, Text } from "@mantine/core"; import { ActionIcon, Box, Group, ScrollArea, Title, Tooltip } from "@mantine/core";
import CommentList from "@/features/comment/components/comment-list.tsx"; import { IconX } from "@tabler/icons-react";
import CommentListWithTabs from "@/features/comment/components/comment-list-with-tabs.tsx";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts"; import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
import React, { ReactNode } from "react"; import React, { ReactNode, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { TableOfContents } from "@/features/editor/components/table-of-contents/table-of-contents.tsx"; import { TableOfContents } from "@/features/editor/components/table-of-contents/table-of-contents.tsx";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { pageEditorAtom } from "@/features/editor/atoms/editor-atoms.ts"; import { pageEditorAtom } from "@/features/editor/atoms/editor-atoms.ts";
import AsideChatPanel from "@/ee/ai-chat/components/aside-chat-panel";
import { PageDetailsAside } from "@/features/page-details/components/page-details-aside.tsx";
import { ASIDE_PANEL_ID } from "@/hooks/use-toggle-aside.tsx";
export default function Aside() { export default function Aside() {
const [{ tab }] = useAtom(asideStateAtom); const [{ tab, isAsideOpen }, setAsideState] = useAtom(asideStateAtom);
const { t } = useTranslation(); const { t } = useTranslation();
const pageEditor = useAtomValue(pageEditorAtom); const pageEditor = useAtomValue(pageEditorAtom);
const closeAside = () => setAsideState((s) => ({ ...s, isAsideOpen: false }));
useEffect(() => {
if (!isAsideOpen) return;
document.getElementById(ASIDE_PANEL_ID)?.focus();
}, [isAsideOpen, tab]);
let title: string; let title: string;
let component: ReactNode; let component: ReactNode;
switch (tab) { switch (tab) {
case "comments": case "comments":
component = <CommentList />; component = <CommentListWithTabs />;
title = "Comments"; title = "Comments";
break; break;
case "toc": case "toc":
component = <TableOfContents editor={pageEditor} />; component = <TableOfContents editor={pageEditor} />;
title = "Table of contents"; title = "Table of contents";
break; break;
case "chat":
component = <AsideChatPanel />;
title = "AI Chat";
break;
case "details":
component = <PageDetailsAside />;
title = "Details";
break;
default: default:
component = null; component = null;
title = null; title = null;
} }
return ( return (
<Box p="md"> <Box p="md" style={{ height: "100%", display: "flex", flexDirection: "column" }}>
{component && ( {component && (
<> <>
<Text mb="md" fw={500}> {tab !== "chat" && (
{t(title)} <Group justify="space-between" wrap="nowrap" mb="md">
</Text> <Title order={2} size="h6" fw={500}>{t(title)}</Title>
<Tooltip label={t("Close")} withArrow>
<ActionIcon
variant="subtle"
color="gray"
onClick={closeAside}
aria-label={t("Close")}
>
<IconX size={18} />
</ActionIcon>
</Tooltip>
</Group>
)}
{tab === "comments" || tab === "chat" ? (
component
) : (
<ScrollArea <ScrollArea
style={{ height: "85vh" }} style={{ height: "85vh" }}
scrollbarSize={5} scrollbarSize={5}
@@ -45,6 +78,7 @@ export default function Aside() {
> >
<div style={{ paddingBottom: "200px" }}>{component}</div> <div style={{ paddingBottom: "200px" }}>{component}</div>
</ScrollArea> </ScrollArea>
)}
</> </>
)} )}
</Box> </Box>
@@ -1,6 +1,7 @@
import { AppShell, Container } from "@mantine/core"; import { AppShell, Container } from "@mantine/core";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import { useTranslation } from "react-i18next";
import SettingsSidebar from "@/components/settings/settings-sidebar.tsx"; import SettingsSidebar from "@/components/settings/settings-sidebar.tsx";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { import {
@@ -10,20 +11,27 @@ import {
sidebarWidthAtom, sidebarWidthAtom,
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts"; } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
import { SpaceSidebar } from "@/features/space/components/sidebar/space-sidebar.tsx"; import { SpaceSidebar } from "@/features/space/components/sidebar/space-sidebar.tsx";
import AiChatSidebar from "@/ee/ai-chat/components/ai-chat-sidebar.tsx";
import { AppHeader } from "@/components/layouts/global/app-header.tsx"; import { AppHeader } from "@/components/layouts/global/app-header.tsx";
import Aside from "@/components/layouts/global/aside.tsx"; import Aside from "@/components/layouts/global/aside.tsx";
import classes from "./app-shell.module.css"; import classes from "./app-shell.module.css";
import { useTrialEndAction } from "@/ee/hooks/use-trial-end-action.tsx"; import { useTrialEndAction } from "@/ee/hooks/use-trial-end-action.tsx";
import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
import GlobalSidebar from "@/components/layouts/global/global-sidebar.tsx";
import { ASIDE_PANEL_ID } from "@/hooks/use-toggle-aside.tsx";
import { MAIN_CONTENT_ID, SkipToMain } from "@/components/ui/skip-to-main.tsx";
export default function GlobalAppShell({ export default function GlobalAppShell({
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const { t } = useTranslation();
useTrialEndAction(); useTrialEndAction();
const [mobileOpened] = useAtom(mobileSidebarAtom); const [mobileOpened] = useAtom(mobileSidebarAtom);
const toggleMobile = useToggleSidebar(mobileSidebarAtom);
const [desktopOpened] = useAtom(desktopSidebarAtom); const [desktopOpened] = useAtom(desktopSidebarAtom);
const [{ isAsideOpen }] = useAtom(asideStateAtom); const [{ isAsideOpen, tab: asideTab }] = useAtom(asideStateAtom);
const [sidebarWidth, setSidebarWidth] = useAtom(sidebarWidthAtom); const [sidebarWidth, setSidebarWidth] = useAtom(sidebarWidthAtom);
const [isResizing, setIsResizing] = useState(false); const [isResizing, setIsResizing] = useState(false);
const sidebarRef = useRef(null); const sidebarRef = useRef(null);
@@ -70,22 +78,23 @@ export default function GlobalAppShell({
const location = useLocation(); const location = useLocation();
const isSettingsRoute = location.pathname.startsWith("/settings"); const isSettingsRoute = location.pathname.startsWith("/settings");
const isSpaceRoute = location.pathname.startsWith("/s/"); const isSpaceRoute = location.pathname.startsWith("/s/");
const isHomeRoute = location.pathname.startsWith("/home"); const isAiRoute = location.pathname.startsWith("/ai");
const isPageRoute = location.pathname.includes("/p/"); const isPageRoute = location.pathname.includes("/p/");
const showGlobalSidebar = !isSpaceRoute && !isSettingsRoute && !isAiRoute;
return ( return (
<>
<SkipToMain />
<AppShell <AppShell
header={{ height: 45 }} header={{ height: 45 }}
navbar={ navbar={{
!isHomeRoute && {
width: isSpaceRoute ? sidebarWidth : 300, width: isSpaceRoute ? sidebarWidth : 300,
breakpoint: "sm", breakpoint: "sm",
collapsed: { collapsed: {
mobile: !mobileOpened, mobile: !mobileOpened,
desktop: !desktopOpened, desktop: !desktopOpened,
}, },
} }}
}
aside={ aside={
isPageRoute && { isPageRoute && {
width: 350, width: 350,
@@ -98,30 +107,61 @@ export default function GlobalAppShell({
<AppShell.Header px="md" className={classes.header}> <AppShell.Header px="md" className={classes.header}>
<AppHeader /> <AppHeader />
</AppShell.Header> </AppShell.Header>
{!isHomeRoute && (
<AppShell.Navbar <AppShell.Navbar
className={classes.navbar} className={classes.navbar}
withBorder={false} withBorder={false}
ref={sidebarRef} ref={sidebarRef}
aria-label={
isSpaceRoute
? t("Space navigation")
: isSettingsRoute
? t("Settings navigation")
: isAiRoute
? t("AI navigation")
: t("Main navigation")
}
> >
{isSpaceRoute && (
<div className={classes.resizeHandle} onMouseDown={startResizing} /> <div className={classes.resizeHandle} onMouseDown={startResizing} />
)}
{isSpaceRoute && <SpaceSidebar />} {isSpaceRoute && <SpaceSidebar />}
{isSettingsRoute && <SettingsSidebar />} {isSettingsRoute && <SettingsSidebar />}
{isAiRoute && <AiChatSidebar />}
{showGlobalSidebar && <GlobalSidebar />}
</AppShell.Navbar> </AppShell.Navbar>
)} <AppShell.Main id={MAIN_CONTENT_ID} tabIndex={-1}>
<AppShell.Main>
{isSettingsRoute ? ( {isSettingsRoute ? (
<Container size={800}>{children}</Container> <Container size={900} pb={80}>
{children}
</Container>
) : ( ) : (
children children
)} )}
</AppShell.Main> </AppShell.Main>
{isPageRoute && ( {isPageRoute && (
<AppShell.Aside className={classes.aside} p="md" withBorder={false}> <AppShell.Aside
id={ASIDE_PANEL_ID}
tabIndex={-1}
className={classes.aside}
p="md"
withBorder={false}
aria-label={
asideTab === "comments"
? t("Comments")
: asideTab === "toc"
? t("Table of contents")
: asideTab === "chat"
? t("AI Chat")
: asideTab === "details"
? t("Details")
: undefined
}
>
<Aside /> <Aside />
</AppShell.Aside> </AppShell.Aside>
)} )}
</AppShell> </AppShell>
</>
); );
} }
@@ -0,0 +1,109 @@
.navbar {
height: 100%;
width: 100%;
padding: var(--mantine-spacing-md);
display: flex;
flex-direction: column;
}
.section {
padding-bottom: var(--mantine-spacing-xs);
}
.link {
cursor: pointer;
display: flex;
align-items: center;
text-decoration: none;
font-size: var(--mantine-font-size-sm);
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
padding-left: var(--mantine-spacing-xs);
min-height: 30px;
border-radius: var(--mantine-radius-sm);
font-weight: 500;
user-select: none;
@mixin hover {
background-color: light-dark(
var(--mantine-color-gray-1),
var(--mantine-color-dark-6)
);
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
}
&:focus-visible {
outline: 2px solid var(--mantine-primary-color-filled);
outline-offset: 2px;
}
&[data-active] {
&,
& :hover {
background-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6));
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
}
}
&[data-disabled] {
cursor: not-allowed;
opacity: 0.5;
@mixin hover {
background-color: transparent;
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
}
}
}
.linkIcon {
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
margin-right: var(--mantine-spacing-sm);
width: rem(16px);
height: rem(16px);
}
.sectionHeader {
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
font-size: var(--mantine-font-size-xs);
color: var(--mantine-color-dimmed);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.spacer {
flex: 1;
}
.bottomSection {
padding-top: var(--mantine-spacing-xs);
border-top: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}
.spaceItem {
cursor: pointer;
display: flex;
align-items: center;
gap: var(--mantine-spacing-sm);
text-decoration: none;
font-size: var(--mantine-font-size-sm);
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
padding-left: var(--mantine-spacing-xs);
min-height: 30px;
border-radius: var(--mantine-radius-sm);
font-weight: 500;
user-select: none;
@mixin hover {
background-color: light-dark(
var(--mantine-color-gray-1),
var(--mantine-color-dark-6)
);
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
}
&:focus-visible {
outline: 2px solid var(--mantine-primary-color-filled);
outline-offset: 2px;
}
}
@@ -0,0 +1,186 @@
import { useEffect, useState } from "react";
import { ScrollArea, Text, Divider, Modal, UnstyledButton, Tooltip } from "@mantine/core";
import {
IconHome,
IconClock,
IconStar,
IconLayoutGrid,
IconSettings,
IconUserPlus,
IconTemplate,
} from "@tabler/icons-react";
import { Link, useLocation } from "react-router-dom";
import classes from "./global-sidebar.module.css";
import { useTranslation } from "react-i18next";
import { useAtom } from "jotai";
import { mobileSidebarAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom";
import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar";
import { useFavoritesQuery } from "@/features/favorite/queries/favorite-query";
import { getSpaceUrl } from "@/lib/config";
import { useDisclosure } from "@mantine/hooks";
import { WorkspaceInviteForm } from "@/features/workspace/components/members/components/workspace-invite-form";
import { CustomAvatar } from "@/components/ui/custom-avatar";
import { AvatarIconType } from "@/features/attachments/types/attachment.types";
import { useHasFeature } from "@/ee/hooks/use-feature";
import { Feature } from "@/ee/features";
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
export default function GlobalSidebar() {
const { t } = useTranslation();
const location = useLocation();
const [active, setActive] = useState(location.pathname);
const [mobileSidebarOpened] = useAtom(mobileSidebarAtom);
const toggleMobileSidebar = useToggleSidebar(mobileSidebarAtom);
const hasTemplates = useHasFeature(Feature.TEMPLATES);
const upgradeLabel = useUpgradeLabel();
const mainNavItems = [
{ label: "Home", icon: IconHome, path: "/home" },
{ label: "Favorites", icon: IconStar, path: "/favorites" },
{ label: "Spaces", icon: IconLayoutGrid, path: "/spaces" },
{
label: "Templates",
icon: IconTemplate,
path: "/templates",
disabled: !hasTemplates,
},
];
const { data: favoriteSpacesData, isPending: isFavoritesPending } = useFavoritesQuery("space");
const favoriteSpaces = favoriteSpacesData?.pages.flatMap((p) => p.items) ?? [];
const sortedFavoriteSpaces = [...favoriteSpaces]
.filter((fav) => fav.space)
.sort((a, b) => {
const cmp = (a.space!.name ?? "").localeCompare(b.space!.name ?? "", undefined, { sensitivity: "base" });
return cmp !== 0 ? cmp : a.id.localeCompare(b.id);
});
const [inviteOpened, { open: openInvite, close: closeInvite }] =
useDisclosure(false);
useEffect(() => {
setActive(location.pathname);
}, [location.pathname]);
const handleNavClick = () => {
if (mobileSidebarOpened) {
toggleMobileSidebar();
}
};
return (
<div className={classes.navbar}>
<ScrollArea w="100%" style={{ flex: 1 }}>
<div className={classes.section}>
{mainNavItems.map((item) =>
item.disabled ? (
<Tooltip
key={item.label}
label={upgradeLabel}
position="right"
withArrow
>
<UnstyledButton
className={classes.link}
data-disabled
aria-disabled="true"
tabIndex={-1}
>
<item.icon className={classes.linkIcon} stroke={2} />
<span>{t(item.label)}</span>
</UnstyledButton>
</Tooltip>
) : (
<Link
key={item.label}
className={classes.link}
data-active={active === item.path || undefined}
aria-current={active === item.path ? "page" : undefined}
to={item.path}
onClick={handleNavClick}
>
<item.icon className={classes.linkIcon} stroke={2} />
<span>{t(item.label)}</span>
</Link>
),
)}
</div>
<Divider my="xs" />
<div className={classes.section}>
<Text className={classes.sectionHeader}>{t("Favorite spaces")}</Text>
{!isFavoritesPending && sortedFavoriteSpaces.length === 0 ? (
<Text size="xs" c="dimmed" pl="xs" py={4}>
{t("Favorite spaces appear here")}
</Text>
) : (
<>
{sortedFavoriteSpaces.slice(0, 10).map((fav) => (
<Link
key={fav.id}
className={classes.spaceItem}
to={getSpaceUrl(fav.space!.slug)}
onClick={handleNavClick}
>
<CustomAvatar
name={fav.space!.name}
avatarUrl={fav.space!.logo}
type={AvatarIconType.SPACE_ICON}
color="initials"
variant="filled"
size={20}
/>
<Text size="sm" fw={500} lineClamp={1}>
{fav.space!.name}
</Text>
</Link>
))}
{sortedFavoriteSpaces.length > 10 && (
<Link
className={classes.spaceItem}
to="/spaces"
onClick={handleNavClick}
>
<Text size="xs" c="dimmed">
{t("View all")}
</Text>
</Link>
)}
</>
)}
</div>
</ScrollArea>
<div className={classes.bottomSection}>
<UnstyledButton
className={classes.link}
onClick={openInvite}
>
<IconUserPlus className={classes.linkIcon} stroke={2} />
<span>{t("Invite People")}</span>
</UnstyledButton>
<Link
className={classes.link}
data-active={active.startsWith("/settings") || undefined}
aria-current={active.startsWith("/settings") ? "page" : undefined}
to="/settings/account/profile"
onClick={handleNavClick}
>
<IconSettings className={classes.linkIcon} stroke={2} />
<span>{t("Settings")}</span>
</Link>
</div>
<Modal
size="550"
opened={inviteOpened}
onClose={closeInvite}
title={t("Invite new members")}
centered
>
<Divider size="xs" mb="xs" />
<ScrollArea h="80%">
<WorkspaceInviteForm onClose={closeInvite} />
</ScrollArea>
</Modal>
</div>
);
}
@@ -10,6 +10,7 @@ export const desktopSidebarAtom = atomWithWebStorage<boolean>(
export const desktopAsideAtom = atom<boolean>(false); export const desktopAsideAtom = atom<boolean>(false);
// Valid `tab` values: "" | "comments" | "toc" | "chat" | "details"
type AsideStateType = { type AsideStateType = {
tab: string; tab: string;
isAsideOpen: boolean; isAsideOpen: boolean;
@@ -1,13 +1,23 @@
import { UserProvider } from "@/features/user/user-provider.tsx"; import { UserProvider } from "@/features/user/user-provider.tsx";
import { Outlet } from "react-router-dom"; import { Outlet, useParams } from "react-router-dom";
import GlobalAppShell from "@/components/layouts/global/global-app-shell.tsx"; import GlobalAppShell from "@/components/layouts/global/global-app-shell.tsx";
import { PosthogUser } from "@/ee/components/posthog-user.tsx";
import { isCloud } from "@/lib/config.ts";
import { SearchSpotlight } from "@/features/search/components/search-spotlight.tsx";
import React from "react";
import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts";
export default function Layout() { export default function Layout() {
const { spaceSlug } = useParams();
const { data: space } = useGetSpaceBySlugQuery(spaceSlug);
return ( return (
<UserProvider> <UserProvider>
<GlobalAppShell> <GlobalAppShell>
<Outlet /> <Outlet />
</GlobalAppShell> </GlobalAppShell>
{isCloud() && <PosthogUser />}
<SearchSpotlight spaceId={space?.id} />
</UserProvider> </UserProvider>
); );
} }
@@ -1,9 +1,20 @@
import { Group, Menu, UnstyledButton, Text } from "@mantine/core";
import { import {
Group,
Menu,
Text,
UnstyledButton,
useMantineColorScheme,
} from "@mantine/core";
import {
IconBrightnessFilled,
IconBrush, IconBrush,
IconCheck,
IconChevronDown, IconChevronDown,
IconDeviceDesktop,
IconLogout, IconLogout,
IconMoon,
IconSettings, IconSettings,
IconSun,
IconUserCircle, IconUserCircle,
IconUsers, IconUsers,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
@@ -14,11 +25,13 @@ import APP_ROUTE from "@/lib/app-route.ts";
import useAuth from "@/features/auth/hooks/use-auth.ts"; import useAuth from "@/features/auth/hooks/use-auth.ts";
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
export default function TopMenu() { export default function TopMenu() {
const { t } = useTranslation(); const { t } = useTranslation();
const [currentUser] = useAtom(currentUserAtom); const [currentUser] = useAtom(currentUserAtom);
const { logout } = useAuth(); const { logout } = useAuth();
const { colorScheme, setColorScheme } = useMantineColorScheme();
const user = currentUser?.user; const user = currentUser?.user;
const workspace = currentUser?.workspace; const workspace = currentUser?.workspace;
@@ -37,6 +50,7 @@ export default function TopMenu() {
name={workspace?.name} name={workspace?.name}
variant="filled" variant="filled"
size="sm" size="sm"
type={AvatarIconType.WORKSPACE_ICON}
/> />
<Text fw={500} size="sm" lh={1} mr={3} lineClamp={1}> <Text fw={500} size="sm" lh={1} mr={3} lineClamp={1}>
{workspace?.name} {workspace?.name}
@@ -101,6 +115,44 @@ export default function TopMenu() {
{t("My preferences")} {t("My preferences")}
</Menu.Item> </Menu.Item>
<Menu.Sub>
<Menu.Sub.Target>
<Menu.Sub.Item leftSection={<IconBrightnessFilled size={16} />}>
{t("Theme")}
</Menu.Sub.Item>
</Menu.Sub.Target>
<Menu.Sub.Dropdown>
<Menu.Item
onClick={() => setColorScheme("light")}
leftSection={<IconSun size={16} />}
rightSection={
colorScheme === "light" ? <IconCheck size={16} /> : null
}
>
{t("Light")}
</Menu.Item>
<Menu.Item
onClick={() => setColorScheme("dark")}
leftSection={<IconMoon size={16} />}
rightSection={
colorScheme === "dark" ? <IconCheck size={16} /> : null
}
>
{t("Dark")}
</Menu.Item>
<Menu.Item
onClick={() => setColorScheme("auto")}
leftSection={<IconDeviceDesktop size={16} />}
rightSection={
colorScheme === "auto" ? <IconCheck size={16} /> : null
}
>
{t("System settings")}
</Menu.Item>
</Menu.Sub.Dropdown>
</Menu.Sub>
<Menu.Divider /> <Menu.Divider />
<Menu.Item onClick={logout} leftSection={<IconLogout size={16} />}> <Menu.Item onClick={logout} leftSection={<IconLogout size={16} />}>
@@ -35,6 +35,12 @@ export default function AppVersion() {
position="middle-end" position="middle-end"
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
disabled={!hasUpdate} disabled={!hasUpdate}
onClick={() => {
window.open(
"https://github.com/docmost/docmost/releases",
"_blank",
);
}}
> >
<Text <Text
size="sm" size="sm"
@@ -44,7 +50,7 @@ export default function AppVersion() {
href="https://github.com/docmost/docmost/releases" href="https://github.com/docmost/docmost/releases"
target="_blank" target="_blank"
> >
v{APP_VERSION} {appVersion?.currentVersion && <>v{appVersion?.currentVersion}</>}
</Text> </Text>
</Indicator> </Indicator>
</Tooltip> </Tooltip>
@@ -0,0 +1,10 @@
import { atom, WritableAtom } from "jotai";
export const settingsOriginAtom: WritableAtom<string | null, [string | null], void> = atom(
null,
(get, set, newValue) => {
if (get(settingsOriginAtom) !== newValue) {
set(settingsOriginAtom, newValue);
}
}
);
@@ -8,10 +8,15 @@ import { getGroups } from "@/features/group/services/group-service.ts";
import { QueryParams } from "@/lib/types.ts"; import { QueryParams } from "@/lib/types.ts";
import { getWorkspaceMembers } from "@/features/workspace/services/workspace-service.ts"; import { getWorkspaceMembers } from "@/features/workspace/services/workspace-service.ts";
import { getLicenseInfo } from "@/ee/licence/services/license-service.ts"; import { getLicenseInfo } from "@/ee/licence/services/license-service.ts";
import { getSsoProviders } from '@/ee/security/services/security-service.ts'; import { getSsoProviders } from "@/ee/security/services/security-service.ts";
import { getShares } from "@/features/share/services/share-service.ts";
import { getApiKeys } from "@/ee/api-key";
import { getAuditLogs } from "@/ee/audit/services/audit-service";
import { getVerificationList } from "@/ee/page-verification/services/page-verification-service";
import { getScimTokens } from "@/ee/scim/services/scim-token-service";
export const prefetchWorkspaceMembers = () => { export const prefetchWorkspaceMembers = () => {
const params = { limit: 100, page: 1, query: "" } as QueryParams; const params: QueryParams = { limit: 100, query: "" };
queryClient.prefetchQuery({ queryClient.prefetchQuery({
queryKey: ["workspaceMembers", params], queryKey: ["workspaceMembers", params],
queryFn: () => getWorkspaceMembers(params), queryFn: () => getWorkspaceMembers(params),
@@ -20,15 +25,15 @@ export const prefetchWorkspaceMembers = () => {
export const prefetchSpaces = () => { export const prefetchSpaces = () => {
queryClient.prefetchQuery({ queryClient.prefetchQuery({
queryKey: ["spaces", { page: 1 }], queryKey: ["spaces", {}],
queryFn: () => getSpaces({ page: 1 }), queryFn: () => getSpaces({}),
}); });
}; };
export const prefetchGroups = () => { export const prefetchGroups = () => {
queryClient.prefetchQuery({ queryClient.prefetchQuery({
queryKey: ["groups", { page: 1 }], queryKey: ["groups", {}],
queryFn: () => getGroups({ page: 1 }), queryFn: () => getGroups({}),
}); });
}; };
@@ -57,3 +62,47 @@ export const prefetchSsoProviders = () => {
queryFn: () => getSsoProviders(), queryFn: () => getSsoProviders(),
}); });
}; };
export const prefetchShares = () => {
queryClient.prefetchQuery({
queryKey: ["share-list", {}],
queryFn: () => getShares({}),
});
};
export const prefetchApiKeys = () => {
queryClient.prefetchQuery({
queryKey: ["api-key-list", {}],
queryFn: () => getApiKeys({}),
});
};
export const prefetchApiKeyManagement = () => {
queryClient.prefetchQuery({
queryKey: ["api-key-list", { adminView: true }],
queryFn: () => getApiKeys({ adminView: true }),
});
};
export const prefetchAuditLogs = () => {
const params = { limit: 50 };
queryClient.prefetchQuery({
queryKey: ["audit-logs", params],
queryFn: () => getAuditLogs(params),
});
};
export const prefetchVerifiedPages = () => {
const params = { limit: 50 };
queryClient.prefetchQuery({
queryKey: ["verification-list", params],
queryFn: () => getVerificationList(params),
});
};
export const prefetchScimTokens = () => {
queryClient.prefetchQuery({
queryKey: ["scim-token-list", { cursor: undefined }],
queryFn: () => getScimTokens({}),
});
};
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Group, Text, ScrollArea, ActionIcon } from "@mantine/core"; import { Group, Text, ScrollArea, ActionIcon, Tooltip } from "@mantine/core";
import { import {
IconUser, IconUser,
IconSettings, IconSettings,
@@ -11,38 +11,52 @@ import {
IconCoin, IconCoin,
IconLock, IconLock,
IconKey, IconKey,
IconWorld,
IconSparkles,
IconHistory,
IconShieldCheck,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { Link, useLocation, useNavigate } from "react-router-dom"; import { Link, useLocation } from "react-router-dom";
import classes from "./settings.module.css"; import classes from "./settings.module.css";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { isCloud } from "@/lib/config.ts"; import { isCloud } from "@/lib/config.ts";
import useUserRole from "@/hooks/use-user-role.tsx"; import useUserRole from "@/hooks/use-user-role.tsx";
import { useAtom } from "jotai/index"; import { useAtom } from "jotai";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts"; import { entitlementAtom } from "@/ee/entitlement/entitlement-atom";
import { Feature } from "@/ee/features";
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
import { import {
prefetchApiKeyManagement,
prefetchApiKeys,
prefetchBilling, prefetchBilling,
prefetchGroups, prefetchGroups,
prefetchLicense, prefetchLicense,
prefetchScimTokens,
prefetchShares,
prefetchSpaces, prefetchSpaces,
prefetchSsoProviders, prefetchSsoProviders,
prefetchWorkspaceMembers, prefetchWorkspaceMembers,
prefetchAuditLogs,
prefetchVerifiedPages,
} from "@/components/settings/settings-queries.tsx"; } from "@/components/settings/settings-queries.tsx";
import AppVersion from "@/components/settings/app-version.tsx"; import AppVersion from "@/components/settings/app-version.tsx";
import { mobileSidebarAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
import { useSettingsNavigation } from "@/hooks/use-settings-navigation";
interface DataItem { type DataItem = {
label: string; label: string;
icon: React.ElementType; icon: React.ElementType;
path: string; path: string;
isCloud?: boolean; feature?: string;
isEnterprise?: boolean; role?: "admin" | "owner";
isAdmin?: boolean; env?: "cloud" | "selfhosted";
isSelfhosted?: boolean; };
}
interface DataGroup { type DataGroup = {
heading: string; heading: string;
items: DataItem[]; items: DataItem[];
} };
const groupedData: DataGroup[] = [ const groupedData: DataGroup[] = [
{ {
@@ -54,34 +68,63 @@ const groupedData: DataGroup[] = [
icon: IconBrush, icon: IconBrush,
path: "/settings/account/preferences", path: "/settings/account/preferences",
}, },
{
label: "API keys",
icon: IconKey,
path: "/settings/account/api-keys",
feature: Feature.API_KEYS,
},
], ],
}, },
{ {
heading: "Workspace", heading: "Workspace",
items: [ items: [
{ label: "General", icon: IconSettings, path: "/settings/workspace" }, { label: "General", icon: IconSettings, path: "/settings/workspace" },
{ { label: "Members", icon: IconUsers, path: "/settings/members" },
label: "Members",
icon: IconUsers,
path: "/settings/members",
},
{ {
label: "Billing", label: "Billing",
icon: IconCoin, icon: IconCoin,
path: "/settings/billing", path: "/settings/billing",
isCloud: true, role: "admin",
isAdmin: true, env: "cloud",
}, },
{ {
label: "Security & SSO", label: "Security & SSO",
icon: IconLock, icon: IconLock,
path: "/settings/security", path: "/settings/security",
isCloud: true, feature: Feature.SECURITY_SETTINGS,
isEnterprise: true, role: "admin",
isAdmin: true,
}, },
{ label: "Groups", icon: IconUsersGroup, path: "/settings/groups" }, { label: "Groups", icon: IconUsersGroup, path: "/settings/groups" },
{ label: "Spaces", icon: IconSpaces, path: "/settings/spaces" }, { label: "Spaces", icon: IconSpaces, path: "/settings/spaces" },
{ label: "Public sharing", icon: IconWorld, path: "/settings/sharing" },
{
label: "Verified pages",
icon: IconShieldCheck,
path: "/settings/verifications",
feature: Feature.PAGE_VERIFICATION,
},
{
label: "API management",
icon: IconKey,
path: "/settings/api-keys",
feature: Feature.API_KEYS,
role: "admin",
},
{
label: "AI settings",
icon: IconSparkles,
path: "/settings/ai",
role: "admin",
},
{
label: "Audit log",
icon: IconHistory,
path: "/settings/audit",
feature: Feature.AUDIT_LOGS,
role: "owner",
env: "selfhosted",
},
], ],
}, },
{ {
@@ -100,39 +143,33 @@ export default function SettingsSidebar() {
const { t } = useTranslation(); const { t } = useTranslation();
const location = useLocation(); const location = useLocation();
const [active, setActive] = useState(location.pathname); const [active, setActive] = useState(location.pathname);
const navigate = useNavigate(); const { goBack } = useSettingsNavigation();
const { isAdmin } = useUserRole(); const { isAdmin, isOwner } = useUserRole();
const [workspace] = useAtom(workspaceAtom); const [entitlements] = useAtom(entitlementAtom);
const upgradeLabel = useUpgradeLabel();
const [mobileSidebarOpened] = useAtom(mobileSidebarAtom);
const toggleMobileSidebar = useToggleSidebar(mobileSidebarAtom);
useEffect(() => { useEffect(() => {
setActive(location.pathname); setActive(location.pathname);
}, [location.pathname]); }, [location.pathname]);
const hasFeature = (f: string) =>
entitlements?.features?.includes(f) ?? false;
const canShowItem = (item: DataItem) => { const canShowItem = (item: DataItem) => {
if (item.isCloud && item.isEnterprise) { if (item.env === "cloud" && !isCloud()) return false;
if (!(isCloud() || workspace?.hasLicenseKey)) return false; if (item.env === "selfhosted" && isCloud()) return false;
return item.isAdmin ? isAdmin : true; if (item.role === "admin" && !isAdmin) return false;
} if (item.role === "owner" && !isOwner) return false;
if (item.isCloud) {
return isCloud() ? (item.isAdmin ? isAdmin : true) : false;
}
if (item.isSelfhosted) {
return !isCloud() ? (item.isAdmin ? isAdmin : true) : false;
}
if (item.isEnterprise) {
return workspace?.hasLicenseKey ? (item.isAdmin ? isAdmin : true) : false;
}
if (item.isAdmin) {
return isAdmin;
}
return true; return true;
}; };
const isItemDisabled = (item: DataItem) => {
if (!item.feature) return false;
return !hasFeature(item.feature);
};
const menuItems = groupedData.map((group) => { const menuItems = groupedData.map((group) => {
if (group.heading === "System" && (!isAdmin || isCloud())) { if (group.heading === "System" && (!isAdmin || isCloud())) {
return null; return null;
@@ -163,17 +200,63 @@ export default function SettingsSidebar() {
prefetchHandler = prefetchBilling; prefetchHandler = prefetchBilling;
break; break;
case "License & Edition": case "License & Edition":
if (workspace?.hasLicenseKey) { if (entitlements?.tier !== "free") {
prefetchHandler = prefetchLicense; prefetchHandler = prefetchLicense;
} }
break; break;
case "Security & SSO": case "Security & SSO":
prefetchHandler = prefetchSsoProviders; prefetchHandler = () => {
prefetchSsoProviders();
prefetchScimTokens();
};
break;
case "Public sharing":
prefetchHandler = prefetchShares;
break;
case "API keys":
prefetchHandler = prefetchApiKeys;
break;
case "API management":
prefetchHandler = prefetchApiKeyManagement;
break;
case "Audit log":
prefetchHandler = prefetchAuditLogs;
break;
case "Verified pages":
prefetchHandler = prefetchVerifiedPages;
break; break;
default: default:
break; break;
} }
const isDisabled = isItemDisabled(item);
if (isDisabled) {
return (
<Tooltip
key={item.label}
label={upgradeLabel}
position="right"
withArrow
>
<span
className={classes.link}
data-disabled
role="link"
aria-disabled="true"
tabIndex={0}
style={{
opacity: 0.5,
cursor: "not-allowed",
}}
>
<item.icon className={classes.linkIcon} stroke={2} />
<span>{t(item.label)}</span>
</span>
</Tooltip>
);
}
return ( return (
<Link <Link
onMouseEnter={prefetchHandler} onMouseEnter={prefetchHandler}
@@ -181,6 +264,11 @@ export default function SettingsSidebar() {
data-active={active.startsWith(item.path) || undefined} data-active={active.startsWith(item.path) || undefined}
key={item.label} key={item.label}
to={item.path} to={item.path}
onClick={() => {
if (mobileSidebarOpened) {
toggleMobileSidebar();
}
}}
> >
<item.icon className={classes.linkIcon} stroke={2} /> <item.icon className={classes.linkIcon} stroke={2} />
<span>{t(item.label)}</span> <span>{t(item.label)}</span>
@@ -195,10 +283,15 @@ export default function SettingsSidebar() {
<div className={classes.navbar}> <div className={classes.navbar}>
<Group className={classes.title} justify="flex-start"> <Group className={classes.title} justify="flex-start">
<ActionIcon <ActionIcon
onClick={() => navigate(-1)} onClick={() => {
goBack();
if (mobileSidebarOpened) {
toggleMobileSidebar();
}
}}
variant="transparent" variant="transparent"
c="gray" c="gray"
aria-label="Back" aria-label={t("Back")}
> >
<IconArrowLeft stroke={2} /> <IconArrowLeft stroke={2} />
</ActionIcon> </ActionIcon>
@@ -4,7 +4,7 @@ import { Divider, Title } from '@mantine/core';
export default function SettingsTitle({ title }: { title: string }) { export default function SettingsTitle({ title }: { title: string }) {
return ( return (
<> <>
<Title order={3}> <Title order={1} size="h3">
{title} {title}
</Title> </Title>
<Divider my="md" /> <Divider my="md" />
@@ -0,0 +1,19 @@
.dark {
@mixin dark {
display: none;
}
@mixin light {
display: block;
}
}
.light {
@mixin light {
display: none;
}
@mixin dark {
display: block;
}
}
+21 -6
View File
@@ -1,13 +1,28 @@
import { Button, Group, useMantineColorScheme } from '@mantine/core'; import {
ActionIcon,
Tooltip,
useComputedColorScheme,
useMantineColorScheme,
} from "@mantine/core";
import { IconMoon, IconSun } from "@tabler/icons-react";
import classes from "./theme-toggle.module.css";
export function ThemeToggle() { export function ThemeToggle() {
const { setColorScheme } = useMantineColorScheme(); const { setColorScheme } = useMantineColorScheme();
const computedColorScheme = useComputedColorScheme();
return ( return (
<Group justify="center" mt="xl"> <Tooltip label="Toggle Color Scheme">
<Button onClick={() => setColorScheme('light')}>Light</Button> <ActionIcon
<Button onClick={() => setColorScheme('dark')}>Dark</Button> variant="default"
<Button onClick={() => setColorScheme('auto')}>Auto</Button> onClick={() => {
</Group> setColorScheme(computedColorScheme === "light" ? "dark" : "light");
}}
aria-label="Toggle color scheme"
>
<IconSun className={classes.light} size={18} stroke={1.5} />
<IconMoon className={classes.dark} size={18} stroke={1.5} />
</ActionIcon>
</Tooltip>
); );
} }
@@ -0,0 +1,50 @@
import { useRef, useState, ReactNode } from "react";
import { Text, TextProps, Tooltip } from "@mantine/core";
type AutoTooltipTextProps = TextProps & {
children: ReactNode;
tooltipLabel?: string;
tooltipProps?: Omit<
React.ComponentProps<typeof Tooltip>,
"children" | "label"
>;
};
export function AutoTooltipText({
children,
tooltipLabel,
tooltipProps,
...textProps
}: AutoTooltipTextProps) {
const textRef = useRef<HTMLParagraphElement>(null);
const [isTruncated, setIsTruncated] = useState(false);
const handleMouseEnter = () => {
const element = textRef.current;
if (element) {
setIsTruncated(element.scrollWidth > element.clientWidth);
}
};
const label = tooltipLabel ?? (typeof children === "string" ? children : "");
return (
<Tooltip
label={label}
disabled={!isTruncated || !label}
multiline
withArrow
withinPortal={false}
{...tooltipProps}
>
<Text
ref={textRef}
truncate
onMouseEnter={handleMouseEnter}
{...textProps}
>
{children}
</Text>
</Tooltip>
);
}
@@ -0,0 +1,68 @@
.root {
position: relative;
}
.track {
display: flex;
gap: var(--mantine-spacing-md);
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
scrollbar-width: none;
-ms-overflow-style: none;
padding: 2px;
margin: -2px;
}
.track::-webkit-scrollbar {
display: none;
}
.track > * {
scroll-snap-align: start;
flex: 0 0 auto;
}
.arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6));
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
cursor: pointer;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
opacity: 0;
pointer-events: none;
transition: opacity 120ms ease, background-color 120ms ease, transform 120ms ease;
z-index: 2;
}
.root:hover .arrow.visible,
.arrow.visible:focus-visible {
opacity: 1;
pointer-events: auto;
}
.arrow:hover {
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5));
}
.arrow:active {
transform: translateY(-50%) scale(0.95);
}
.arrowLeft {
left: -14px;
}
.arrowRight {
right: -14px;
}
@@ -0,0 +1,77 @@
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import classes from "./card-carousel.module.css";
type Props = {
children: ReactNode;
ariaLabel?: string;
};
export default function CardCarousel({ children, ariaLabel }: Props) {
const { t } = useTranslation();
const trackRef = useRef<HTMLDivElement>(null);
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(false);
const updateScrollState = useCallback(() => {
const el = trackRef.current;
if (!el) return;
const maxScroll = el.scrollWidth - el.clientWidth;
setCanScrollLeft(el.scrollLeft > 1);
setCanScrollRight(el.scrollLeft < maxScroll - 1);
}, []);
useEffect(() => {
updateScrollState();
const el = trackRef.current;
if (!el) return;
const observer = new ResizeObserver(updateScrollState);
observer.observe(el);
for (const child of Array.from(el.children)) {
observer.observe(child);
}
return () => observer.disconnect();
}, [updateScrollState, children]);
const scrollBy = (direction: 1 | -1) => {
const el = trackRef.current;
if (!el) return;
el.scrollBy({ left: direction * el.clientWidth * 0.85, behavior: "smooth" });
};
return (
<div className={classes.root}>
<div
ref={trackRef}
className={classes.track}
onScroll={updateScrollState}
{...(ariaLabel ? { role: "region", "aria-label": ariaLabel } : {})}
>
{children}
</div>
<button
type="button"
className={`${classes.arrow} ${classes.arrowLeft} ${canScrollLeft ? classes.visible : ""}`}
onClick={() => scrollBy(-1)}
aria-label={t("Scroll left")}
tabIndex={canScrollLeft ? 0 : -1}
>
<IconChevronLeft size={18} />
</button>
<button
type="button"
className={`${classes.arrow} ${classes.arrowRight} ${canScrollRight ? classes.visible : ""}`}
onClick={() => scrollBy(1)}
aria-label={t("Scroll right")}
tabIndex={canScrollRight ? 0 : -1}
>
<IconChevronRight size={18} />
</button>
</div>
);
}
@@ -0,0 +1,29 @@
/*
* Focus styling for list-style tables (recent changes, favorites, all
* spaces, groups, verified pages, shares).
*
* Per WAI-ARIA Authoring Practices and Adrian Roselli's guidance on table
* accessibility (https://adrianroselli.com/2020/02/block-links-cards-clickable-regions-etc.html),
* data tables should not be made fully clickable. Only the title cell is the
* link, and that link is what receives Tab focus.
*
* - `.row` adds a subtle background tint when the row contains the focused
* element, so keyboard users can see which row they're inspecting.
* - `.link` adds a visible :focus-visible outline on the title link itself.
*
* No stretched-link pseudo here on purpose: absolutely-positioned pseudos
* inside table cells cause column reflow on focus in Chromium.
*/
.row:focus-within {
background-color: light-dark(
var(--mantine-color-gray-1),
var(--mantine-color-dark-6)
);
}
.link:focus-visible {
outline: 2px solid var(--mantine-primary-color-filled);
outline-offset: 2px;
border-radius: var(--mantine-radius-sm);
}
@@ -1,9 +1,10 @@
import React from "react"; import React from "react";
import { Avatar } from "@mantine/core"; import { Avatar, MantineColor } from "@mantine/core";
import { getAvatarUrl } from "@/lib/config.ts"; import { getAvatarUrl } from "@/lib/config.ts";
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
interface CustomAvatarProps { interface CustomAvatarProps {
avatarUrl: string; avatarUrl?: string;
name: string; name: string;
color?: string; color?: string;
size?: string | number; size?: string | number;
@@ -11,21 +12,61 @@ interface CustomAvatarProps {
variant?: string; variant?: string;
style?: any; style?: any;
component?: any; component?: any;
type?: AvatarIconType;
mt?: string | number;
}
// `color.shade` pairs whose contrast meets WCAG AA (4.5:1) in BOTH variants:
// - filled: white text on the shade as bg
// - light: shade as text on the color's light-bg (10% color.6 over white)
// Avoids lime/yellow/green/orange — even their dark shades have weak
// contrast. grape and indigo were bumped from .7 to darker shades because
// the original picks failed: grape.7 was 4.02/3.61 (both fail) and
// indigo.7 was 4.98/4.39 (light fails by a hair).
const SAFE_INITIALS_COLORS: MantineColor[] = [
"blue.8",
"cyan.9",
"grape.9",
"indigo.8",
"pink.8",
"red.8",
"violet.7",
];
function hashName(input: string) {
let hash = 0;
for (let i = 0; i < input.length; i += 1) {
hash = (hash << 5) - hash + input.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash);
}
function pickInitialsColor(name: string) {
return SAFE_INITIALS_COLORS[hashName(name) % SAFE_INITIALS_COLORS.length];
}
function sanitizeInitialsSource(name: string) {
const sanitized = name.replace(/[^\p{L}\p{N}\s]/gu, " ").trim();
return sanitized || name;
} }
export const CustomAvatar = React.forwardRef< export const CustomAvatar = React.forwardRef<
HTMLInputElement, HTMLInputElement,
CustomAvatarProps CustomAvatarProps
>(({ avatarUrl, name, ...props }: CustomAvatarProps, ref) => { >(({ avatarUrl, name, type, color, ...props }: CustomAvatarProps, ref) => {
const avatarLink = getAvatarUrl(avatarUrl); const avatarLink = getAvatarUrl(avatarUrl, type);
const resolvedColor =
!color || color === "initials" ? pickInitialsColor(name ?? "") : color;
const initialsSource = sanitizeInitialsSource(name ?? "");
return ( return (
<Avatar <Avatar
ref={ref} ref={ref}
src={avatarLink} src={avatarLink}
name={name} name={initialsSource}
alt={name} alt={name}
color="initials" color={resolvedColor}
{...props} {...props}
/> />
); );
@@ -0,0 +1,71 @@
import { useState, useEffect } from "react";
import { Modal, Button, Group } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { DestinationPicker } from "./destination-picker";
import {
DestinationPickerModalProps,
DestinationSelection,
} from "./destination-picker.types";
export function DestinationPickerModal({
opened,
onClose,
title,
actionLabel,
onSelect,
loading,
excludePageId,
pageLimit,
initialSpaceId,
searchSpacesOnly,
}: DestinationPickerModalProps) {
const { t } = useTranslation();
const [selection, setSelection] = useState<DestinationSelection | null>(null);
useEffect(() => {
if (!opened) {
setSelection(null);
}
}, [opened]);
return (
<Modal.Root
opened={opened}
onClose={onClose}
size={550}
padding="lg"
yOffset="10vh"
onClick={(e) => e.stopPropagation()}
>
<Modal.Overlay />
<Modal.Content>
<Modal.Header py={0}>
<Modal.Title fw={500}>{title}</Modal.Title>
<Modal.CloseButton aria-label={t("Close")} />
</Modal.Header>
<Modal.Body>
<DestinationPicker
onSelectionChange={setSelection}
excludePageId={excludePageId}
pageLimit={pageLimit}
initialSpaceId={initialSpaceId}
searchSpacesOnly={searchSpacesOnly}
/>
<Group justify="flex-end" mt="md">
<Button variant="default" onClick={onClose}>
{t("Close")}
</Button>
<Button
onClick={() => selection && onSelect(selection)}
disabled={!selection}
loading={loading}
>
{actionLabel}
</Button>
</Group>
</Modal.Body>
</Modal.Content>
</Modal.Root>
);
}
@@ -0,0 +1,134 @@
.searchInput {
margin-bottom: var(--mantine-spacing-sm);
}
.scrollArea {
max-height: 50vh;
}
.row {
padding: 6px 12px;
border-radius: var(--mantine-radius-sm);
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
transition: background-color 150ms ease;
user-select: none;
@mixin hover {
background-color: light-dark(
var(--mantine-color-gray-0),
var(--mantine-color-dark-6)
);
}
&:focus-visible {
outline: 2px solid var(--mantine-primary-color-filled);
outline-offset: -2px;
}
}
.selected {
background-color: light-dark(
var(--mantine-color-blue-0),
var(--mantine-color-dark-5)
);
border-left: 2px solid var(--mantine-primary-color-filled);
}
.spaceRow {
composes: row;
font-weight: 500;
}
.pageRow {
composes: row;
font-weight: 400;
}
.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.chevron {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--mantine-radius-sm);
flex-shrink: 0;
transition: transform 150ms ease;
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
@mixin hover {
background-color: light-dark(
var(--mantine-color-gray-1),
var(--mantine-color-dark-5)
);
}
}
.chevronExpanded {
transform: rotate(90deg);
}
.loadMore {
text-align: center;
padding: 6px;
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
font-size: var(--mantine-font-size-sm);
cursor: pointer;
@mixin hover {
text-decoration: underline;
}
}
.selectedIndicator {
padding: 8px 12px;
font-size: var(--mantine-font-size-sm);
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
margin-top: var(--mantine-spacing-xs);
}
.emptyState {
padding: 12px;
text-align: center;
font-size: var(--mantine-font-size-sm);
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
}
.searchResult {
composes: row;
}
.pageTitle {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: var(--mantine-font-size-sm);
}
.spaceName {
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
font-size: var(--mantine-font-size-xs);
flex-shrink: 0;
}
.iconWrapper {
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: 16px;
line-height: 1;
}
@@ -0,0 +1,234 @@
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
import { ActionIcon, TextInput, ScrollArea, Loader } from "@mantine/core";
import { useDebouncedValue } from "@mantine/hooks";
import { IconSearch, IconFileDescription } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import { useGetSpacesQuery } from "@/features/space/queries/space-query";
import { useSearchSuggestionsQuery } from "@/features/search/queries/search-query";
import { ISpace } from "@/features/space/types/space.types";
import { IPage } from "@/features/page/types/page.types";
import { DestinationSelection } from "./destination-picker.types";
import { SpaceRow } from "./space-row";
import classes from "./destination-picker.module.css";
type DestinationPickerProps = {
onSelectionChange: (selection: DestinationSelection | null) => void;
excludePageId?: string;
pageLimit?: number;
initialSpaceId?: string;
searchSpacesOnly?: boolean;
};
export function DestinationPicker({
onSelectionChange,
excludePageId,
pageLimit = 15,
initialSpaceId,
searchSpacesOnly,
}: DestinationPickerProps) {
const { t } = useTranslation();
const [searchQuery, setSearchQuery] = useState("");
const [selection, setSelection] = useState<DestinationSelection | null>(null);
const [debouncedQuery] = useDebouncedValue(searchQuery, 300);
const viewportRef = useRef<HTMLDivElement>(null);
const { data: spacesData, isLoading: spacesLoading } = useGetSpacesQuery({
limit: 100,
});
const searchEnabled =
!searchSpacesOnly && debouncedQuery && debouncedQuery.length >= 2;
const { data: searchData, isLoading: searchLoading } =
useSearchSuggestionsQuery({
query: searchEnabled ? debouncedQuery : "",
includePages: true,
limit: 20,
});
const isSearching = !!searchEnabled;
const filteredSpaces = useMemo(() => {
const items = spacesData?.items ?? [];
if (!searchSpacesOnly || !debouncedQuery) return items;
const fold = (s: string) =>
s
.normalize("NFD")
.replace(/[̀-ͯ]/g, "")
.toLocaleLowerCase();
const term = fold(debouncedQuery);
return items.filter((s) => fold(s.name).includes(term));
}, [spacesData, searchSpacesOnly, debouncedQuery]);
const selectedId =
selection?.type === "space" ? selection.spaceId : selection?.pageId ?? null;
const updateSelection = useCallback(
(next: DestinationSelection | null) => {
setSelection(next);
onSelectionChange(next);
},
[onSelectionChange],
);
const handleSearchResultClick = (page: Partial<IPage>) => {
if (!page.space || !page.id) return;
updateSelection({
type: "page",
spaceId: page.space.id,
pageId: page.id,
page,
space: page.space,
});
setSearchQuery("");
};
const handleSelectSpace = useCallback(
(space: ISpace) => {
updateSelection({ type: "space", spaceId: space.id, space });
},
[updateSelection],
);
const handleSelectPage = useCallback(
(page: Partial<IPage>, space: ISpace) => {
if (!page.id) return;
updateSelection({
type: "page",
spaceId: page.spaceId ?? space.id,
pageId: page.id,
page,
space,
});
},
[updateSelection],
);
// Pre-select space when initialSpaceId is set and spaces have loaded.
// Only runs once: skip if user has already made a selection.
useEffect(() => {
if (!initialSpaceId || selection) return;
const match = spacesData?.items?.find((s) => s.id === initialSpaceId);
if (match) {
updateSelection({ type: "space", spaceId: match.id, space: match });
requestAnimationFrame(() => {
const el = viewportRef.current?.querySelector<HTMLElement>(
`[data-space-id="${match.id}"]`,
);
el?.scrollIntoView({ block: "nearest" });
});
}
}, [initialSpaceId, selection, spacesData, updateSelection]);
return (
<>
<TextInput
leftSection={<IconSearch size={16} />}
placeholder={
searchSpacesOnly
? t("Search spaces...")
: t("Search pages and spaces...")
}
aria-label={
searchSpacesOnly
? t("Search spaces...")
: t("Search pages and spaces...")
}
variant="filled"
value={searchQuery}
onChange={(e) => setSearchQuery(e.currentTarget.value)}
className={classes.searchInput}
/>
<ScrollArea
h="50vh"
offsetScrollbars
className={classes.scrollArea}
viewportRef={viewportRef}
>
{isSearching ? (
searchLoading ? (
<div className={classes.emptyState}>
<Loader size="xs" />
</div>
) : searchData?.pages && searchData.pages.length > 0 ? (
searchData.pages.map(
(page) =>
page && (
<div
key={page.id}
className={classes.searchResult}
role="button"
tabIndex={0}
onClick={() => handleSearchResultClick(page)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleSearchResultClick(page);
}
}}
>
<div className={classes.iconWrapper}>
{page.icon ? (
page.icon
) : (
<ActionIcon
component="div"
variant="transparent"
c="gray"
size={22}
>
<IconFileDescription size={18} />
</ActionIcon>
)}
</div>
<div className={classes.pageTitle}>
{page.title || t("Untitled")}
</div>
{page.space && (
<div className={classes.spaceName}>
{page.space.name}
</div>
)}
</div>
),
)
) : (
<div className={classes.emptyState}>{t("No results found")}</div>
)
) : spacesLoading ? (
<div className={classes.emptyState}>
<Loader size="xs" />
</div>
) : filteredSpaces.length === 0 ? (
<div className={classes.emptyState}>
{searchSpacesOnly && debouncedQuery
? t("No spaces found")
: t("No results found")}
</div>
) : (
filteredSpaces.map((space) => (
<SpaceRow
key={space.id}
space={space}
limit={pageLimit}
selectedId={selectedId}
excludePageId={excludePageId}
onSelectSpace={handleSelectSpace}
onSelectPage={handleSelectPage}
/>
))
)}
</ScrollArea>
{selection && (
<div className={classes.selectedIndicator}>
{selection.type === "space"
? selection.space.name
: `${selection.space.name} / ${selection.page.title || t("Untitled")}`}
</div>
)}
</>
);
}
@@ -0,0 +1,25 @@
import { ISpace } from "@/features/space/types/space.types";
import { IPage } from "@/features/page/types/page.types";
export type DestinationSelection =
| { type: "space"; spaceId: string; space: ISpace }
| {
type: "page";
spaceId: string;
pageId: string;
page: Partial<IPage>;
space: Partial<ISpace>;
};
export type DestinationPickerModalProps = {
opened: boolean;
onClose: () => void;
title: string;
actionLabel: string;
onSelect: (selection: DestinationSelection) => void | Promise<void>;
loading?: boolean;
excludePageId?: string;
pageLimit?: number;
initialSpaceId?: string;
searchSpacesOnly?: boolean;
};
@@ -0,0 +1,94 @@
import { useInfiniteQuery } from "@tanstack/react-query";
import { Loader } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { getSidebarPages } from "@/features/page/services/page-service";
import { IPage } from "@/features/page/types/page.types";
import { IPagination } from "@/lib/types";
import { PageRow } from "./page-row";
import classes from "./destination-picker.module.css";
type PageChildrenProps = {
spaceId: string;
pageId?: string;
depth: number;
limit: number;
selectedId: string | null;
excludePageId?: string;
onSelectPage: (page: Partial<IPage>) => void;
};
export function PageChildren({
spaceId,
pageId,
depth,
limit,
selectedId,
excludePageId,
onSelectPage,
}: PageChildrenProps) {
const { t } = useTranslation();
const { data, isLoading, hasNextPage, fetchNextPage } = useInfiniteQuery({
queryKey: ["destination-pages", spaceId, pageId ?? "root"],
queryFn: ({ pageParam }) =>
getSidebarPages({
spaceId,
pageId,
limit,
cursor: pageParam,
}),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage: IPagination<IPage>) =>
lastPage.meta?.nextCursor ?? undefined,
});
const pages = data?.pages.flatMap((page) => page.items) ?? [];
if (isLoading) {
return (
<div className={classes.emptyState}>
<Loader size="xs" />
</div>
);
}
if (pages.length === 0) {
return (
<div className={classes.emptyState}>
{pageId ? t("No pages inside") : t("No pages in this space")}
</div>
);
}
return (
<>
{pages.map((page) => (
<PageRow
key={page.id}
page={page}
depth={depth}
limit={limit}
selectedId={selectedId}
excludePageId={excludePageId}
onSelect={onSelectPage}
/>
))}
{hasNextPage && (
<div
className={classes.loadMore}
onClick={() => fetchNextPage()}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
fetchNextPage();
}
}}
role="button"
tabIndex={0}
>
{t("Load more")}
</div>
)}
</>
);
}
@@ -0,0 +1,115 @@
import { KeyboardEvent, useState } from "react";
import { ActionIcon } from "@mantine/core";
import { IconChevronRight, IconFileDescription } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import { IPage } from "@/features/page/types/page.types";
import { PageChildren } from "./page-children";
import classes from "./destination-picker.module.css";
type PageRowProps = {
page: Partial<IPage>;
depth: number;
limit: number;
selectedId: string | null;
excludePageId?: string;
onSelect: (page: Partial<IPage>) => void;
};
export function PageRow({
page,
depth,
limit,
selectedId,
excludePageId,
onSelect,
}: PageRowProps) {
const { t } = useTranslation();
const [expanded, setExpanded] = useState(false);
const isExcluded = page.id === excludePageId;
const isSelected = page.id === selectedId;
const rowClasses = [
classes.pageRow,
isSelected && classes.selected,
isExcluded && classes.disabled,
]
.filter(Boolean)
.join(" ");
const handleSelect = () => {
if (!isExcluded) onSelect(page);
};
const handleRowKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
if (e.target !== e.currentTarget) return;
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleSelect();
}
};
return (
<>
<div
className={rowClasses}
style={{ paddingLeft: depth * 20 + 12 }}
role="button"
tabIndex={isExcluded ? -1 : 0}
aria-disabled={isExcluded || undefined}
onClick={handleSelect}
onKeyDown={handleRowKeyDown}
>
{page.hasChildren ? (
<ActionIcon
className={`${classes.chevron} ${expanded ? classes.chevronExpanded : ""}`}
variant="subtle"
color="gray"
size="sm"
aria-label={expanded ? t("Collapse") : t("Expand")}
aria-expanded={expanded}
onClick={(e) => {
e.stopPropagation();
setExpanded(!expanded);
}}
>
<IconChevronRight size={14} />
</ActionIcon>
) : (
<div style={{ width: 20, flexShrink: 0 }} />
)}
<div className={classes.iconWrapper}>
{page.icon ? (
page.icon
) : (
<ActionIcon
component="div"
variant="transparent"
c="gray"
size={22}
>
<IconFileDescription size={18} />
</ActionIcon>
)}
</div>
<div className={classes.pageTitle}>
{page.title || t("Untitled")}
</div>
</div>
{expanded && page.hasChildren && (
<PageChildren
spaceId={page.spaceId}
pageId={page.id}
depth={depth + 1}
limit={limit}
selectedId={selectedId}
excludePageId={excludePageId}
onSelectPage={onSelect}
/>
)}
</>
);
}
@@ -0,0 +1,130 @@
import { KeyboardEvent, useState } from "react";
import { ActionIcon, Tooltip } from "@mantine/core";
import { IconChevronRight, IconLock } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import { ISpace } from "@/features/space/types/space.types";
import { IPage } from "@/features/page/types/page.types";
import { SpaceRole } from "@/lib/types";
import { CustomAvatar } from "@/components/ui/custom-avatar";
import { AvatarIconType } from "@/features/attachments/types/attachment.types";
import { PageChildren } from "./page-children";
import classes from "./destination-picker.module.css";
type SpaceRowProps = {
space: ISpace;
limit: number;
selectedId: string | null;
excludePageId?: string;
onSelectSpace: (space: ISpace) => void;
onSelectPage: (page: Partial<IPage>, space: ISpace) => void;
};
export function SpaceRow({
space,
limit,
selectedId,
excludePageId,
onSelectSpace,
onSelectPage,
}: SpaceRowProps) {
const { t } = useTranslation();
const [expanded, setExpanded] = useState(false);
const writable =
!!space.membership?.role && space.membership.role !== SpaceRole.READER;
const isSelected = space.id === selectedId;
const rowClasses = [
classes.spaceRow,
isSelected && classes.selected,
!writable && classes.disabled,
]
.filter(Boolean)
.join(" ");
const handleSelect = () => {
if (writable) onSelectSpace(space);
};
const handleRowKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
if (e.target !== e.currentTarget) return;
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleSelect();
}
};
const rowContent = (
<div
className={rowClasses}
data-space-id={space.id}
role="button"
tabIndex={writable ? 0 : -1}
aria-disabled={!writable || undefined}
onClick={handleSelect}
onKeyDown={handleRowKeyDown}
>
{writable ? (
<ActionIcon
className={`${classes.chevron} ${expanded ? classes.chevronExpanded : ""}`}
variant="subtle"
color="gray"
size="sm"
aria-label={expanded ? t("Collapse") : t("Expand")}
aria-expanded={expanded}
onClick={(e) => {
e.stopPropagation();
setExpanded(!expanded);
}}
>
<IconChevronRight size={14} />
</ActionIcon>
) : (
<div style={{ width: 20, flexShrink: 0 }} />
)}
<CustomAvatar
name={space.name}
avatarUrl={space.logo}
type={AvatarIconType.SPACE_ICON}
size={22}
/>
<div className={classes.pageTitle}>{space.name}</div>
{!writable && (
<IconLock
size={14}
color="var(--mantine-color-gray-5)"
/>
)}
</div>
);
return (
<>
{writable ? (
rowContent
) : (
<Tooltip
label={t("You don't have permission to create pages here")}
position="right"
withArrow
>
<div>{rowContent}</div>
</Tooltip>
)}
{expanded && writable && (
<PageChildren
spaceId={space.id}
depth={1}
limit={limit}
selectedId={selectedId}
excludePageId={excludePageId}
onSelectPage={(page) => onSelectPage(page, space)}
/>
)}
</>
);
}
+66 -4
View File
@@ -1,4 +1,4 @@
import React, { ReactNode, useState } from "react"; import React, { ReactNode, useEffect, useState } from "react";
import { import {
ActionIcon, ActionIcon,
Popover, Popover,
@@ -7,14 +7,35 @@ import {
} from "@mantine/core"; } from "@mantine/core";
import { useClickOutside, useDisclosure, useWindowEvent } from "@mantine/hooks"; import { useClickOutside, useDisclosure, useWindowEvent } from "@mantine/hooks";
import { Suspense } from "react"; import { Suspense } from "react";
const Picker = React.lazy(() => import("@emoji-mart/react"));
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
// Load the picker module AND the emoji data in parallel inside the lazy
// resolution, then bind the data into the component. React.lazy only finishes
// suspending once both are in memory, so the Suspense boundary hides the
// Remove button until the Picker can render with real content.
const Picker = React.lazy(async () => {
const [pickerModule, dataModule] = await Promise.all([
import("@slidoapp/emoji-mart-react"),
import("@slidoapp/emoji-mart-data"),
]);
const PickerComp = pickerModule.default;
const data = dataModule.default;
return {
default: (props: any) => <PickerComp {...props} data={data} />,
};
});
export interface EmojiPickerInterface { export interface EmojiPickerInterface {
onEmojiSelect: (emoji: any) => void; onEmojiSelect: (emoji: any) => void;
icon: ReactNode; icon: ReactNode;
removeEmojiAction: () => void; removeEmojiAction: () => void;
readOnly: boolean; readOnly: boolean;
actionIconProps?: {
size?: string;
variant?: string;
c?: string;
tabIndex?: number;
};
} }
function EmojiPicker({ function EmojiPicker({
@@ -22,6 +43,7 @@ function EmojiPicker({
icon, icon,
removeEmojiAction, removeEmojiAction,
readOnly, readOnly,
actionIconProps,
}: EmojiPickerInterface) { }: EmojiPickerInterface) {
const { t } = useTranslation(); const { t } = useTranslation();
const [opened, handlers] = useDisclosure(false); const [opened, handlers] = useDisclosure(false);
@@ -44,6 +66,38 @@ function EmojiPicker({
} }
}); });
// emoji-mart's built-in autoFocus calls .focus() without preventScroll, which
// makes the browser scroll every scrollable ancestor of the search input to
// bring it on screen — including the page editor's scroll container, so the
// page jumps to the top whenever the picker is opened from a scrolled-down
// position. The search input lives inside the <em-emoji-picker> custom
// element's shadow root, so we poll for it after the dropdown mounts and
// focus it ourselves with preventScroll.
useEffect(() => {
if (!opened || !dropdown) return;
let cancelled = false;
let rafId = 0;
const tryFocus = (attempts: number) => {
if (cancelled) return;
const pickerEl = dropdown.querySelector("em-emoji-picker");
const input = pickerEl?.shadowRoot?.querySelector<HTMLInputElement>(
'input[type="search"]',
);
if (input) {
input.focus({ preventScroll: true });
return;
}
if (attempts < 60) {
rafId = requestAnimationFrame(() => tryFocus(attempts + 1));
}
};
rafId = requestAnimationFrame(() => tryFocus(0));
return () => {
cancelled = true;
cancelAnimationFrame(rafId);
};
}, [opened, dropdown]);
const handleEmojiSelect = (emoji) => { const handleEmojiSelect = (emoji) => {
onEmojiSelect(emoji); onEmojiSelect(emoji);
handlers.close(); handlers.close();
@@ -64,14 +118,22 @@ function EmojiPicker({
closeOnEscape={true} closeOnEscape={true}
> >
<Popover.Target ref={setTarget}> <Popover.Target ref={setTarget}>
<ActionIcon c="gray" variant="transparent" onClick={handlers.toggle}> <ActionIcon
c={actionIconProps?.c || "gray"}
variant={actionIconProps?.variant || "transparent"}
size={actionIconProps?.size}
tabIndex={actionIconProps?.tabIndex}
onClick={handlers.toggle}
aria-label={t("Pick emoji")}
aria-haspopup="dialog"
aria-expanded={opened}
>
{icon} {icon}
</ActionIcon> </ActionIcon>
</Popover.Target> </Popover.Target>
<Suspense fallback={null}> <Suspense fallback={null}>
<Popover.Dropdown bg="000" style={{ border: "none" }} ref={setDropdown}> <Popover.Dropdown bg="000" style={{ border: "none" }} ref={setDropdown}>
<Picker <Picker
data={async () => (await import("@emoji-mart/data")).default}
onEmojiSelect={handleEmojiSelect} onEmojiSelect={handleEmojiSelect}
perLine={8} perLine={8}
skinTonePosition="search" skinTonePosition="search"
@@ -0,0 +1,8 @@
.root {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
}
@@ -0,0 +1,30 @@
import { Stack, Text } from "@mantine/core";
import { type TablerIcon } from "@tabler/icons-react";
import { ReactNode } from "react";
import classes from "./empty-state.module.css";
type EmptyStateProps = {
icon: TablerIcon;
title: string;
description?: string;
action?: ReactNode;
};
export function EmptyState({ icon: Icon, title, description, action }: EmptyStateProps) {
return (
<div className={classes.root}>
<Stack align="center" gap="xs">
<Icon size={40} stroke={1.5} color="var(--mantine-color-dimmed)" />
<Text size="lg" fw={500}>
{title}
</Text>
{description && (
<Text size="sm" c="dimmed" maw={350}>
{description}
</Text>
)}
{action}
</Stack>
</div>
);
}
@@ -0,0 +1,47 @@
import { Box } from "@mantine/core";
import React from "react";
interface ResponsiveSettingsRowProps {
children: React.ReactNode;
}
export function ResponsiveSettingsRow({ children }: ResponsiveSettingsRowProps) {
return (
<Box
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
gap: "1rem",
flexWrap: "wrap",
}}
>
{children}
</Box>
);
}
interface ResponsiveSettingsContentProps {
children: React.ReactNode;
}
export function ResponsiveSettingsContent({ children }: ResponsiveSettingsContentProps) {
return (
<Box style={{ flex: "1 1 300px", minWidth: 0 }}>
{children}
</Box>
);
}
interface ResponsiveSettingsControlProps {
children: React.ReactNode;
}
export function ResponsiveSettingsControl({ children }: ResponsiveSettingsControlProps) {
return (
<Box style={{ flex: "0 0 auto" }}>
{children}
</Box>
);
}
@@ -14,7 +14,14 @@ export interface SidebarToggleProps extends BoxProps, ElementProps<"button"> {
const SidebarToggle = React.forwardRef<HTMLButtonElement, SidebarToggleProps>( const SidebarToggle = React.forwardRef<HTMLButtonElement, SidebarToggleProps>(
({ opened, size = "sm", ...others }, ref) => { ({ opened, size = "sm", ...others }, ref) => {
return ( return (
<ActionIcon size={size} {...others} variant="subtle" color="gray" ref={ref}> <ActionIcon
size={size}
aria-expanded={opened}
{...others}
variant="subtle"
color="gray"
ref={ref}
>
{opened ? ( {opened ? (
<IconLayoutSidebarRightExpand /> <IconLayoutSidebarRightExpand />
) : ( ) : (
@@ -0,0 +1,27 @@
.skipLink {
position: absolute;
top: 8px;
left: 8px;
z-index: 9999;
padding: 8px 16px;
background: var(--mantine-color-body);
color: var(--mantine-color-text);
border: 2px solid var(--mantine-color-blue-6);
border-radius: 4px;
text-decoration: none;
font-weight: 500;
font-size: var(--mantine-font-size-sm);
transform: translateY(-200%);
transition: transform 0.15s ease-out;
}
.skipLink:focus {
transform: translateY(0);
outline: none;
}
@media print {
.skipLink {
display: none !important;
}
}
@@ -0,0 +1,13 @@
import { useTranslation } from "react-i18next";
import classes from "./skip-to-main.module.css";
export const MAIN_CONTENT_ID = "main-content";
export function SkipToMain() {
const { t } = useTranslation();
return (
<a href={`#${MAIN_CONTENT_ID}`} className={classes.skipLink}>
{t("Skip to main content")}
</a>
);
}
@@ -0,0 +1,105 @@
import { useEffect, useRef } from "react";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { useChatInfoQuery } from "../queries/ai-chat-query";
import { useChatStream } from "../hooks/use-chat-stream";
import ChatMessageList from "./chat-message-list";
import ChatEmptyState from "./chat-empty-state";
import ChatInput from "./chat-input";
import type { HomeAiPromptInitialState } from "@/features/home/components/home-ai-prompt";
import classes from "../styles/ai-chat.module.css";
export default function AiChatLayout() {
const { chatId } = useParams<{ chatId: string }>();
const location = useLocation();
const navigate = useNavigate();
const chatInfoQuery = useChatInfoQuery(chatId);
// If the URL points at a chat the user does not own, the info fetch 404s.
// Bounce them back to /ai so they cannot interact with any chat UI (including
// kicking off orphan uploads) tied to a chat they have no access to.
useEffect(() => {
if (chatId && chatInfoQuery.isError) {
navigate("/ai", { replace: true });
}
}, [chatId, chatInfoQuery.isError, navigate]);
const {
messages,
streamingContent,
streamingToolCalls,
isStreaming,
error,
sendMessage,
stopGeneration,
hydrateFromServer,
} = useChatStream(chatId);
const autoSentRef = useRef(false);
useEffect(() => {
if (chatInfoQuery.data?.messages) {
hydrateFromServer(chatInfoQuery.data.messages);
}
}, [chatInfoQuery.data, hydrateFromServer]);
useEffect(() => {
if (autoSentRef.current || chatId) return;
const state = location.state as HomeAiPromptInitialState | null;
if (!state?.initialContent && !state?.initialAttachments?.length) return;
autoSentRef.current = true;
sendMessage(
state.initialContent ?? "",
state.initialMentions ?? [],
state.initialAttachments ?? [],
);
navigate(location.pathname, { replace: true, state: null });
}, [chatId, location, navigate, sendMessage]);
const hasMessages = messages.length > 0 || isStreaming || !!chatId;
// While the redirect effect is running (or if the user is still on this
// component for any reason) never render the chat UI for a forbidden chat.
if (chatId && chatInfoQuery.isError) {
return null;
}
return (
<div className={classes.main}>
{hasMessages ? (
<>
<ChatMessageList
messages={messages}
isStreaming={isStreaming}
streamingContent={streamingContent}
streamingToolCalls={streamingToolCalls}
/>
{error && (
<div
style={{
padding: "var(--mantine-spacing-sm) var(--mantine-spacing-lg)",
color: "var(--mantine-color-red-6)",
fontSize: "var(--mantine-font-size-sm)",
}}
>
{error}
</div>
)}
<div className={classes.inputArea}>
<ChatInput
isStreaming={isStreaming}
onSend={sendMessage}
onStop={stopGeneration}
chatId={chatId}
/>
</div>
</>
) : (
<ChatEmptyState
isStreaming={isStreaming}
onSend={sendMessage}
onStop={stopGeneration}
/>
)}
</div>
);
}
@@ -0,0 +1,167 @@
import { useState, useRef, useEffect, useMemo, useCallback } from "react";
import { ActionIcon, Menu, TextInput } from "@mantine/core";
import { IconDots, IconTrash, IconEdit } from "@tabler/icons-react";
import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
import type { AiChat } from "../types/ai-chat.types";
import classes from "../styles/chat-sidebar.module.css";
type Props = {
chat: AiChat;
isActive: boolean;
onDelete: (chatId: string, title: string | null) => void;
onRename: (chatId: string, title: string) => void;
};
function formatChatDate(
isoString: string | Date,
locale: string | undefined,
): string {
const date = new Date(isoString);
if (Number.isNaN(date.getTime())) return "";
const now = new Date();
const startOfToday = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
).getTime();
const ts = date.getTime();
const sameYear = date.getFullYear() === now.getFullYear();
if (ts >= startOfToday) {
return date.toLocaleTimeString(locale, {
hour: "numeric",
minute: "2-digit",
});
}
if (sameYear) {
return date.toLocaleDateString(locale, {
month: "short",
day: "numeric",
});
}
return date.toLocaleDateString(locale, {
month: "short",
day: "numeric",
year: "numeric",
});
}
export default function AiChatSidebarItem({
chat,
isActive,
onDelete,
onRename,
}: Props) {
const { t, i18n } = useTranslation();
const [renaming, setRenaming] = useState(false);
const [renameValue, setRenameValue] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
const formattedDate = useMemo(
() => formatChatDate(chat.updatedAt, i18n.language),
[chat.updatedAt, i18n.language],
);
useEffect(() => {
if (renaming) {
// Wait for the input to be mounted before selecting.
const id = window.setTimeout(() => inputRef.current?.select(), 0);
return () => window.clearTimeout(id);
}
}, [renaming]);
const startRename = useCallback(() => {
setRenameValue(chat.title || "");
setRenaming(true);
}, [chat.title]);
const submitRename = useCallback(() => {
const trimmed = renameValue.trim();
if (trimmed && trimmed !== chat.title) {
onRename(chat.id, trimmed);
}
setRenaming(false);
}, [renameValue, chat.id, chat.title, onRename]);
if (renaming) {
return (
<div className={classes.chatItem} data-active={isActive || undefined}>
<TextInput
ref={inputRef}
size="xs"
variant="unstyled"
placeholder={t("Chat name")}
value={renameValue}
onChange={(e) => setRenameValue(e.currentTarget.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
submitRename();
} else if (e.key === "Escape") {
e.preventDefault();
setRenaming(false);
}
}}
onBlur={submitRename}
classNames={{ input: classes.chatItemRenameInput }}
style={{ flex: 1 }}
/>
</div>
);
}
return (
<Link
to={`/ai/chat/${chat.id}`}
className={classes.chatItem}
data-active={isActive || undefined}
>
<span className={classes.chatItemTitle}>
{chat.title || t("Untitled chat")}
</span>
<span className={classes.chatItemDate}>{formattedDate}</span>
<div className={classes.chatItemActions}>
<Menu position="bottom-end" withinPortal>
<Menu.Target>
<ActionIcon
variant="subtle"
size="xs"
color="gray"
onClick={(e) => e.preventDefault()}
aria-label={t("Chat menu")}
>
<IconDots size={14} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
leftSection={<IconEdit size={14} />}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
startRename();
}}
>
{t("Rename")}
</Menu.Item>
<Menu.Item
leftSection={<IconTrash size={14} />}
color="red"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onDelete(chat.id, chat.title);
}}
>
{t("Delete")}
</Menu.Item>
</Menu.Dropdown>
</Menu>
</div>
</Link>
);
}
@@ -0,0 +1,204 @@
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
import {
ActionIcon,
Center,
Text,
TextInput,
Loader,
Tooltip,
} from "@mantine/core";
import { modals } from "@mantine/modals";
import { useDebouncedValue } from "@mantine/hooks";
import { IconPlus, IconSearch, IconMessageCircle2 } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import {
useChatsQuery,
useDeleteChatMutation,
useUpdateChatTitleMutation,
useSearchChatsQuery,
} from "../queries/ai-chat-query";
import AiChatSidebarItem from "./ai-chat-sidebar-item";
import { groupChatsByAge } from "../utils/group-chats-by-age";
import classes from "../styles/chat-sidebar.module.css";
export default function AiChatSidebar() {
const { t } = useTranslation();
const navigate = useNavigate();
const { chatId } = useParams<{ chatId: string }>();
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 300);
const chatsQuery = useChatsQuery();
const searchQuery = useSearchChatsQuery(debouncedSearch);
const deleteMutation = useDeleteChatMutation();
const renameMutation = useUpdateChatTitleMutation();
const chats = useMemo(() => {
if (debouncedSearch) {
return searchQuery.data || [];
}
return chatsQuery.data?.pages.flatMap((p) => p.items) || [];
}, [debouncedSearch, searchQuery.data, chatsQuery.data]);
const groupedChats = useMemo(() => groupChatsByAge(chats, t), [chats, t]);
const sentinelRef = useRef<HTMLDivElement>(null);
const { hasNextPage, fetchNextPage, isFetchingNextPage } = chatsQuery;
const isSearching = Boolean(debouncedSearch);
useEffect(() => {
if (isSearching) return;
const sentinel = sentinelRef.current;
if (!sentinel) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
},
{ threshold: 0.1 },
);
observer.observe(sentinel);
return () => observer.disconnect();
}, [isSearching, hasNextPage, isFetchingNextPage, fetchNextPage]);
const handleNewChat = useCallback(
(event: React.MouseEvent<HTMLAnchorElement>) => {
if (
event.button !== 0 ||
event.ctrlKey ||
event.metaKey ||
event.shiftKey
) {
return;
}
event.preventDefault();
navigate("/ai");
},
[navigate],
);
const handleDelete = useCallback(
(id: string, title: string | null) => {
modals.openConfirmModal({
title: t("Delete chat"),
centered: true,
children: (
<Text size="sm">
{t("Are you sure you want to delete '{{title}}'? This action cannot be undone.", {
title: title || t("Untitled"),
})}
</Text>
),
labels: { confirm: t("Delete"), cancel: t("Cancel") },
confirmProps: { color: "red" },
onConfirm: () => {
deleteMutation.mutate(id, {
onSuccess: () => {
if (chatId === id) {
navigate("/ai");
}
},
});
},
});
},
[deleteMutation, chatId, navigate, t],
);
const handleRename = useCallback(
(chatId: string, title: string) => {
renameMutation.mutate({ chatId, title });
},
[renameMutation],
);
const isLoading = chatsQuery.isLoading || searchQuery.isLoading;
return (
<div className={classes.sidebar}>
<div className={classes.header}>
<h2 className={classes.title}>{t("AI Chat")}</h2>
<Tooltip label={t("New chat")} openDelay={250} withArrow>
<ActionIcon
component={Link}
to="/ai"
variant="subtle"
color="gray"
onClick={handleNewChat}
aria-label={t("New chat")}
>
<IconPlus size={18} />
</ActionIcon>
</Tooltip>
</div>
<TextInput
className={classes.searchInput}
placeholder={t("Search chats...")}
aria-label={t("Search chats")}
leftSection={<IconSearch size={14} />}
size="xs"
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<div className={classes.chatList}>
{isLoading && <Loader size="xs" mx="auto" mt="md" />}
{!isLoading && chats.length === 0 && (
<div className={classes.chatListEmpty}>
<IconMessageCircle2
size={28}
stroke={1.5}
className={classes.chatListEmptyIcon}
/>
<div className={classes.chatListEmptyTitle}>
{isSearching ? t("No chats found") : t("No conversations yet")}
</div>
<div className={classes.chatListEmptyHint}>
{isSearching
? t("Try a different search term.")
: t("Start a new chat to see it here.")}
</div>
</div>
)}
{isSearching
? chats.map((chat) => (
<AiChatSidebarItem
key={chat.id}
chat={chat}
isActive={chat.id === chatId}
onDelete={handleDelete}
onRename={handleRename}
/>
))
: groupedChats.map((group) => (
<div key={group.key} className={classes.chatGroup}>
<h3 className={classes.chatGroupLabel}>{group.label}</h3>
{group.chats.map((chat) => (
<AiChatSidebarItem
key={chat.id}
chat={chat}
isActive={chat.id === chatId}
onDelete={handleDelete}
onRename={handleRename}
/>
))}
</div>
))}
{!isSearching && (
<>
<div ref={sentinelRef} style={{ height: 1 }} />
{isFetchingNextPage && (
<Center py="xs">
<Loader size="xs" />
</Center>
)}
</>
)}
</div>
</div>
);
}
@@ -0,0 +1,67 @@
import { useState } from "react";
import { TextInput, Loader, Text, ScrollArea } from "@mantine/core";
import { IconSearch } from "@tabler/icons-react";
import { useChatsQuery, useSearchChatsQuery } from "../queries/ai-chat-query";
import { useDebouncedValue } from "@mantine/hooks";
import { useTranslation } from "react-i18next";
import classes from "../styles/aside-chat-panel.module.css";
type Props = {
activeChatId: string | undefined;
onSelect: (chatId: string) => void;
};
export default function AsideChatHistory({ activeChatId, onSelect }: Props) {
const { t } = useTranslation();
const [searchValue, setSearchValue] = useState("");
const [debouncedSearch] = useDebouncedValue(searchValue, 300);
const chatsQuery = useChatsQuery();
const searchQuery = useSearchChatsQuery(debouncedSearch);
const isSearching = debouncedSearch.length > 0;
const chats = isSearching
? (searchQuery.data ?? [])
: (chatsQuery.data?.pages.flatMap((p) => p.items) ?? []);
const isLoading = isSearching ? searchQuery.isLoading : chatsQuery.isLoading;
return (
<div>
<TextInput
placeholder={t("Search chats...")}
leftSection={<IconSearch size={14} />}
size="xs"
mb="xs"
value={searchValue}
onChange={(e) => setSearchValue(e.currentTarget.value)}
/>
{isLoading ? (
<div style={{ display: "flex", justifyContent: "center", padding: 16 }}>
<Loader size="sm" />
</div>
) : chats.length === 0 ? (
<Text size="sm" c="dimmed" ta="center" py="md">
{isSearching ? t("No chats found") : t("No chat history")}
</Text>
) : (
<ScrollArea.Autosize mah={300} scrollbars="y">
<div className={classes.historyList}>
{chats.map((chat) => (
<div
key={chat.id}
className={classes.historyItem}
data-active={chat.id === activeChatId || undefined}
onClick={() => onSelect(chat.id)}
>
<span className={classes.historyItemTitle}>
{chat.title || t("Untitled chat")}
</span>
</div>
))}
</div>
</ScrollArea.Autosize>
)}
</div>
);
}
@@ -0,0 +1,269 @@
import { useState, useEffect, useCallback } from "react";
import { ActionIcon, Popover, Tooltip, UnstyledButton } from "@mantine/core";
import {
IconPlus,
IconChevronDown,
IconArrowsDiagonal,
IconX,
IconSparkles,
IconFileText,
IconLanguage,
IconSearch,
} from "@tabler/icons-react";
import { useAtom } from "jotai";
import { useNavigate, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom";
import { usePageQuery } from "@/features/page/queries/page-query";
import { extractPageSlugId } from "@/lib";
import { useChatStream } from "../hooks/use-chat-stream";
import { useChatInfoQuery } from "../queries/ai-chat-query";
import ChatMessageList from "./chat-message-list";
import ChatInput from "./chat-input";
import AsideChatHistory from "./aside-chat-history";
import type { ChatAttachment, PageMention } from "../types/ai-chat.types";
import classes from "../styles/aside-chat-panel.module.css";
type QuickAction = {
icon: React.ReactNode;
label: string;
prompt: string;
};
export default function AsideChatPanel() {
const { t } = useTranslation();
const navigate = useNavigate();
const [, setAsideState] = useAtom(asideStateAtom);
const [chatId, setChatId] = useState<string | undefined>(undefined);
const [historyOpen, setHistoryOpen] = useState(false);
const [contextPages, setContextPages] = useState<PageMention[]>([]);
const { pageSlug } = useParams();
const slugId = extractPageSlugId(pageSlug);
const { data: page } = usePageQuery({ pageId: slugId });
const chatInfoQuery = useChatInfoQuery(chatId);
const {
messages,
streamingContent,
streamingToolCalls,
isStreaming,
error,
sendMessage,
stopGeneration,
hydrateFromServer,
} = useChatStream(chatId, {
onChatCreated: (newChatId) => {
setChatId(newChatId);
},
});
useEffect(() => {
if (page && !chatId) {
setContextPages([{ id: page.id, title: page.title || "", slugId: page.slugId }]);
}
}, [page, chatId]);
const handleRemoveContextPage = useCallback((pageId: string) => {
setContextPages((prev) => prev.filter((p) => p.id !== pageId));
}, []);
useEffect(() => {
if (chatInfoQuery.data?.messages) {
hydrateFromServer(chatInfoQuery.data.messages);
}
}, [chatInfoQuery.data, hydrateFromServer]);
// Drop the open chatId if the current user lost access to it (404/403 on
// the info fetch). Reverts the panel to a fresh chat instead of presenting
// an input tied to a chat the user does not own.
useEffect(() => {
if (chatId && chatInfoQuery.isError) {
setChatId(undefined);
}
}, [chatId, chatInfoQuery.isError]);
const handleNewChat = useCallback(
(event: React.MouseEvent<HTMLAnchorElement>) => {
if (
event.button !== 0 ||
event.ctrlKey ||
event.metaKey ||
event.shiftKey
) {
return;
}
event.preventDefault();
setChatId(undefined);
if (page) {
setContextPages([
{ id: page.id, title: page.title || "", slugId: page.slugId },
]);
}
},
[page],
);
const handleSelectChat = useCallback((selectedChatId: string) => {
setChatId(selectedChatId);
setHistoryOpen(false);
}, []);
const handleExpand = useCallback(() => {
if (chatId) {
navigate(`/ai/chat/${chatId}`);
} else {
navigate("/ai");
}
setAsideState({ tab: "", isAsideOpen: false });
}, [chatId, navigate, setAsideState]);
const handleClose = useCallback(() => {
setAsideState({ tab: "", isAsideOpen: false });
}, [setAsideState]);
const handleSend = useCallback(
(content: string, mentions: PageMention[], attachments: ChatAttachment[]) => {
const contextPageId = contextPages.length > 0 ? contextPages[0].id : undefined;
sendMessage(content, mentions, attachments, contextPageId);
},
[sendMessage, contextPages],
);
const handleQuickAction = useCallback(
(prompt: string) => {
handleSend(prompt, [], []);
},
[handleSend],
);
const hasMessages = messages.length > 0 || isStreaming;
const quickActions: QuickAction[] = [
{ icon: <IconFileText size={16} />, label: t("Summarize this page"), prompt: "Summarize this page" },
{ icon: <IconLanguage size={16} />, label: t("Translate this page"), prompt: "Translate this page" },
{ icon: <IconSearch size={16} />, label: t("Analyze for insights"), prompt: "Analyze this page for insights" },
];
return (
<div className={classes.panel}>
<div className={classes.toolbar}>
<Popover
opened={historyOpen}
onChange={setHistoryOpen}
position="bottom-start"
width={280}
shadow="md"
>
<Popover.Target>
<UnstyledButton
className={classes.titleButton}
onClick={() => setHistoryOpen((o) => !o)}
>
<span className={classes.titleText}>
{chatInfoQuery.data?.chat?.title || t("New chat")}
</span>
<IconChevronDown size={16} stroke={1.75} />
</UnstyledButton>
</Popover.Target>
<Popover.Dropdown>
<AsideChatHistory activeChatId={chatId} onSelect={handleSelectChat} />
</Popover.Dropdown>
</Popover>
<div className={classes.toolbarSpacer} />
<Tooltip label={t("New chat")} openDelay={250}>
<ActionIcon
component="a"
href="/ai"
variant="subtle"
color="dark"
aria-label={t("New chat")}
onClick={handleNewChat}
>
<IconPlus size={20} stroke={1.75} />
</ActionIcon>
</Tooltip>
<Tooltip label={t("Open full page")} openDelay={250}>
<ActionIcon
variant="subtle"
color="dark"
aria-label={t("Open full page")}
onClick={handleExpand}
>
<IconArrowsDiagonal size={18} stroke={1.5} />
</ActionIcon>
</Tooltip>
<Tooltip label={t("Close")} openDelay={250}>
<ActionIcon
variant="subtle"
color="dark"
aria-label={t("Close")}
onClick={handleClose}
>
<IconX size={20} stroke={1.75} />
</ActionIcon>
</Tooltip>
</div>
{error && (
<div
style={{
padding: "var(--mantine-spacing-xs) var(--mantine-spacing-sm)",
color: "var(--mantine-color-red-6)",
fontSize: "var(--mantine-font-size-xs)",
}}
>
{error}
</div>
)}
{hasMessages ? (
<>
<div className={classes.messages} data-aside-chat>
<ChatMessageList
messages={messages}
isStreaming={isStreaming}
streamingContent={streamingContent}
streamingToolCalls={streamingToolCalls}
/>
</div>
</>
) : (
<div className={classes.emptyState}>
<IconSparkles size={36} stroke={1.5} className={classes.emptyStateIcon} />
<div className={classes.emptyStateTitle}>{t("How can I help you today?")}</div>
<div className={classes.quickActions}>
{quickActions.map((action) => (
<button
key={action.label}
type="button"
className={classes.quickAction}
onClick={() => handleQuickAction(action.prompt)}
>
<span className={classes.quickActionIcon}>{action.icon}</span>
{action.label}
</button>
))}
</div>
</div>
)}
<div className={classes.inputArea}>
<ChatInput
isStreaming={isStreaming}
onSend={handleSend}
onStop={stopGeneration}
placeholder={t("Ask anything...")}
autofocus={false}
contextPages={contextPages}
onRemoveContextPage={handleRemoveContextPage}
variant="flat"
chatId={chatId}
/>
</div>
</div>
);
}
@@ -0,0 +1,91 @@
import {
IconSparkles,
IconSearch,
IconFilePlus,
IconEdit,
IconFileText,
} from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import ChatInput from "./chat-input";
import type { ChatAttachment, PageMention } from "../types/ai-chat.types";
import classes from "../styles/ai-chat.module.css";
type Suggestion = {
icon: React.ReactNode;
text: string;
prompt: string;
};
const SUGGESTIONS: Suggestion[] = [
{
icon: <IconSearch size={16} />,
text: "Search across all pages",
prompt: "Search for pages about ",
},
{
icon: <IconFilePlus size={16} />,
text: "Create a new page",
prompt: "Create a new page titled ",
},
{
icon: <IconFileText size={16} />,
text: "Summarize a page",
prompt: "Summarize the page @",
},
{
icon: <IconEdit size={16} />,
text: "Update page content",
prompt: "Update the page @",
},
];
type Props = {
isStreaming: boolean;
onSend: (content: string, mentions: PageMention[], attachments: ChatAttachment[]) => void;
onStop: () => void;
};
export default function ChatEmptyState({ isStreaming, onSend, onStop }: Props) {
const { t } = useTranslation();
const handleSuggestionClick = (prompt: string) => {
onSend(prompt, [], []);
};
return (
<div className={classes.emptyState}>
<IconSparkles size={48} stroke={1.5} className={classes.emptyStateIcon} />
<div className={classes.emptyStateBrand}>{t("Docmost AI")}</div>
<h1 className={classes.emptyStateTitle}>
{t("What can I help you with?")}
</h1>
<div className={classes.emptyStateInput}>
<ChatInput
isStreaming={isStreaming}
onSend={onSend}
onStop={onStop}
placeholder={t("Ask anything... Use @ to mention pages")}
autofocus
/>
</div>
<div className={classes.suggestionsSection}>
<h2 className={classes.suggestionsLabel}>{t("Get started")}</h2>
<div className={classes.suggestionsGrid}>
{SUGGESTIONS.map((s) => (
<button
key={s.text}
type="button"
className={classes.suggestionCard}
onClick={() => handleSuggestionClick(s.prompt)}
>
<span className={classes.suggestionIcon}>{s.icon}</span>
<span className={classes.suggestionText}>{s.text}</span>
</button>
))}
</div>
</div>
</div>
);
}
@@ -0,0 +1,424 @@
import { useCallback, useRef, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { IconArrowUp, IconPaperclip, IconPlayerStopFilled, IconX, IconFile, IconPhoto, IconPlus, IconAt, IconFileText } from "@tabler/icons-react";
import { Popover } from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { EditorContent, ReactNodeViewRenderer, useEditor } from "@tiptap/react";
import { Placeholder } from "@tiptap/extension-placeholder";
import { CharacterCount } from "@tiptap/extensions";
import { StarterKit } from "@tiptap/starter-kit";
import { Mention, LinkExtension } from "@docmost/editor-ext";
import EmojiCommand from "@/features/editor/extensions/emoji-command";
import mentionRenderItems from "@/features/editor/components/mention/mention-suggestion";
import MentionView from "@/features/editor/components/mention/mention-view";
import { uploadChatFile } from "../services/ai-chat-service";
import type { ChatAttachment, PageMention } from "../types/ai-chat.types";
import classes from "../styles/chat-input.module.css";
type PendingAttachment = ChatAttachment & { uploading: boolean };
const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "webp", "gif"];
const ACCEPTED_FILE_TYPES = ".pdf,.docx,.txt,.csv,.md,.png,.jpg,.jpeg,.webp";
// Kept in sync with MAX_ATTACHMENTS_PER_MESSAGE in apps/server/src/ee/ai-chat/ai-chat-limits.ts
const MAX_ATTACHMENTS_PER_MESSAGE = 5;
type Props = {
isStreaming: boolean;
onSend: (content: string, mentions: PageMention[], attachments: ChatAttachment[]) => void;
onStop: () => void;
placeholder?: string;
autofocus?: boolean;
contextPages?: PageMention[];
onRemoveContextPage?: (pageId: string) => void;
variant?: "card" | "flat";
showDisclaimer?: boolean;
chatId?: string;
};
function extractMentions(json: any): PageMention[] {
const mentions: PageMention[] = [];
const seen = new Set<string>();
function walk(node: any) {
if (node.type === "mention" && node.attrs?.entityType === "page" && node.attrs?.entityId) {
if (!seen.has(node.attrs.entityId)) {
seen.add(node.attrs.entityId);
mentions.push({
id: node.attrs.entityId,
title: node.attrs.label || "",
slugId: node.attrs.slugId || "",
});
}
}
if (node.content) {
for (const child of node.content) {
walk(child);
}
}
}
walk(json);
return mentions;
}
function editorJsonToText(json: any): string {
let text = "";
function walk(node: any) {
if (node.type === "text") {
text += node.text || "";
} else if (node.type === "mention") {
text += `@${node.attrs?.label || ""}`;
} else if (node.type === "paragraph") {
if (text.length > 0) text += "\n";
if (node.content) {
for (const child of node.content) {
walk(child);
}
}
return;
}
if (node.content) {
for (const child of node.content) {
walk(child);
}
}
}
walk(json);
return text;
}
export default function ChatInput({
isStreaming,
onSend,
onStop,
placeholder,
autofocus = true,
contextPages,
onRemoveContextPage,
variant = "card",
showDisclaimer = true,
chatId,
}: Props) {
const chatIdRef = useRef(chatId);
chatIdRef.current = chatId;
const { t } = useTranslation();
const [isEmpty, setIsEmpty] = useState(true);
const [pendingAttachments, setPendingAttachments] = useState<PendingAttachment[]>([]);
const [plusMenuOpen, setPlusMenuOpen] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const onSendRef = useRef(onSend);
onSendRef.current = onSend;
const handleFileSelect = useCallback(async (files: FileList | null) => {
if (!files?.length) return;
const room = MAX_ATTACHMENTS_PER_MESSAGE - pendingAttachments.length;
if (room <= 0) {
notifications.show({
color: "yellow",
message: t("You can attach up to {{max}} files per message.", {
max: MAX_ATTACHMENTS_PER_MESSAGE,
}),
});
if (fileInputRef.current) fileInputRef.current.value = "";
return;
}
const incoming = Array.from(files);
const accepted = incoming.slice(0, room);
if (incoming.length > accepted.length) {
notifications.show({
color: "yellow",
message: t(
"Only the first {{n}} file(s) were added (max {{max}} per message).",
{ n: accepted.length, max: MAX_ATTACHMENTS_PER_MESSAGE },
),
});
}
for (const file of accepted) {
const tempId = `uploading-${Date.now()}-${Math.random()}`;
const ext = file.name.split(".").pop()?.toLowerCase() || "";
const placeholder: PendingAttachment = {
id: tempId,
fileName: file.name,
fileExt: ext,
fileSize: file.size,
mimeType: file.type,
uploading: true,
};
setPendingAttachments((prev) => [...prev, placeholder]);
try {
const uploaded = await uploadChatFile(file, chatIdRef.current);
setPendingAttachments((prev) =>
prev.map((a) =>
a.id === tempId ? { ...uploaded, uploading: false } : a,
),
);
} catch {
setPendingAttachments((prev) => prev.filter((a) => a.id !== tempId));
}
}
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
}, [pendingAttachments.length, t]);
const removeAttachment = useCallback((id: string) => {
setPendingAttachments((prev) => prev.filter((a) => a.id !== id));
}, []);
const handleSubmit = useCallback(() => {
if (!editor || isStreaming) return;
const json = editor.getJSON();
const text = editorJsonToText(json).trim();
const readyAttachments = pendingAttachments.filter((a) => !a.uploading);
if (!text && readyAttachments.length === 0) return;
const mentions = extractMentions(json);
onSendRef.current(text, mentions, readyAttachments);
editor.commands.clearContent();
editor.commands.focus();
setPendingAttachments([]);
}, [isStreaming, pendingAttachments]);
const handleSubmitRef = useRef(handleSubmit);
handleSubmitRef.current = handleSubmit;
const editor = useEditor({
extensions: [
StarterKit.configure({
gapcursor: false,
dropcursor: false,
link: false,
}),
Placeholder.configure({
placeholder: placeholder || t("Ask anything... Use @ to mention pages"),
}),
CharacterCount.configure({
limit: 50000,
}),
LinkExtension,
EmojiCommand,
Mention.configure({
suggestion: {
allowSpaces: true,
items: () => [],
// @ts-ignore
render: mentionRenderItems,
},
HTMLAttributes: {
class: "mention",
},
}).extend({
addNodeView() {
this.editor.isInitialized = true;
return ReactNodeViewRenderer(MentionView);
},
}),
],
editorProps: {
attributes: {
role: "textbox",
"aria-label": placeholder || t("Ask anything... Use @ to mention pages"),
"aria-multiline": "true",
},
handleDOMEvents: {
keydown: (_view, event) => {
if (
["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Enter"].includes(
event.key,
)
) {
const emojiCommand = document.querySelector("#emoji-command");
const mentionPopup = document.querySelector("#mention");
if (emojiCommand || mentionPopup) {
return true;
}
}
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
handleSubmitRef.current();
return true;
}
},
},
},
content: "",
editable: true,
immediatelyRender: true,
shouldRerenderOnTransaction: false,
autofocus: autofocus ? "end" : false,
onUpdate: ({ editor: e }) => {
setIsEmpty(!e.getText().trim());
},
});
useEffect(() => {
if (editor && autofocus) {
editor.commands.focus();
}
}, [editor]);
const hasContent = !isEmpty || pendingAttachments.some((a) => !a.uploading) || (contextPages?.length ?? 0) > 0;
const wrapperClass = variant === "flat" ? classes.inputWrapperFlat : classes.inputWrapper;
return (
<>
<div className={wrapperClass} data-chat-input>
<input
ref={fileInputRef}
type="file"
accept={ACCEPTED_FILE_TYPES}
multiple
aria-label={t("Add files")}
tabIndex={-1}
style={{ display: "none" }}
onChange={(e) => handleFileSelect(e.target.files)}
/>
{((contextPages?.length ?? 0) > 0 || pendingAttachments.length > 0) && (
<div className={classes.attachmentChips}>
{contextPages?.map((page) => (
<div key={page.id} className={classes.attachmentChip}>
<IconFileText size={14} />
<span className={classes.attachmentChipName}>
{page.title || "Untitled"}
</span>
{onRemoveContextPage && (
<button
type="button"
className={classes.attachmentChipRemove}
onClick={() => onRemoveContextPage(page.id)}
aria-label={`Remove ${page.title}`}
>
<IconX size={12} />
</button>
)}
</div>
))}
{pendingAttachments.map((attachment) => (
<div
key={attachment.id}
className={`${classes.attachmentChip} ${attachment.uploading ? classes.attachmentChipUploading : ""}`}
>
{IMAGE_EXTENSIONS.includes(attachment.fileExt) ? (
<IconPhoto size={14} />
) : (
<IconFile size={14} />
)}
<span className={classes.attachmentChipName}>
{attachment.fileName}
</span>
{!attachment.uploading && (
<button
type="button"
className={classes.attachmentChipRemove}
onClick={() => removeAttachment(attachment.id)}
aria-label={`Remove ${attachment.fileName}`}
>
<IconX size={12} />
</button>
)}
</div>
))}
</div>
)}
<EditorContent editor={editor} className={classes.editorContent} />
<div className={classes.actions}>
<Popover
opened={plusMenuOpen}
onChange={setPlusMenuOpen}
position="top-start"
width={220}
shadow="md"
trapFocus
returnFocus
>
<Popover.Target>
<button
type="button"
className={classes.plusButton}
onClick={() => setPlusMenuOpen((o) => !o)}
aria-label="Add content"
>
<IconPlus size={14} />
</button>
</Popover.Target>
<Popover.Dropdown p={4}>
<button
type="button"
className={classes.plusMenuItem}
onClick={() => {
fileInputRef.current?.click();
setPlusMenuOpen(false);
}}
disabled={pendingAttachments.length >= MAX_ATTACHMENTS_PER_MESSAGE}
title={
pendingAttachments.length >= MAX_ATTACHMENTS_PER_MESSAGE
? t("Max {{max}} files per message", {
max: MAX_ATTACHMENTS_PER_MESSAGE,
})
: undefined
}
>
<IconPaperclip size={16} className={classes.plusMenuIcon} />
{t("Add files")}
</button>
<button
type="button"
className={classes.plusMenuItem}
onClick={() => {
editor?.commands.insertContent("@");
editor?.commands.focus();
setPlusMenuOpen(false);
}}
>
<IconAt size={16} className={classes.plusMenuIcon} />
Mention a page
</button>
</Popover.Dropdown>
</Popover>
<div style={{ flex: 1 }} />
{isStreaming ? (
<button
type="button"
className={classes.stopButton}
onClick={onStop}
aria-label="Stop generation"
>
<IconPlayerStopFilled size={14} />
</button>
) : (
<button
type="button"
className={classes.sendButton}
onClick={handleSubmit}
disabled={!hasContent}
aria-label="Send message"
>
<IconArrowUp size={16} stroke={2.5} />
</button>
)}
</div>
</div>
{showDisclaimer && (
<div className={classes.disclaimer}>
{t("AI-generated content may not be accurate.")}
</div>
)}
</>
);
}
@@ -0,0 +1,219 @@
import { useEffect, useRef, useCallback, useState } from "react";
import { ErrorBoundary } from "react-error-boundary";
import { IconArrowDown, IconAlertTriangle } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import { VisuallyHidden } from "@mantine/core";
import type { AiChatMessage, AiChatToolCall } from "../types/ai-chat.types";
import ChatMessage from "./chat-message";
import classes from "../styles/ai-chat.module.css";
function ChatMessageErrorFallback() {
const { t } = useTranslation();
return (
<div className={classes.messageErrorFallback}>
<IconAlertTriangle size={14} />
<span>{t("Failed to render this message.")}</span>
</div>
);
}
type Props = {
messages: AiChatMessage[];
isStreaming: boolean;
streamingContent: string;
streamingToolCalls: AiChatToolCall[];
};
const BOTTOM_THRESHOLD_PX = 32;
const SCROLL_UP_THRESHOLD_PX = 5;
const SMOOTH_SCROLL_SETTLE_MS = 600;
export default function ChatMessageList({
messages,
isStreaming,
streamingContent,
streamingToolCalls,
}: Props) {
const { t } = useTranslation();
const containerRef = useRef<HTMLDivElement>(null);
const bottomRef = useRef<HTMLDivElement>(null);
const isAtBottomRef = useRef(true);
const isAutoScrollingRef = useRef(false);
const prevScrollTopRef = useRef(0);
const [showScrollButton, setShowScrollButton] = useState(false);
// Dedicated status-region announcement for screen readers. Rather than
// putting aria-live on the whole transcript (which re-fires for every
// streamed token), announce "AI is thinking…" when streaming starts and
// the full assistant reply once streaming completes — a single, clean read.
const [statusAnnouncement, setStatusAnnouncement] = useState("");
const wasStreamingRef = useRef(false);
useEffect(() => {
const justStartedStreaming = isStreaming && !wasStreamingRef.current;
const justFinishedStreaming = !isStreaming && wasStreamingRef.current;
if (justStartedStreaming) {
setStatusAnnouncement(t("AI is thinking..."));
} else if (justFinishedStreaming) {
const lastMessage = messages[messages.length - 1];
if (lastMessage?.role === "assistant" && lastMessage.content) {
// Strip markdown punctuation so screen readers don't read symbols
// like # * _ ` ~ aloud. A plain-text version is fine — the styled
// version stays in the DOM for visual users.
const plainText = lastMessage.content
.replace(/[#*_`~]/g, "")
.replace(/\s+/g, " ")
.trim();
setStatusAnnouncement(plainText);
} else {
setStatusAnnouncement("");
}
}
wasStreamingRef.current = isStreaming;
}, [isStreaming, messages, t]);
const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => {
const container = containerRef.current;
if (!container) return;
isAutoScrollingRef.current = true;
const target = container.scrollHeight - container.clientHeight;
container.scrollTo({ top: target, behavior });
prevScrollTopRef.current = target;
isAtBottomRef.current = true;
setShowScrollButton(false);
if (behavior === "smooth") {
setTimeout(() => {
isAutoScrollingRef.current = false;
if (containerRef.current) {
prevScrollTopRef.current = containerRef.current.scrollTop;
}
}, SMOOTH_SCROLL_SETTLE_MS);
} else {
isAutoScrollingRef.current = false;
}
}, []);
const handleScroll = useCallback(() => {
if (isAutoScrollingRef.current) return;
const container = containerRef.current;
if (!container) return;
const currentScrollTop = container.scrollTop;
const scrolledUp =
currentScrollTop < prevScrollTopRef.current - SCROLL_UP_THRESHOLD_PX;
prevScrollTopRef.current = currentScrollTop;
const distanceFromBottom =
container.scrollHeight - currentScrollTop - container.clientHeight;
const atBottom = distanceFromBottom <= BOTTOM_THRESHOLD_PX;
if (scrolledUp) {
isAtBottomRef.current = atBottom;
} else if (atBottom) {
isAtBottomRef.current = true;
}
setShowScrollButton(!atBottom);
}, []);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
container.addEventListener("scroll", handleScroll, { passive: true });
return () => container.removeEventListener("scroll", handleScroll);
}, [handleScroll]);
// Instant scroll during streaming to keep up with rapid updates
useEffect(() => {
if (isAtBottomRef.current) {
scrollToBottom("instant");
}
}, [streamingContent, streamingToolCalls.length, scrollToBottom]);
// Smooth scroll for new messages. Always force-scroll when the latest
// message is from the user (they just sent it), even if they were reading
// scrollback.
useEffect(() => {
const lastMessage = messages[messages.length - 1];
const lastIsUser = lastMessage?.role === "user";
if (lastIsUser || isAtBottomRef.current) {
scrollToBottom("smooth");
return;
}
// No auto-scroll: recompute from actual layout so that chat switches to
// content that doesn't overflow correctly hide the button even when no
// scroll event fires.
const container = containerRef.current;
if (!container) return;
const distanceFromBottom =
container.scrollHeight - container.scrollTop - container.clientHeight;
const atBottom = distanceFromBottom <= BOTTOM_THRESHOLD_PX;
isAtBottomRef.current = atBottom;
setShowScrollButton(!atBottom);
}, [messages, scrollToBottom]);
return (
<div className={classes.messageListWrapper}>
{/* Single status region for chat announcements. Kept outside the
scrolling transcript so changes here trigger one polite read per
state change instead of re-announcing every streamed token. */}
<VisuallyHidden role="status" aria-live="polite">
{statusAnnouncement}
</VisuallyHidden>
<div
ref={containerRef}
className={classes.messageList}
aria-label={t("Chat transcript")}
>
{messages.map((msg) => (
<ErrorBoundary
key={msg.id}
fallback={<ChatMessageErrorFallback />}
>
<ChatMessage message={msg} />
</ErrorBoundary>
))}
{isStreaming && (
<ErrorBoundary
resetKeys={[streamingContent, streamingToolCalls.length]}
fallback={<ChatMessageErrorFallback />}
>
<ChatMessage
message={{
id: "streaming",
chatId: "",
role: "assistant",
content: null,
toolCalls: null,
metadata: null,
createdAt: new Date().toISOString(),
}}
isStreaming
streamingContent={streamingContent}
streamingToolCalls={streamingToolCalls}
/>
</ErrorBoundary>
)}
<div ref={bottomRef} />
</div>
{showScrollButton && (
<button
type="button"
aria-label={t("Scroll to bottom")}
className={classes.scrollToBottomButton}
onClick={() => scrollToBottom("smooth")}
>
<IconArrowDown size={16} stroke={2} />
</button>
)}
</div>
);
}
@@ -0,0 +1,156 @@
import { useCallback } from "react";
import { useNavigate } from "react-router";
import { useTranslation } from "react-i18next";
import DOMPurify from "dompurify";
import { ActionIcon, Tooltip } from "@mantine/core";
import {
IconCheck,
IconCopy,
IconFile,
IconLoader2,
IconPhoto,
} from "@tabler/icons-react";
import { markdownToHtml } from "@docmost/editor-ext";
import { CopyButton } from "@/components/common/copy-button";
import type { AiChatMessage, AiChatToolCall } from "../types/ai-chat.types";
import ChatToolGroup from "./chat-tool-group";
import classes from "../styles/chat-message.module.css";
import CopyTextButton from "@/components/common/copy.tsx";
const chatSanitizer = DOMPurify();
chatSanitizer.addHook("afterSanitizeAttributes", (node) => {
if (node.tagName === "A") {
const href = node.getAttribute("href") || "";
if (href.startsWith("http://") || href.startsWith("https://")) {
node.setAttribute("target", "_blank");
node.setAttribute("rel", "noopener noreferrer");
}
}
});
const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "webp", "gif"];
type Props = {
message: AiChatMessage;
isStreaming?: boolean;
streamingContent?: string;
streamingToolCalls?: AiChatToolCall[];
};
export default function ChatMessage({
message,
isStreaming,
streamingContent,
streamingToolCalls,
}: Props) {
const navigate = useNavigate();
const { t } = useTranslation();
const handleContentClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
const target = e.target as HTMLElement;
const anchor = target.closest("a");
if (!anchor) return;
const href = anchor.getAttribute("href");
if (href && (href.startsWith("/s/") || href.startsWith("/p/"))) {
e.preventDefault();
navigate(href);
}
},
[navigate],
);
if (message.role === "tool") return null;
const isUser = message.role === "user";
const content = isStreaming ? streamingContent : message.content;
const toolCalls = isStreaming ? streamingToolCalls : message.toolCalls;
if (isUser) {
const displayContent = (content || "").replace(
/\n\n<referenced_pages>[\s\S]*<\/referenced_pages>$/,
"",
);
const attachments =
(message.metadata?.attachments as {
id: string;
fileName: string;
fileExt: string;
}[]) || [];
return (
<div
className={classes.userMessage}
role="article"
aria-label={t("You said:")}
>
<div className={classes.userBubble}>
{attachments.length > 0 && (
<div className={classes.messageAttachments}>
{attachments.map((a) => (
<span key={a.id} className={classes.messageAttachmentChip}>
{IMAGE_EXTENSIONS.includes(a.fileExt) ? (
<IconPhoto size={13} />
) : (
<IconFile size={13} />
)}
{a.fileName}
</span>
))}
</div>
)}
{displayContent}
</div>
</div>
);
}
// Only label the article when there's something meaningful to announce.
// Tool-only assistant turns (no text) shouldn't announce "Assistant said:" with empty content.
const hasAnnouncableContent = Boolean(content);
return (
<div
className={classes.assistantMessage}
role="article"
aria-label={hasAnnouncableContent ? t("Assistant said:") : undefined}
>
<div className={classes.messageContent}>
{toolCalls && toolCalls.length > 0 && (
<ChatToolGroup toolCalls={toolCalls} isStreaming={isStreaming} />
)}
{content && (
<div
onClick={handleContentClick}
dangerouslySetInnerHTML={{
__html: chatSanitizer.sanitize(
markdownToHtml(content) as string,
{ ADD_ATTR: ["target", "rel"] },
),
}}
/>
)}
{isStreaming && (
<>
{!content && (
<span className={classes.processingIndicator}>
<IconLoader2 size={16} className={classes.processingSpinner} />
Thinking
</span>
)}
<span className={classes.streamingCursor} />
</>
)}
</div>
{!isStreaming && message.content && (
<div className={classes.messageActions}>
<CopyTextButton
text={message?.content}
label={t("Copy assistant response")}
/>
</div>
)}
</div>
);
}
@@ -0,0 +1,65 @@
import { useState } from "react";
import {
IconChevronRight,
IconChevronDown,
IconLoader2,
} from "@tabler/icons-react";
import type { AiChatToolCall } from "../types/ai-chat.types";
import ChatToolResult, { TOOL_LABELS } from "./chat-tool-result";
import classes from "../styles/chat-message.module.css";
type Props = {
toolCalls: AiChatToolCall[];
isStreaming?: boolean;
};
export default function ChatToolGroup({ toolCalls, isStreaming }: Props) {
const [expanded, setExpanded] = useState(false);
if (!toolCalls || toolCalls.length === 0) return null;
const activeCall =
isStreaming && toolCalls.length > 0
? [...toolCalls].reverse().find((tc) => tc.result === undefined)
: undefined;
const activeLabel = activeCall
? TOOL_LABELS[activeCall.name] || activeCall.name
: null;
return (
<div className={classes.toolGroup}>
<div
className={classes.toolGroupHeader}
role="button"
tabIndex={0}
aria-expanded={expanded}
onClick={() => setExpanded((prev) => !prev)}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
setExpanded((prev) => !prev);
}
}}
>
{activeLabel ? (
<IconLoader2 size={12} className={classes.processingSpinner} />
) : expanded ? (
<IconChevronDown size={12} />
) : (
<IconChevronRight size={12} />
)}
<span className={classes.toolGroupLabel}>
{activeLabel ? `${activeLabel}` : `Steps ${toolCalls.length}`}
</span>
</div>
{expanded && (
<div className={classes.toolGroupSteps}>
{toolCalls.map((tc) => (
<ChatToolResult key={tc.id} toolCall={tc} />
))}
</div>
)}
</div>
);
}
@@ -0,0 +1,49 @@
import { useState } from "react";
import { IconChevronRight, IconChevronDown } from "@tabler/icons-react";
import type { AiChatToolCall } from "../types/ai-chat.types";
import classes from "../styles/chat-message.module.css";
export const TOOL_LABELS: Record<string, string> = {
list_spaces: "Listed spaces",
search_pages: "Searched pages",
get_page: "Read page",
create_page: "Created page",
update_page: "Updated page",
};
type Props = {
toolCall: AiChatToolCall;
};
export default function ChatToolResult({ toolCall }: Props) {
const [expanded, setExpanded] = useState(false);
const label = TOOL_LABELS[toolCall.name] || toolCall.name;
return (
<div className={classes.toolStep}>
<div
className={classes.toolStepRow}
onClick={() => setExpanded((prev) => !prev)}
>
<span className={classes.toolStepBullet}>·</span>
{expanded ? (
<IconChevronDown size={12} />
) : (
<IconChevronRight size={12} />
)}
<span>{label}</span>
</div>
{expanded && (
<div className={classes.toolStepDetails}>
<pre style={{ margin: 0, whiteSpace: "pre-wrap" }}>
{JSON.stringify(
{ args: toolCall.args, result: toolCall.result },
null,
2,
)}
</pre>
</div>
)}
</div>
);
}
@@ -0,0 +1,67 @@
import { Badge, Group, Text, Switch, Tooltip } from "@mantine/core";
import { useAtom } from "jotai";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
import { notifications } from "@mantine/notifications";
import { useHasFeature } from "@/ee/hooks/use-feature";
import { Feature } from "@/ee/features";
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
export default function EnableAiChat() {
const { t } = useTranslation();
return (
<Group justify="space-between" wrap="nowrap" gap="xl">
<div>
<Group gap="xs" align="center">
<Text size="md">{t("AI Chat")}</Text>
<Badge color="gray" variant="light" size="sm" radius="sm">
{t("Beta")}
</Badge>
</Group>
<Text size="sm" c="dimmed">
{t(
"Enable AI Chat to allow users to have multi-turn conversations with AI about your workspace content.",
)}
</Text>
</div>
<AiChatToggle />
</Group>
);
}
function AiChatToggle() {
const { t } = useTranslation();
const [workspace, setWorkspace] = useAtom(workspaceAtom);
const [checked, setChecked] = useState(workspace?.settings?.ai?.chat);
const hasAccess = useHasFeature(Feature.AI);
const upgradeLabel = useUpgradeLabel();
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.currentTarget.checked;
try {
const updatedWorkspace = await updateWorkspace({ aiChat: value } as any);
setChecked(value);
setWorkspace(updatedWorkspace);
} catch (err: any) {
notifications.show({
message: err?.response?.data?.message,
color: "red",
});
}
};
return (
<Tooltip label={upgradeLabel} disabled={hasAccess} refProp="rootRef">
<Switch
defaultChecked={checked}
onChange={handleChange}
disabled={!hasAccess}
aria-label={t("Toggle AI Chat")}
/>
</Tooltip>
);
}
@@ -0,0 +1,227 @@
import { useState, useCallback, useEffect, useRef } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import { sendChatMessage } from "../services/ai-chat-service";
import type {
AiChatMessage,
AiChatStreamEvent,
AiChatToolCall,
ChatAttachment,
PageMention,
} from "../types/ai-chat.types";
type ChatStreamOptions = {
onChatCreated?: (chatId: string) => void;
};
export function useChatStream(
chatId: string | undefined,
options?: ChatStreamOptions,
) {
const [messages, setMessages] = useState<AiChatMessage[]>([]);
const [streamingContent, setStreamingContent] = useState("");
const [streamingToolCalls, setStreamingToolCalls] = useState<AiChatToolCall[]>(
[],
);
const [isStreaming, setIsStreaming] = useState(false);
const [error, setError] = useState<string | null>(null);
const [errorCode, setErrorCode] = useState<string | null>(null);
const [isRetryable, setIsRetryable] = useState(false);
const abortRef = useRef<AbortController | null>(null);
const queryClient = useQueryClient();
const navigate = useNavigate();
const currentChatIdRef = useRef(chatId);
currentChatIdRef.current = chatId;
// Tracks which chatId the local `messages` state currently represents.
// Set when we seed from a server fetch AND when we optimistically own a
// freshly-created chat after `chat_created`. This is the single authority
// marker that keeps server-state effects from clobbering in-flight streams.
const hydratedChatIdRef = useRef<string | undefined>(undefined);
// Reset local state when the consumer switches to a different chat.
// Skip the reset if the new chatId is one the hook itself already claimed
// during a new-chat flow — in that case our optimistic state is the truth.
useEffect(() => {
if (chatId && chatId === hydratedChatIdRef.current) return;
hydratedChatIdRef.current = undefined;
setMessages([]);
setError(null);
setErrorCode(null);
setIsRetryable(false);
}, [chatId]);
const hydrateFromServer = useCallback((msgs: AiChatMessage[]) => {
const forId = currentChatIdRef.current;
if (!forId) return;
if (hydratedChatIdRef.current === forId) return;
hydratedChatIdRef.current = forId;
setMessages(msgs);
}, []);
const sendMessage = useCallback(
(content: string, mentions: PageMention[] = [], attachments: ChatAttachment[] = [], contextPageId?: string) => {
if (isStreaming || (!content.trim() && attachments.length === 0)) return;
setError(null);
setErrorCode(null);
setIsRetryable(false);
setIsStreaming(true);
setStreamingContent("");
setStreamingToolCalls([]);
const metadata: Record<string, unknown> = {};
if (mentions.length) {
metadata.mentionedPageIds = mentions.map((m) => m.id);
}
if (attachments.length) {
metadata.attachments = attachments.map((a) => ({
id: a.id,
fileName: a.fileName,
fileExt: a.fileExt,
}));
}
const userMessage: AiChatMessage = {
id: `temp-${Date.now()}`,
chatId: currentChatIdRef.current || "",
role: "user",
content,
toolCalls: null,
metadata: Object.keys(metadata).length ? metadata : null,
createdAt: new Date().toISOString(),
};
setMessages((prev) => [...prev, userMessage]);
const attachmentIds = attachments.map((a) => a.id);
const abortController = sendChatMessage(
{
chatId: currentChatIdRef.current,
content,
mentionedPageIds: mentions.map((m) => m.id),
...(contextPageId && { contextPageId }),
...(attachmentIds.length && { attachmentIds }),
},
(event: AiChatStreamEvent) => {
switch (event.type) {
case "chat_created":
currentChatIdRef.current = event.chatId;
// Claim authority over this new chatId so when the consumer's
// prop catches up via navigation/onChatCreated, the reset effect
// sees a match and preserves our optimistic messages.
hydratedChatIdRef.current = event.chatId;
if (options?.onChatCreated) {
options.onChatCreated(event.chatId);
} else {
navigate(`/ai/chat/${event.chatId}`, { replace: true });
}
queryClient.invalidateQueries({ queryKey: ["ai-chats"] });
break;
case "content":
setStreamingContent((prev) => prev + event.text);
break;
case "tool_call":
setStreamingToolCalls((prev) => [
...prev,
{
id: event.id,
name: event.name,
args: event.args,
},
]);
break;
case "tool_result":
setStreamingToolCalls((prev) =>
prev.map((tc) =>
tc.id === event.id ? { ...tc, result: event.result } : tc,
),
);
break;
case "done": {
setStreamingContent((currentContent) => {
setStreamingToolCalls((currentToolCalls) => {
const assistantMessage: AiChatMessage = {
id: event.messageId,
chatId: currentChatIdRef.current || "",
role: "assistant",
content: currentContent || null,
toolCalls: currentToolCalls.length
? currentToolCalls
: null,
metadata: event.usage ? { tokenUsage: event.usage } : null,
createdAt: new Date().toISOString(),
};
setMessages((prev) => [...prev, assistantMessage]);
return [];
});
return "";
});
setIsStreaming(false);
queryClient.invalidateQueries({
queryKey: ["ai-chat", currentChatIdRef.current],
});
break;
}
case "error":
setError(event.message);
setErrorCode(event.code || null);
setIsRetryable(event.retryable || false);
setIsStreaming(false);
break;
}
},
(errorMsg) => {
setError(errorMsg);
setIsStreaming(false);
},
() => {
setIsStreaming(false);
},
);
abortRef.current = abortController;
},
[isStreaming, navigate, queryClient],
);
const stopGeneration = useCallback(() => {
abortRef.current?.abort();
abortRef.current = null;
setStreamingContent((currentContent) => {
setStreamingToolCalls((currentToolCalls) => {
if (currentContent || currentToolCalls.length > 0) {
const partialMessage: AiChatMessage = {
id: `stopped-${Date.now()}`,
chatId: currentChatIdRef.current || "",
role: "assistant",
content: currentContent || null,
toolCalls: currentToolCalls.length ? currentToolCalls : null,
metadata: null,
createdAt: new Date().toISOString(),
};
setMessages((prev) => [...prev, partialMessage]);
}
return [];
});
return "";
});
setIsStreaming(false);
}, []);
return {
messages,
streamingContent,
streamingToolCalls,
isStreaming,
error,
errorCode,
isRetryable,
sendMessage,
stopGeneration,
hydrateFromServer,
};
}
@@ -0,0 +1,39 @@
import { useParams } from "react-router-dom";
import { ErrorBoundary } from "react-error-boundary";
import { Button } from "@mantine/core";
import { IconAlertTriangle } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import AiChatLayout from "../components/ai-chat-layout";
import { EmptyState } from "@/components/ui/empty-state.tsx";
import classes from "../styles/ai-chat.module.css";
export default function AiChat() {
const { t } = useTranslation();
const { chatId } = useParams<{ chatId: string }>();
return (
<div className={classes.layout}>
<ErrorBoundary
resetKeys={[chatId]}
fallbackRender={({ resetErrorBoundary }) => (
<EmptyState
icon={IconAlertTriangle}
title={t("Failed to load chat. An error occurred.")}
action={
<Button
variant="default"
size="sm"
mt="xs"
onClick={resetErrorBoundary}
>
{t("Try again")}
</Button>
}
/>
)}
>
<AiChatLayout />
</ErrorBoundary>
</div>
);
}
@@ -0,0 +1,61 @@
import {
useQuery,
useMutation,
useQueryClient,
useInfiniteQuery,
} from "@tanstack/react-query";
import {
listChats,
getChatInfo,
deleteChat,
updateChatTitle,
searchChats,
} from "../services/ai-chat-service";
export function useChatsQuery() {
return useInfiniteQuery({
queryKey: ["ai-chats"],
queryFn: ({ pageParam }) =>
listChats({ cursor: pageParam, limit: 30 }),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) =>
lastPage.meta.hasNextPage ? lastPage.meta.nextCursor : undefined,
});
}
export function useChatInfoQuery(chatId: string | undefined) {
return useQuery({
queryKey: ["ai-chat", chatId],
queryFn: () => getChatInfo(chatId!),
enabled: !!chatId,
});
}
export function useDeleteChatMutation() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (chatId: string) => deleteChat(chatId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["ai-chats"] });
},
});
}
export function useUpdateChatTitleMutation() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ chatId, title }: { chatId: string; title: string }) =>
updateChatTitle(chatId, title),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["ai-chats"] });
},
});
}
export function useSearchChatsQuery(query: string) {
return useQuery({
queryKey: ["ai-chats-search", query],
queryFn: () => searchChats(query),
enabled: query.length > 0,
});
}
@@ -0,0 +1,144 @@
import api from "@/lib/api-client.ts";
import type {
AiChat,
AiChatMessage,
AiChatStreamEvent,
ChatAttachment,
} from "../types/ai-chat.types";
import { IPagination } from "@/lib/types.ts";
export async function createChat(): Promise<AiChat> {
const req = await api.post<AiChat>("/ai/chats/create");
return req.data;
}
export async function listChats(params?: {
limit?: number;
cursor?: string;
}): Promise<IPagination<AiChat>> {
const req = await api.post("/ai/chats", params);
return req.data;
}
export async function getChatInfo(
chatId: string,
): Promise<{ chat: AiChat; messages: AiChatMessage[] }> {
const req = await api.post("/ai/chats/info", { chatId });
return req.data;
}
export async function deleteChat(chatId: string): Promise<void> {
await api.post("/ai/chats/delete", { chatId });
}
export async function updateChatTitle(
chatId: string,
title: string,
): Promise<void> {
await api.post("/ai/chats/update", { chatId, title });
}
export async function searchChats(query: string): Promise<AiChat[]> {
const req = await api.post("/ai/chats/search", { query });
return req.data;
}
export async function uploadChatFile(
file: File,
chatId?: string,
): Promise<ChatAttachment> {
const formData = new FormData();
formData.append("file", file);
if (chatId) {
formData.append("chatId", chatId);
}
return await api.post("/ai/chats/upload", formData, {
headers: { "Content-Type": "multipart/form-data" },
});
}
export function sendChatMessage(
params: {
chatId?: string;
content: string;
mentionedPageIds?: string[];
contextPageId?: string;
attachmentIds?: string[];
},
onEvent: (event: AiChatStreamEvent) => void,
onError?: (error: string) => void,
onComplete?: () => void,
): AbortController {
const abortController = new AbortController();
(async () => {
try {
const response = await fetch("/api/ai/chats/send", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(params),
signal: abortController.signal,
credentials: "include",
});
if (!response.ok) {
const errorBody = await response.text();
let errorMessage = `HTTP error ${response.status}`;
try {
const parsed = JSON.parse(errorBody);
errorMessage = parsed.message || errorMessage;
} catch {
// use default
}
onError?.(errorMessage);
return;
}
const reader = response.body?.getReader();
const decoder = new TextDecoder();
if (!reader) {
onError?.("Response body is not readable");
return;
}
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6);
if (data === "[DONE]") {
onComplete?.();
return;
}
try {
const parsed = JSON.parse(data) as AiChatStreamEvent;
onEvent(parsed);
} catch {
// Skip invalid JSON
}
}
}
}
} finally {
reader.releaseLock();
}
onComplete?.();
} catch (error: any) {
if (error.name !== "AbortError") {
onError?.(error.message);
}
}
})();
return abortController;
}
@@ -0,0 +1,171 @@
.layout {
display: flex;
height: 100%;
width: 100%;
}
.main {
flex: 1;
display: flex;
flex-direction: column;
height: calc(100vh - 45px - 2 * var(--mantine-spacing-md));
max-width: 900px;
margin: 0 auto;
width: 100%;
}
.messageListWrapper {
flex: 1;
position: relative;
display: flex;
flex-direction: column;
min-height: 0;
height: 100%;
width: 100%;
}
.messageList {
flex: 1;
overflow-y: auto;
padding: var(--mantine-spacing-md) var(--mantine-spacing-lg);
}
.messageErrorFallback {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
margin-bottom: var(--mantine-spacing-lg);
border-radius: var(--mantine-radius-sm);
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
font-size: var(--mantine-font-size-xs);
}
.scrollToBottomButton {
position: absolute;
bottom: 12px;
left: 50%;
transform: translateX(-50%);
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6));
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
cursor: pointer;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: background-color 120ms ease, border-color 120ms ease, transform 120ms ease;
z-index: 2;
}
.scrollToBottomButton:hover {
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5));
border-color: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-3));
}
.scrollToBottomButton:active {
transform: translateX(-50%) scale(0.95);
}
.inputArea {
padding: var(--mantine-spacing-xs) var(--mantine-spacing-lg) var(--mantine-spacing-lg);
}
/* Empty state - Notion AI style centered layout */
.emptyState {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--mantine-spacing-xl) var(--mantine-spacing-lg);
}
.emptyStateIcon {
width: 48px;
height: 48px;
margin-bottom: var(--mantine-spacing-sm);
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
}
.emptyStateBrand {
font-size: var(--mantine-font-size-xs);
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--mantine-color-dimmed);
margin-bottom: var(--mantine-spacing-xs);
}
.emptyStateTitle {
font-size: 1.5rem;
font-weight: 600;
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
margin-top: 0;
margin-bottom: var(--mantine-spacing-xl);
text-align: center;
}
.emptyStateInput {
width: 100%;
max-width: 600px;
margin-bottom: var(--mantine-spacing-xl);
padding: 6px 0;
}
.suggestionsSection {
width: 100%;
max-width: 600px;
}
.suggestionsLabel {
font-size: var(--mantine-font-size-xs);
font-weight: 500;
color: var(--mantine-color-dimmed);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-top: 0;
margin-bottom: var(--mantine-spacing-sm);
}
.suggestionsGrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--mantine-spacing-sm);
}
.suggestionCard {
display: flex;
align-items: flex-start;
gap: var(--mantine-spacing-sm);
padding: var(--mantine-spacing-sm) var(--mantine-spacing-md);
border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
border-radius: var(--mantine-radius-md);
cursor: pointer;
background: transparent;
transition: background-color 150ms, border-color 150ms;
text-align: left;
width: 100%;
@mixin hover {
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
border-color: light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}
}
.suggestionIcon {
flex-shrink: 0;
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
margin-top: 1px;
}
.suggestionText {
font-size: var(--mantine-font-size-sm);
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
line-height: 1.4;
}
@@ -0,0 +1,139 @@
.panel {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.toolbar {
display: flex;
align-items: center;
gap: 4px;
padding: 0 0 var(--mantine-spacing-sm) 0;
border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
}
.toolbarSpacer {
flex: 1;
}
.titleButton {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
border-radius: var(--mantine-radius-sm);
font-size: var(--mantine-font-size-sm);
font-weight: 500;
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
max-width: 60%;
min-width: 0;
}
.titleButton:hover {
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6));
}
.titleText {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
.messages {
flex: 1;
overflow-y: auto;
padding: var(--mantine-spacing-sm) 0;
scroll-behavior: smooth;
}
.inputArea {
padding-top: var(--mantine-spacing-sm);
}
.emptyState {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--mantine-spacing-md);
padding: var(--mantine-spacing-xl) var(--mantine-spacing-sm);
}
.emptyStateIcon {
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
}
.emptyStateTitle {
font-size: var(--mantine-font-size-lg);
font-weight: 600;
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
text-align: center;
}
.quickActions {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
}
.quickAction {
display: flex;
align-items: center;
gap: var(--mantine-spacing-sm);
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
border-radius: var(--mantine-radius-md);
cursor: pointer;
background: transparent;
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
font-size: var(--mantine-font-size-sm);
text-align: left;
width: 100%;
transition: background-color 150ms, border-color 150ms;
@mixin hover {
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
border-color: light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}
}
.quickActionIcon {
flex-shrink: 0;
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
}
.historyList {
max-height: 300px;
overflow-y: auto;
}
.historyItem {
display: flex;
align-items: center;
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
cursor: pointer;
border-radius: var(--mantine-radius-sm);
font-size: var(--mantine-font-size-sm);
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
transition: background-color 150ms;
@mixin hover {
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
}
&[data-active] {
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
}
}
.historyItemTitle {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@@ -0,0 +1,242 @@
.inputWrapper {
position: relative;
overflow: hidden;
border: 1px solid
light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
border-radius: 16px;
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
box-shadow: light-dark(
0 2px 40px 4px rgba(0, 0, 0, 0.07),
0 2px 40px 4px rgba(0, 0, 0, 0.5)
);
transition:
border-color 150ms,
box-shadow 150ms;
&:focus-within {
border-color: light-dark(
var(--mantine-color-gray-3),
var(--mantine-color-dark-4)
);
box-shadow: light-dark(
0 4px 48px 6px rgba(0, 0, 0, 0.09),
0 4px 48px 6px rgba(0, 0, 0, 0.6)
);
}
}
.inputWrapperFlat {
position: relative;
overflow: hidden;
border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
border-radius: 12px;
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
box-shadow: none;
transition: border-color 150ms;
&:focus-within {
border-color: light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}
}
.disclaimer {
margin-top: 6px;
text-align: center;
font-size: var(--mantine-font-size-xs);
color: var(--mantine-color-dimmed);
}
.attachmentChips {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 10px 14px 0;
}
.attachmentChip {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 8px;
border-radius: 8px;
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6));
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
font-size: var(--mantine-font-size-xs);
max-width: 200px;
}
.attachmentChipUploading {
opacity: 0.55;
}
.attachmentChipName {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.attachmentChipRemove {
display: flex;
align-items: center;
justify-content: center;
border: none;
background: none;
cursor: pointer;
padding: 0;
margin-left: 2px;
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
border-radius: 50%;
@mixin hover {
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
}
}
.editorContent {
overflow: hidden;
:global(.ProseMirror) {
outline: none;
border: none;
background-color: transparent;
padding: 14px 18px 8px;
font-size: 15px;
line-height: 1.6;
max-height: 200px;
overflow-y: auto;
min-height: 24px;
color: light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0));
}
:global(.ProseMirror p) {
margin-block-start: 0;
margin-block-end: 0;
}
:global(.ProseMirror p.is-editor-empty:first-child::before) {
color: var(--mantine-color-placeholder);
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
}
.actions {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 4px 12px 10px;
gap: var(--mantine-spacing-xs);
}
.sendButton {
width: 28px;
height: 28px;
min-width: 28px;
min-height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: none;
cursor: pointer;
transition: background-color 150ms, opacity 150ms;
background: light-dark(var(--mantine-color-dark-9), var(--mantine-color-gray-0));
color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-9));
&:disabled {
opacity: 0.25;
cursor: default;
}
@mixin hover {
&:not(:disabled) {
opacity: 0.85;
}
}
}
.attachButton {
display: flex;
align-items: center;
justify-content: center;
border: none;
background: none;
cursor: pointer;
padding: 2px;
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
transition: color 150ms;
@mixin hover {
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
}
}
.plusButton {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
background: none;
cursor: pointer;
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-3));
transition: color 150ms, background-color 150ms;
@mixin hover {
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
}
}
.plusMenuItem {
display: flex;
align-items: center;
gap: var(--mantine-spacing-sm);
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
border: none;
background: none;
cursor: pointer;
width: 100%;
font-size: var(--mantine-font-size-sm);
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
border-radius: var(--mantine-radius-sm);
transition: background-color 150ms;
@mixin hover {
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
}
&:disabled {
opacity: 0.45;
cursor: not-allowed;
background: none;
}
}
.plusMenuIcon {
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
}
.stopButton {
width: 28px;
height: 28px;
min-width: 28px;
min-height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
cursor: pointer;
transition: background-color 150ms;
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6));
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
@mixin hover {
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
}
}
@@ -0,0 +1,286 @@
.message {
margin-bottom: var(--mantine-spacing-lg);
}
.userMessage {
composes: message;
display: flex;
justify-content: flex-end;
}
.userBubble {
max-width: 75%;
padding: 10px 16px;
border-radius: 18px;
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6));
color: light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0));
font-size: 15px;
line-height: 1.6;
word-wrap: break-word;
overflow-wrap: break-word;
}
[data-aside-chat] .userBubble {
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
}
.userBubble p {
margin: 0;
}
.messageAttachments {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 6px;
}
.messageAttachmentChip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 6px;
background: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-2));
font-size: var(--mantine-font-size-xs);
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.assistantMessage {
composes: message;
}
.messageContent {
font-size: 15px;
line-height: 1.7;
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-1));
word-wrap: break-word;
overflow-wrap: break-word;
}
.messageContent p {
margin: 0 0 0.75em 0;
}
.messageContent p:last-child {
margin-bottom: 0;
}
.messageContent ul,
.messageContent ol {
margin: 0.5em 0 0.75em 0;
padding-left: 1.5em;
}
.messageContent li {
margin-bottom: 0.3em;
}
.messageContent h1,
.messageContent h2,
.messageContent h3 {
margin: 1em 0 0.5em 0;
font-weight: 600;
color: light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0));
}
.messageContent h1 {
font-size: 1.4em;
}
.messageContent h2 {
font-size: 1.2em;
}
.messageContent h3 {
font-size: 1.05em;
}
.messageContent pre {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
padding: var(--mantine-spacing-sm) var(--mantine-spacing-md);
border-radius: var(--mantine-radius-md);
overflow-x: auto;
font-size: var(--mantine-font-size-sm);
margin: 0.75em 0;
}
.messageContent code {
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6));
padding: 2px 6px;
border-radius: 4px;
font-size: 0.88em;
}
.messageContent pre code {
background: none;
padding: 0;
}
.messageContent blockquote {
border-left: 3px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
padding-left: var(--mantine-spacing-md);
margin: 0.75em 0;
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-2));
}
.messageContent a {
color: light-dark(var(--mantine-color-blue-7), var(--mantine-color-blue-4));
text-decoration: none;
@mixin hover {
text-decoration: underline;
}
}
.messageContent a[href^="/s/"],
.messageContent a[href^="/p/"] {
color: light-dark(var(--mantine-color-dark-4), var(--mantine-color-dark-1));
font-weight: 500;
text-decoration: none;
cursor: pointer;
@mixin light {
border-bottom: 0.05em solid var(--mantine-color-dark-0);
}
@mixin dark {
border-bottom: 0.05em solid var(--mantine-color-dark-2);
}
@mixin hover {
text-decoration: none;
@mixin light {
border-bottom-color: var(--mantine-color-dark-2);
}
@mixin dark {
border-bottom-color: var(--mantine-color-dark-0);
}
}
}
.messageContent hr {
border: none;
border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
margin: 1em 0;
}
.toolGroup {
margin: 6px 0;
font-size: var(--mantine-font-size-xs);
}
.toolGroupHeader {
display: inline-flex;
align-items: center;
gap: 6px;
cursor: pointer;
user-select: none;
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
line-height: 1.4;
transition: color 120ms ease;
}
.toolGroupHeader:hover {
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
}
.toolGroupLabel {
font-weight: 500;
}
.toolGroupSteps {
margin-top: 4px;
padding-left: 14px;
display: flex;
flex-direction: column;
gap: 2px;
}
.toolStep {
font-size: var(--mantine-font-size-xs);
}
.toolStepRow {
display: inline-flex;
align-items: center;
gap: 4px;
cursor: pointer;
user-select: none;
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
line-height: 1.5;
transition: color 120ms ease;
}
.toolStepRow:hover {
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
}
.toolStepBullet {
display: inline-block;
width: 8px;
text-align: center;
opacity: 0.6;
}
.toolStepDetails {
margin-top: 4px;
margin-left: 18px;
padding: 6px 10px;
border-radius: var(--mantine-radius-sm);
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
font-size: 11px;
line-height: 1.5;
overflow-x: auto;
}
.messageActions {
display: flex;
align-items: center;
gap: 4px;
margin-top: 4px;
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
}
.processingIndicator {
display: inline-flex;
align-items: center;
gap: 6px;
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
font-size: var(--mantine-font-size-sm);
}
.processingSpinner {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.streamingCursor {
display: inline-block;
width: 2px;
height: 1em;
background: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
animation: blink 1s step-end infinite;
vertical-align: text-bottom;
margin-left: 1px;
}
@keyframes blink {
50% {
opacity: 0;
}
}
@@ -0,0 +1,147 @@
.sidebar {
height: 100%;
width: 100%;
padding: var(--mantine-spacing-md);
display: flex;
flex-direction: column;
gap: var(--mantine-spacing-xs);
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: var(--mantine-spacing-xs);
}
.title {
margin: 0;
font-weight: 600;
font-size: var(--mantine-font-size-sm);
}
.searchInput {
margin-bottom: var(--mantine-spacing-xs);
}
.chatList {
flex: 1;
overflow-y: auto;
}
.chatGroup + .chatGroup {
margin-top: var(--mantine-spacing-sm);
}
.chatGroupLabel {
margin: 0;
padding: 4px var(--mantine-spacing-xs);
font-size: var(--mantine-font-size-xs);
font-weight: 600;
color: var(--mantine-color-dimmed);
user-select: none;
}
.chatListEmpty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--mantine-spacing-xl) var(--mantine-spacing-md);
text-align: center;
gap: 4px;
user-select: none;
}
.chatListEmptyIcon {
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
margin-bottom: var(--mantine-spacing-xs);
}
.chatListEmptyTitle {
font-size: var(--mantine-font-size-sm);
font-weight: 600;
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
}
.chatListEmptyHint {
font-size: var(--mantine-font-size-xs);
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-3));
line-height: 1.4;
}
.chatItem {
display: flex;
align-items: center;
padding: 8px var(--mantine-spacing-xs);
border-radius: var(--mantine-radius-sm);
cursor: pointer;
text-decoration: none;
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
font-size: var(--mantine-font-size-sm);
user-select: none;
gap: var(--mantine-spacing-xs);
@mixin hover {
background-color: light-dark(
var(--mantine-color-gray-1),
var(--mantine-color-dark-6)
);
}
&[data-active] {
background-color: light-dark(
var(--mantine-color-gray-2),
var(--mantine-color-dark-6)
);
}
}
.chatItemTitle {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chatItemDate {
font-size: var(--mantine-font-size-xs);
color: var(--mantine-color-dimmed);
white-space: nowrap;
transition: opacity 150ms;
}
.chatItemRenameInput {
font-size: var(--mantine-font-size-sm);
padding: 0;
height: auto;
min-height: 0;
background: transparent;
color: inherit;
}
.chatItem:hover .chatItemDate,
.chatItem:focus-within .chatItemDate {
opacity: 0;
}
.chatItemActions {
position: absolute;
right: var(--mantine-spacing-xs);
opacity: 0;
transition: opacity 150ms;
}
.chatItem {
position: relative;
}
.chatItem:hover .chatItemActions,
.chatItem:focus-within .chatItemActions {
opacity: 1;
}
.chatItemActions :global(.mantine-ActionIcon-root):focus-visible {
outline: 2px solid var(--mantine-primary-color-filled);
outline-offset: 2px;
}
@@ -0,0 +1,49 @@
export type AiChat = {
id: string;
workspaceId: string;
creatorId: string;
title: string | null;
createdAt: string;
updatedAt: string;
};
export type AiChatToolCall = {
id: string;
name: string;
args: Record<string, unknown>;
result?: unknown;
};
export type AiChatMessage = {
id: string;
chatId: string;
role: 'user' | 'assistant' | 'tool';
content: string | null;
toolCalls: AiChatToolCall[] | null;
metadata: Record<string, unknown> | null;
createdAt: string;
};
export type AiChatStreamEvent =
| { type: 'chat_created'; chatId: string }
| { type: 'content'; text: string }
| { type: 'tool_call'; id: string; name: string; args: Record<string, unknown> }
| { type: 'tool_result'; id: string; result: unknown }
| { type: 'done'; messageId: string; usage?: Record<string, number> }
| { type: 'error'; message: string; code?: string; retryable?: boolean };
export type PageMention = {
id: string;
title: string;
slugId: string;
spaceSlug?: string;
icon?: string;
};
export type ChatAttachment = {
id: string;
fileName: string;
fileExt: string;
fileSize: number;
mimeType: string;
};
@@ -0,0 +1,45 @@
import type { AiChat } from "../types/ai-chat.types";
export type ChatGroup = { key: string; label: string; chats: AiChat[] };
export function groupChatsByAge(
chats: AiChat[],
t: (key: string) => string,
): ChatGroup[] {
if (chats.length === 0) return [];
const now = new Date();
const startOfToday = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
).getTime();
const startOfYesterday = startOfToday - 24 * 60 * 60 * 1000;
const startOfLast7 = startOfToday - 7 * 24 * 60 * 60 * 1000;
const startOfLast30 = startOfToday - 30 * 24 * 60 * 60 * 1000;
const buckets: Record<string, ChatGroup> = {
today: { key: "today", label: t("Today"), chats: [] },
yesterday: { key: "yesterday", label: t("Yesterday"), chats: [] },
last7: { key: "last7", label: t("Previous 7 days"), chats: [] },
last30: { key: "last30", label: t("Previous 30 days"), chats: [] },
older: { key: "older", label: t("Older"), chats: [] },
};
for (const chat of chats) {
const ts = new Date(chat.updatedAt).getTime();
if (ts >= startOfToday) buckets.today.chats.push(chat);
else if (ts >= startOfYesterday) buckets.yesterday.chats.push(chat);
else if (ts >= startOfLast7) buckets.last7.chats.push(chat);
else if (ts >= startOfLast30) buckets.last30.chats.push(chat);
else buckets.older.chats.push(chat);
}
return [
buckets.today,
buckets.yesterday,
buckets.last7,
buckets.last30,
buckets.older,
].filter((b) => b.chats.length > 0);
}
@@ -0,0 +1,113 @@
import React, { useMemo } from "react";
import { Paper, Text, Group, Stack, Loader, Box } from "@mantine/core";
import { IconSparkles, IconFileText } from "@tabler/icons-react";
import { Link } from "react-router-dom";
import { IAiSearchResponse } from "../services/ai-search-service.ts";
import { buildPageUrl } from "@/features/page/page.utils.ts";
import { markdownToHtml } from "@docmost/editor-ext";
import DOMPurify from "dompurify";
import { useTranslation } from "react-i18next";
interface AiSearchResultProps {
result?: IAiSearchResponse;
isLoading?: boolean;
streamingAnswer?: string;
streamingSources?: any[];
}
export function AiSearchResult({
result,
isLoading,
streamingAnswer = "",
streamingSources = [],
}: AiSearchResultProps) {
const { t } = useTranslation();
// Use streaming data if available, otherwise fall back to result
const answer = streamingAnswer || result?.answer || "";
const sources =
streamingSources.length > 0 ? streamingSources : result?.sources || [];
// Deduplicate sources by pageId, keeping the one with highest similarity
const deduplicatedSources = useMemo(() => {
if (!sources || sources.length === 0) return [];
const pageMap = new Map();
sources.forEach((source) => {
const existing = pageMap.get(source.pageId);
if (!existing || source.similarity > existing.similarity) {
pageMap.set(source.pageId, source);
}
});
return Array.from(pageMap.values());
}, [sources]);
if (isLoading && !answer) {
return (
<Paper p="md" radius="md" withBorder>
<Group>
<Loader size="sm" />
<Text size="sm">{t("AI is thinking...")}</Text>
</Group>
</Paper>
);
}
if (!answer && !isLoading) {
return null;
}
return (
<Stack gap="md" p="md">
<Paper p="md" radius="md" withBorder>
<Group gap="xs" mb="sm">
<IconSparkles size={20} color="var(--mantine-color-blue-6)" />
<Text fw={600} size="sm">
{t("AI Answer")}
</Text>
{isLoading && <Loader size="xs" />}
</Group>
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(markdownToHtml(answer) as string),
}}
/>
</Paper>
{deduplicatedSources.length > 0 && (
<Stack gap="xs">
<Text size="xs" fw={600} c="dimmed">
{t("Sources")}
</Text>
{deduplicatedSources.map((source) => (
<Box
key={source.pageId}
component={Link}
to={buildPageUrl(source.spaceSlug, source.slugId, source.title)}
style={{
textDecoration: "none",
color: "inherit",
display: "block",
}}
>
<Paper
p="xs"
radius="sm"
withBorder
style={{ cursor: "pointer" }}
>
<Group gap="xs">
<IconFileText size={16} />
<Text size="sm" truncate>
{source.title}
</Text>
</Group>
</Paper>
</Box>
))}
</Stack>
)}
</Stack>
);
}

Some files were not shown because too many files have changed in this diff Show More