@kernhq/module-quire 0.10.3 → 0.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kernhq/module-quire",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.4",
|
|
4
4
|
"description": "Kern Quire: collaborative documents, spaces and page trees",
|
|
5
5
|
"homepage": "https://github.com/KernAIO/module-quire#readme",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@kernhq/contracts": "^0.7.0",
|
|
56
56
|
"@kernhq/kernel": "^0.7.2",
|
|
57
|
-
"@kernhq/ui": "^0.
|
|
57
|
+
"@kernhq/ui": "^0.12.0",
|
|
58
58
|
"@tanstack/svelte-query": "^6.1.0",
|
|
59
59
|
"svelte": "^5.46.0"
|
|
60
60
|
},
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"@kernhq/contracts": "^0.7.0",
|
|
76
76
|
"@kernhq/kernel": "^0.7.2",
|
|
77
77
|
"@kernhq/tsconfig": "^0.1.0",
|
|
78
|
-
"@kernhq/ui": "^0.
|
|
78
|
+
"@kernhq/ui": "^0.12.0",
|
|
79
79
|
"@tanstack/svelte-query": "^6.1.0",
|
|
80
80
|
"@types/node": "^24.0.0",
|
|
81
81
|
"@types/pg": "^8.15.0",
|
|
@@ -73,12 +73,19 @@ const user = $derived({
|
|
|
73
73
|
the narrow schema with issue descriptions, and only a page has a reader (`renderPageDoc`) able
|
|
74
74
|
to draw all of this outside the browser.
|
|
75
75
|
-->
|
|
76
|
+
<!--
|
|
77
|
+
`label` is not decoration: the surface carries `role="textbox"`, and a textbox with no
|
|
78
|
+
accessible name is announced as nothing at all. This is the wiki's main writing surface, so
|
|
79
|
+
it was the one control in the product that most needed a name and did not have one. The page
|
|
80
|
+
title is the honest name for it; `page_body` covers an untitled page.
|
|
81
|
+
-->
|
|
76
82
|
<CollaborativeEditor
|
|
77
83
|
{url}
|
|
78
84
|
{name}
|
|
79
85
|
{user}
|
|
80
86
|
page
|
|
81
87
|
placeholder={t('editor_placeholder')}
|
|
88
|
+
label={doc.title || t('page_body')}
|
|
82
89
|
{onpeers}
|
|
83
90
|
{onstatus}
|
|
84
91
|
{commentRanges}
|
package/src/client/i18n.ts
CHANGED
|
@@ -30,7 +30,9 @@ export const en: Record<string, Message> = {
|
|
|
30
30
|
'quire.comments_empty': 'No comments yet',
|
|
31
31
|
'quire.comments_empty_desc': 'Select some text to ask about it.',
|
|
32
32
|
'quire.edited_ago': 'Edited {when}',
|
|
33
|
+
'quire.edited_ago_by': 'Edited {when} by {who}',
|
|
33
34
|
'quire.editor_mock': 'Not connected to the collaboration service',
|
|
35
|
+
'quire.page_body': 'Page body',
|
|
34
36
|
'quire.editor_mock_desc':
|
|
35
37
|
'This is the demo interface, which has no server behind it. Writing here would not be saved.',
|
|
36
38
|
'quire.editor_no_session': 'You are not signed in',
|
|
@@ -366,7 +368,9 @@ const ar: Record<string, Message> = {
|
|
|
366
368
|
'quire.comments_empty': 'لا توجد تعليقات بعد',
|
|
367
369
|
'quire.comments_empty_desc': 'حدّد نصًا لتسأل عنه.',
|
|
368
370
|
'quire.edited_ago': 'عُدّلت {when}',
|
|
371
|
+
'quire.edited_ago_by': 'عُدّلت {when} بواسطة {who}',
|
|
369
372
|
'quire.editor_mock': 'غير متصل بخدمة التعاون',
|
|
373
|
+
'quire.page_body': 'متن الصفحة',
|
|
370
374
|
'quire.editor_mock_desc': 'هذه واجهة العرض التجريبي، ولا يوجد خادم خلفها. لن يُحفظ ما تكتبه هنا.',
|
|
371
375
|
'quire.editor_no_session': 'لم تسجّل الدخول',
|
|
372
376
|
'quire.editor_no_session_desc': 'سجّل الدخول مرة أخرى للكتابة في هذه الصفحة.',
|
|
@@ -713,7 +717,9 @@ const de: Record<string, Message> = {
|
|
|
713
717
|
'quire.comments_empty': 'Noch keine Kommentare',
|
|
714
718
|
'quire.comments_empty_desc': 'Markiere Text, um dazu etwas zu fragen.',
|
|
715
719
|
'quire.edited_ago': 'Bearbeitet {when}',
|
|
720
|
+
'quire.edited_ago_by': 'Bearbeitet {when} von {who}',
|
|
716
721
|
'quire.editor_mock': 'Nicht mit dem Kollaborationsdienst verbunden',
|
|
722
|
+
'quire.page_body': 'Seiteninhalt',
|
|
717
723
|
'quire.editor_mock_desc':
|
|
718
724
|
'Dies ist die Demo-Oberfläche, hinter der kein Server steht. Was du hier schreibst, wird nicht gespeichert.',
|
|
719
725
|
'quire.editor_no_session': 'Du bist nicht angemeldet',
|
|
@@ -1052,7 +1058,9 @@ const fa: Record<string, Message> = {
|
|
|
1052
1058
|
'quire.comments_empty': 'هنوز دیدگاهی نیست',
|
|
1053
1059
|
'quire.comments_empty_desc': 'متنی را انتخاب کنید تا دربارهٔ آن بپرسید.',
|
|
1054
1060
|
'quire.edited_ago': 'ویرایش {when}',
|
|
1061
|
+
'quire.edited_ago_by': 'ویرایش {when} توسط {who}',
|
|
1055
1062
|
'quire.editor_mock': 'به سرویس همکاری وصل نیست',
|
|
1063
|
+
'quire.page_body': 'متن صفحه',
|
|
1056
1064
|
'quire.editor_mock_desc': 'این نسخهٔ نمایشی است و سروری پشت آن نیست. آنچه اینجا بنویسید ذخیره نمیشود.',
|
|
1057
1065
|
'quire.editor_no_session': 'وارد نشدهاید',
|
|
1058
1066
|
'quire.editor_no_session_desc': 'برای نوشتن در این صفحه دوباره وارد شوید.',
|
|
@@ -1381,7 +1389,9 @@ const tr: Record<string, Message> = {
|
|
|
1381
1389
|
'quire.comments_empty': 'Henüz yorum yok',
|
|
1382
1390
|
'quire.comments_empty_desc': 'Hakkında soru sormak için bir metin seçin.',
|
|
1383
1391
|
'quire.edited_ago': '{when} düzenlendi',
|
|
1392
|
+
'quire.edited_ago_by': '{who} tarafından {when} düzenlendi',
|
|
1384
1393
|
'quire.editor_mock': 'İş birliği hizmetine bağlı değil',
|
|
1394
|
+
'quire.page_body': 'Sayfa gövdesi',
|
|
1385
1395
|
'quire.editor_mock_desc': 'Bu, arkasında sunucu olmayan tanıtım arayüzü. Burada yazdıklarınız kaydedilmez.',
|
|
1386
1396
|
'quire.editor_no_session': 'Oturum açmadınız',
|
|
1387
1397
|
'quire.editor_no_session_desc': 'Bu sayfada yazmak için yeniden oturum açın.',
|
|
@@ -3,10 +3,12 @@ import {
|
|
|
3
3
|
Avatar,
|
|
4
4
|
Button,
|
|
5
5
|
type CollabPeer,
|
|
6
|
+
coreApi,
|
|
6
7
|
DropdownMenu,
|
|
7
8
|
EmptyState,
|
|
8
9
|
Icon,
|
|
9
10
|
IconButton,
|
|
11
|
+
keys,
|
|
10
12
|
navigation,
|
|
11
13
|
Page,
|
|
12
14
|
relativeTime,
|
|
@@ -18,6 +20,7 @@ import { getQuireApi } from '../api-instance.js'
|
|
|
18
20
|
import CommentsPanel from '../components/CommentsPanel.svelte'
|
|
19
21
|
import PageEditor from '../components/PageEditor.svelte'
|
|
20
22
|
import VersionHistory from '../components/VersionHistory.svelte'
|
|
23
|
+
import { type CoreApi, toPerson } from '../core-api.js'
|
|
21
24
|
import DatabaseView from '../database/DatabaseView.svelte'
|
|
22
25
|
import { t } from '../i18n.js'
|
|
23
26
|
import { canQuire } from '../permissions.js'
|
|
@@ -51,12 +54,32 @@ const workspaceSlug = $derived(navigation.workspaceSlug)
|
|
|
51
54
|
const workspace = $derived(session.workspaces.find((w) => w.slug === workspaceSlug))
|
|
52
55
|
const workspaceId = $derived(workspace?.id ?? '')
|
|
53
56
|
|
|
57
|
+
const core = coreApi<CoreApi>()
|
|
58
|
+
/*
|
|
59
|
+
* Who last edited this, by name.
|
|
60
|
+
*
|
|
61
|
+
* The byline drew `<Avatar id={doc.updatedBy} />` with no name — a "?" square with an empty
|
|
62
|
+
* accessible name — over the words "Edited 1h ago", so the one line on the page whose job is to say
|
|
63
|
+
* who touched it said everything except that. The same member list the margin and the person cells
|
|
64
|
+
* read.
|
|
65
|
+
*/
|
|
66
|
+
const membersQuery = createQuery(() => ({
|
|
67
|
+
queryKey: keys.members(workspaceId),
|
|
68
|
+
enabled: Boolean(workspaceId),
|
|
69
|
+
queryFn: () => core.workspaces.members.list({ workspaceId, limit: 200 }),
|
|
70
|
+
}))
|
|
71
|
+
|
|
54
72
|
const query = createQuery(() => ({
|
|
55
73
|
queryKey: quireKeys.page(workspaceId, pageId),
|
|
56
74
|
enabled: Boolean(workspaceId && pageId),
|
|
57
75
|
queryFn: () => api.pages.get({ workspaceId, pageId }),
|
|
58
76
|
}))
|
|
59
77
|
const doc = $derived(query.data ?? null)
|
|
78
|
+
const editor = $derived.by(() => {
|
|
79
|
+
const id = doc?.updatedBy ?? null
|
|
80
|
+
if (!id) return null
|
|
81
|
+
return (membersQuery.data?.items ?? []).map(toPerson).find((p) => p.id === id) ?? null
|
|
82
|
+
})
|
|
60
83
|
|
|
61
84
|
const editable = $derived(canQuire('pageEdit'))
|
|
62
85
|
/** A database page draws a view where the prose would be, and a table needs the whole width. */
|
|
@@ -278,8 +301,12 @@ async function trash() {
|
|
|
278
301
|
</div>
|
|
279
302
|
|
|
280
303
|
<div class="byline">
|
|
281
|
-
<Avatar id={doc.updatedBy} size={24} />
|
|
282
|
-
<span>
|
|
304
|
+
<Avatar id={doc.updatedBy} name={editor?.name} src={editor?.avatarUrl ?? undefined} size={24} />
|
|
305
|
+
<span>
|
|
306
|
+
{editor
|
|
307
|
+
? t('edited_ago_by', { when: relativeTime(doc.updatedAt), who: editor.name })
|
|
308
|
+
: t('edited_ago', { when: relativeTime(doc.updatedAt) })}
|
|
309
|
+
</span>
|
|
283
310
|
{#if doc.kind === 'live'}
|
|
284
311
|
<span class="chip"><Icon name="square-pen" size={12} /> {t('kind_live')}</span>
|
|
285
312
|
{/if}
|
|
@@ -35,6 +35,27 @@ const spacesQuery = createQuery(() => ({
|
|
|
35
35
|
}))
|
|
36
36
|
const space = $derived((spacesQuery.data ?? []).find((s) => s.key === spaceKey) ?? null)
|
|
37
37
|
|
|
38
|
+
/*
|
|
39
|
+
* A space without a home page is not an empty space.
|
|
40
|
+
*
|
|
41
|
+
* This screen used to branch on `homepageId` alone and tell anyone whose space had no home page
|
|
42
|
+
* that it had no pages at all — while the sidebar beside it listed them. Opening a space means
|
|
43
|
+
* "show me this space", so with no home page chosen it opens the first top-level page, and the
|
|
44
|
+
* empty state is kept for the one case that is actually empty.
|
|
45
|
+
*/
|
|
46
|
+
const treeQuery = createQuery(() => ({
|
|
47
|
+
queryKey: quireKeys.tree(workspaceId, space?.id ?? ''),
|
|
48
|
+
enabled: Boolean(workspaceId && space && !space.homepageId),
|
|
49
|
+
queryFn: () => api.pages.tree({ workspaceId, spaceId: space?.id ?? '', includeArchived: false }),
|
|
50
|
+
}))
|
|
51
|
+
|
|
52
|
+
/** The first top-level page by position — the same order the sidebar draws. */
|
|
53
|
+
const firstPageId = $derived(
|
|
54
|
+
(treeQuery.data ?? [])
|
|
55
|
+
.filter((p) => p.parentId === null && !p.archivedAt)
|
|
56
|
+
.sort((a, b) => (a.position < b.position ? -1 : a.position > b.position ? 1 : 0))[0]?.id ?? null,
|
|
57
|
+
)
|
|
58
|
+
|
|
38
59
|
let creating = $state(false)
|
|
39
60
|
async function createFirst() {
|
|
40
61
|
if (!space || creating) return
|
|
@@ -66,6 +87,10 @@ async function createFirst() {
|
|
|
66
87
|
</div>
|
|
67
88
|
{:else if space.homepageId}
|
|
68
89
|
<PageView {spaceKey} pageId={space.homepageId} />
|
|
90
|
+
{:else if treeQuery.isLoading}
|
|
91
|
+
<div class="pad"><Skeleton height="36px" /></div>
|
|
92
|
+
{:else if firstPageId}
|
|
93
|
+
<PageView {spaceKey} pageId={firstPageId} />
|
|
69
94
|
{:else}
|
|
70
95
|
<div class="pad">
|
|
71
96
|
<EmptyState icon="file-text" title={t('space_empty')} description={t('space_empty_desc')}>
|