@kernhq/module-quire 0.13.1 → 0.15.0

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.
Files changed (123) hide show
  1. package/dist/contract/models.d.ts +394 -0
  2. package/dist/contract/models.d.ts.map +1 -1
  3. package/dist/contract/models.js +284 -0
  4. package/dist/contract/models.js.map +1 -1
  5. package/dist/contract/permissions.d.ts +14 -0
  6. package/dist/contract/permissions.d.ts.map +1 -1
  7. package/dist/contract/permissions.js +114 -0
  8. package/dist/contract/permissions.js.map +1 -1
  9. package/dist/contract/properties.d.ts +7 -7
  10. package/dist/contract/router.d.ts +1124 -96
  11. package/dist/contract/router.d.ts.map +1 -1
  12. package/dist/contract/router.js +343 -1
  13. package/dist/contract/router.js.map +1 -1
  14. package/dist/server/_impl.d.ts +983 -1168
  15. package/dist/server/_impl.d.ts.map +1 -1
  16. package/dist/server/_impl.js +308 -0
  17. package/dist/server/_impl.js.map +1 -1
  18. package/dist/server/export/html.d.ts +67 -0
  19. package/dist/server/export/html.d.ts.map +1 -0
  20. package/dist/server/export/html.js +206 -0
  21. package/dist/server/export/html.js.map +1 -0
  22. package/dist/server/export/markdown.d.ts +51 -0
  23. package/dist/server/export/markdown.d.ts.map +1 -0
  24. package/dist/server/export/markdown.js +351 -0
  25. package/dist/server/export/markdown.js.map +1 -0
  26. package/dist/server/export/pdf.d.ts +20 -0
  27. package/dist/server/export/pdf.d.ts.map +1 -0
  28. package/dist/server/export/pdf.js +91 -0
  29. package/dist/server/export/pdf.js.map +1 -0
  30. package/dist/server/export/zip.d.ts +31 -0
  31. package/dist/server/export/zip.d.ts.map +1 -0
  32. package/dist/server/export/zip.js +158 -0
  33. package/dist/server/export/zip.js.map +1 -0
  34. package/dist/server/import/csv.d.ts +77 -0
  35. package/dist/server/import/csv.d.ts.map +1 -0
  36. package/dist/server/import/csv.js +263 -0
  37. package/dist/server/import/csv.js.map +1 -0
  38. package/dist/server/import/html.d.ts +52 -0
  39. package/dist/server/import/html.d.ts.map +1 -0
  40. package/dist/server/import/html.js +472 -0
  41. package/dist/server/import/html.js.map +1 -0
  42. package/dist/server/import/markdown.d.ts +63 -0
  43. package/dist/server/import/markdown.d.ts.map +1 -0
  44. package/dist/server/import/markdown.js +692 -0
  45. package/dist/server/import/markdown.js.map +1 -0
  46. package/dist/server/import/plan.d.ts +70 -0
  47. package/dist/server/import/plan.d.ts.map +1 -0
  48. package/dist/server/import/plan.js +761 -0
  49. package/dist/server/import/plan.js.map +1 -0
  50. package/dist/server/import/ydoc.d.ts +35 -0
  51. package/dist/server/import/ydoc.d.ts.map +1 -0
  52. package/dist/server/import/ydoc.js +91 -0
  53. package/dist/server/import/ydoc.js.map +1 -0
  54. package/dist/server/import/zip.d.ts +63 -0
  55. package/dist/server/import/zip.d.ts.map +1 -0
  56. package/dist/server/import/zip.js +308 -0
  57. package/dist/server/import/zip.js.map +1 -0
  58. package/dist/server/index.d.ts.map +1 -1
  59. package/dist/server/index.js +3 -1
  60. package/dist/server/index.js.map +1 -1
  61. package/dist/server/render.d.ts +158 -0
  62. package/dist/server/render.d.ts.map +1 -1
  63. package/dist/server/render.js +240 -0
  64. package/dist/server/render.js.map +1 -1
  65. package/dist/server/schema.d.ts +704 -1
  66. package/dist/server/schema.d.ts.map +1 -1
  67. package/dist/server/schema.js +254 -1
  68. package/dist/server/schema.js.map +1 -1
  69. package/dist/server/services/databases.d.ts +10 -10
  70. package/dist/server/services/export.d.ts +176 -0
  71. package/dist/server/services/export.d.ts.map +1 -0
  72. package/dist/server/services/export.js +822 -0
  73. package/dist/server/services/export.js.map +1 -0
  74. package/dist/server/services/import.d.ts +109 -0
  75. package/dist/server/services/import.d.ts.map +1 -0
  76. package/dist/server/services/import.js +570 -0
  77. package/dist/server/services/import.js.map +1 -0
  78. package/dist/server/services/index.d.ts +32 -1
  79. package/dist/server/services/index.d.ts.map +1 -1
  80. package/dist/server/services/index.js +87 -4
  81. package/dist/server/services/index.js.map +1 -1
  82. package/dist/server/services/macros.d.ts +83 -0
  83. package/dist/server/services/macros.d.ts.map +1 -0
  84. package/dist/server/services/macros.js +488 -0
  85. package/dist/server/services/macros.js.map +1 -0
  86. package/dist/server/services/publications.d.ts +2 -1
  87. package/dist/server/services/publications.d.ts.map +1 -1
  88. package/dist/server/services/publications.js +43 -4
  89. package/dist/server/services/publications.js.map +1 -1
  90. package/dist/server/services/templates.d.ts +135 -0
  91. package/dist/server/services/templates.d.ts.map +1 -0
  92. package/dist/server/services/templates.js +897 -0
  93. package/dist/server/services/templates.js.map +1 -0
  94. package/dist/server/services/versions.d.ts +13 -1
  95. package/dist/server/services/versions.d.ts.map +1 -1
  96. package/dist/server/services/versions.js +3 -1
  97. package/dist/server/services/versions.js.map +1 -1
  98. package/migrations/0010_transfers.sql +154 -0
  99. package/migrations/0011_templates.sql +157 -0
  100. package/migrations/meta/_journal.json +14 -0
  101. package/package.json +5 -5
  102. package/src/client/components/ExportDialog.svelte +685 -0
  103. package/src/client/components/ImportDialog.svelte +702 -0
  104. package/src/client/components/ImportReport.svelte +310 -0
  105. package/src/client/components/NewSpaceDialog.svelte +77 -8
  106. package/src/client/components/PageEditor.svelte +80 -0
  107. package/src/client/components/PagePicker.svelte +264 -0
  108. package/src/client/components/SaveAsTemplateDialog.svelte +502 -0
  109. package/src/client/components/SidebarSpaces.svelte +112 -1
  110. package/src/client/components/TemplatePicker.svelte +437 -0
  111. package/src/client/i18n.ts +941 -0
  112. package/src/client/index.ts +51 -0
  113. package/src/client/mock.ts +592 -0
  114. package/src/client/module.ts +36 -0
  115. package/src/client/pages/PageView.svelte +86 -0
  116. package/src/client/pages/SpacePage.svelte +20 -4
  117. package/src/client/pages/TransfersPage.svelte +570 -0
  118. package/src/client/permissions.ts +11 -0
  119. package/src/client/query.ts +40 -0
  120. package/src/client/transfers.ts +142 -0
  121. package/src/contract/models.ts +343 -0
  122. package/src/contract/permissions.ts +117 -0
  123. package/src/contract/router.ts +375 -0
@@ -49,6 +49,27 @@ export const quireClientModule = defineClientModule({
49
49
  component: () => import('./pages/SpacePage.svelte'),
50
50
  permission: QUIRE_PERMISSIONS.spaceView,
51
51
  },
52
+ /**
53
+ * Work on its way out and on its way in.
54
+ *
55
+ * Two literal segments, so it beats `/quire/:space` however the two are ordered — the shell
56
+ * scores a match as `literals * 1000 + segments`. Its place in this list is for a reader.
57
+ *
58
+ * **No `permission`, on purpose**, and this is the one route here without one. The screen has
59
+ * two halves behind two independent keys — `quire.page.export` is a member by default,
60
+ * `quire.page.import` is owner-and-admin — so a single gate would either hide it from somebody
61
+ * who may import or show it to somebody who may do neither; each half asks its own key instead.
62
+ * And a gate here would misbehave rather than 404: a route the shell refuses falls through to
63
+ * `/quire/:space`, which would tell the person "Space not found" about a space called
64
+ * *transfers*.
65
+ */
66
+ {
67
+ path: '/quire/transfers',
68
+ component: () => import('./pages/TransfersPage.svelte'),
69
+ get title() {
70
+ return t('transfers')
71
+ },
72
+ },
52
73
  /**
53
74
  * Declared before `/quire/:space/:page` for a reader's benefit only — the shell resolves by
54
75
  * specificity, not by order, so the two literal segments here beat the one in the page route
@@ -93,6 +114,21 @@ export const quireClientModule = defineClientModule({
93
114
  permission: QUIRE_PERMISSIONS.spaceManage,
94
115
  run: (ctx) => ctx.navigate('/quire?new=1'),
95
116
  },
117
+ /*
118
+ * The palette is the second way to a running job, and the one that works from anywhere.
119
+ * Gated on `page.export` rather than left open: the command list is a menu, and an entry that
120
+ * leads to a screen with both halves empty is an entry that teaches people to ignore the list.
121
+ * Somebody who may import and not export reaches the screen from the import dialog instead.
122
+ */
123
+ {
124
+ id: 'quire.transfers',
125
+ get label() {
126
+ return t('cmd_transfers')
127
+ },
128
+ icon: 'package',
129
+ permission: QUIRE_PERMISSIONS.pageExport,
130
+ run: (ctx) => ctx.navigate('/quire/transfers'),
131
+ },
96
132
  ],
97
133
 
98
134
  sidebar: [
@@ -22,10 +22,12 @@ import { untrack } from 'svelte'
22
22
  import { getQuireApi } from '../api-instance.js'
23
23
  import CommentsPanel from '../components/CommentsPanel.svelte'
24
24
  import ConfirmDialog from '../components/ConfirmDialog.svelte'
25
+ import ExportDialog from '../components/ExportDialog.svelte'
25
26
  import FavoriteStar from '../components/FavoriteStar.svelte'
26
27
  import PageEditor from '../components/PageEditor.svelte'
27
28
  import PageLabels from '../components/PageLabels.svelte'
28
29
  import PublishDialog from '../components/PublishDialog.svelte'
30
+ import SaveAsTemplateDialog from '../components/SaveAsTemplateDialog.svelte'
29
31
  import VersionHistory from '../components/VersionHistory.svelte'
30
32
  import { type CoreApi, toPerson } from '../core-api.js'
31
33
  import DatabaseView from '../database/DatabaseView.svelte'
@@ -77,6 +79,19 @@ const membersQuery = createQuery(() => ({
77
79
  queryFn: () => core.workspaces.members.list({ workspaceId, limit: 200 }),
78
80
  }))
79
81
 
82
+ /*
83
+ * The space by name, for the one sentence that needs it: "offered only in Handbook".
84
+ *
85
+ * The same query key the sidebar already holds, so this is a cache read rather than a request —
86
+ * and the key is what the URL carries, which is not a thing to show somebody in a sentence.
87
+ */
88
+ const spacesQuery = createQuery(() => ({
89
+ queryKey: quireKeys.spaces(workspaceId),
90
+ enabled: Boolean(workspaceId),
91
+ queryFn: () => api.spaces.list({ workspaceId, includeArchived: false }),
92
+ }))
93
+ const spaceName = $derived((spacesQuery.data ?? []).find((space) => space.key === spaceKey)?.name ?? spaceKey)
94
+
80
95
  const query = createQuery(() => ({
81
96
  queryKey: quireKeys.page(workspaceId, pageId),
82
97
  enabled: Boolean(workspaceId && pageId),
@@ -266,6 +281,23 @@ async function revert() {
266
281
  let shareOpen = $state(false)
267
282
  const canPublish = $derived(canQuire('pagePublish'))
268
283
 
284
+ /**
285
+ * Taking this page out as a file.
286
+ *
287
+ * Nothing is loaded for it here: the dialog is the only thing that knows what an export needs, and
288
+ * it asks for the space, the tree and any job already running the moment it opens. This screen's
289
+ * whole part in the feature is a flag and a menu entry, which is what an entry point should be.
290
+ */
291
+ let exportOpen = $state(false)
292
+
293
+ /**
294
+ * Saving this page as something to write with again.
295
+ *
296
+ * A flag and a menu entry, exactly like the export above: what a template needs to know is the
297
+ * dialog's business, and it reads the starters and the space list itself when it opens.
298
+ */
299
+ let templateOpen = $state(false)
300
+
269
301
  const publicationsQuery = createQuery(() => ({
270
302
  queryKey: quireKeys.publications(workspaceId, doc?.spaceId ?? ''),
271
303
  enabled: canPublish && Boolean(workspaceId && doc?.spaceId),
@@ -592,6 +624,42 @@ async function undoTrash(workspace: string, id: string, spaceId: string, title:
592
624
  },
593
625
  ]
594
626
  : []),
627
+ /*
628
+ * Every kind of page, not only a `page`. A live doc and a database both render through
629
+ * `renderPageDoc` like anything else — a database exports as the table it is — so the
630
+ * restriction that belongs on **Share to the web** does not belong here.
631
+ *
632
+ * `page.export` rather than `page.view`, matching what `exports.start` asks: reading a
633
+ * handbook a page at a time and walking out with the whole thing in a zip are different
634
+ * acts, and an administrator is allowed to think so.
635
+ */
636
+ ...(canQuire('pageExport')
637
+ ? [
638
+ {
639
+ id: 'export',
640
+ label: t('export_menu'),
641
+ icon: 'download',
642
+ onSelect: () => (exportOpen = true),
643
+ },
644
+ ]
645
+ : []),
646
+ /*
647
+ * `space.manage`, not `page.edit`, matching what `templates.createFromPage` asks — and the
648
+ * asymmetry is the point. A template changes what everybody in the space is offered the
649
+ * next time they make a page, which is the space's furniture rather than this page's
650
+ * content, exactly as a label is. Somebody who may write here is not thereby somebody who
651
+ * may add to everybody else's picker.
652
+ */
653
+ ...(canQuire('spaceManage')
654
+ ? [
655
+ {
656
+ id: 'save-template',
657
+ label: t('template_menu'),
658
+ icon: 'clipboard-list',
659
+ onSelect: () => (templateOpen = true),
660
+ },
661
+ ]
662
+ : []),
595
663
  {
596
664
  id: 'watch',
597
665
  label: watching ? t('watch_stop') : t('watch'),
@@ -757,6 +825,24 @@ async function undoTrash(workspace: string, id: string, spaceId: string, title:
757
825
 
758
826
  <PublishDialog bind:open={shareOpen} {workspaceId} spaceId={doc.spaceId} page={doc} />
759
827
 
828
+ <ExportDialog
829
+ bind:open={exportOpen}
830
+ {workspaceId}
831
+ spaceId={doc.spaceId}
832
+ page={{ id: doc.id, title: doc.title }}
833
+ />
834
+
835
+ {#if canQuire('spaceManage')}
836
+ <SaveAsTemplateDialog
837
+ bind:open={templateOpen}
838
+ {workspaceId}
839
+ spaceId={doc.spaceId}
840
+ {spaceName}
841
+ pageId={doc.id}
842
+ pageTitle={doc.title}
843
+ />
844
+ {/if}
845
+
760
846
  <!--
761
847
  The body says nothing about numbers until it knows them. Naming a count before the tree has
762
848
  loaded would be the same silent lie in a smaller size — "it goes to the trash" for a page that
@@ -2,6 +2,7 @@
2
2
  import { Button, EmptyState, navigation, Skeleton, session } from '@kernhq/ui'
3
3
  import { createQuery } from '@tanstack/svelte-query'
4
4
  import { getQuireApi } from '../api-instance.js'
5
+ import TemplatePicker from '../components/TemplatePicker.svelte'
5
6
  import { t } from '../i18n.js'
6
7
  import { canQuire } from '../permissions.js'
7
8
  import { quireKeys } from '../query.js'
@@ -57,6 +58,16 @@ const firstPageId = $derived(
57
58
  )
58
59
 
59
60
  let creating = $state(false)
61
+ /**
62
+ * The first page of an empty space is the one page most worth offering a template for — a handbook
63
+ * that starts as a how-to is a handbook somebody kept writing. The picker still opens on **Blank
64
+ * page**, so nothing about starting from nothing got slower.
65
+ */
66
+ let pickerOpen = $state(false)
67
+
68
+ const open = (pageId: string) =>
69
+ void navigation.go(`/${workspaceSlug}/quire/${encodeURIComponent(spaceKey)}/${encodeURIComponent(pageId)}`)
70
+
60
71
  async function createFirst() {
61
72
  if (!space || creating) return
62
73
  creating = true
@@ -70,9 +81,7 @@ async function createFirst() {
70
81
  icon: null,
71
82
  afterId: null,
72
83
  })
73
- void navigation.go(
74
- `/${workspaceSlug}/quire/${encodeURIComponent(spaceKey)}/${encodeURIComponent(created.id)}`,
75
- )
84
+ open(created.id)
76
85
  } finally {
77
86
  creating = false
78
87
  }
@@ -96,11 +105,18 @@ async function createFirst() {
96
105
  <EmptyState icon="file-text" title={t('space_empty')} description={t('space_empty_desc')}>
97
106
  {#snippet actions()}
98
107
  {#if canQuire('pageCreate')}
99
- <Button disabled={creating} onclick={createFirst}>{t('new_page')}</Button>
108
+ <Button aria-busy={creating} onclick={() => (pickerOpen = true)}>{t('new_page')}</Button>
100
109
  {/if}
101
110
  {/snippet}
102
111
  </EmptyState>
103
112
  </div>
113
+ <TemplatePicker
114
+ bind:open={pickerOpen}
115
+ {workspaceId}
116
+ spaceId={space.id}
117
+ onBlank={() => void createFirst()}
118
+ onMade={(result) => result.pageId && open(result.pageId)}
119
+ />
104
120
  {/if}
105
121
 
106
122
  <style>