@abraca/nuxt 2.11.0 → 2.14.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 (93) hide show
  1. package/dist/module.d.mts +15 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +9 -0
  4. package/dist/runtime/components/ACodeEditor.vue +123 -22
  5. package/dist/runtime/components/ADocPickerModal.d.vue.ts +31 -0
  6. package/dist/runtime/components/ADocPickerModal.vue +191 -0
  7. package/dist/runtime/components/ADocPickerModal.vue.d.ts +31 -0
  8. package/dist/runtime/components/ADocViewToggle.d.vue.ts +40 -0
  9. package/dist/runtime/components/ADocViewToggle.vue +234 -0
  10. package/dist/runtime/components/ADocViewToggle.vue.d.ts +40 -0
  11. package/dist/runtime/components/ADocumentTree.vue +66 -1
  12. package/dist/runtime/components/AEditor.d.vue.ts +17 -10
  13. package/dist/runtime/components/AEditor.vue +403 -167
  14. package/dist/runtime/components/AEditor.vue.d.ts +17 -10
  15. package/dist/runtime/components/ANodePanel.d.vue.ts +9 -1
  16. package/dist/runtime/components/ANodePanel.vue +553 -481
  17. package/dist/runtime/components/ANodePanel.vue.d.ts +9 -1
  18. package/dist/runtime/components/ATagsEditor.d.vue.ts +19 -0
  19. package/dist/runtime/components/ATagsEditor.vue +60 -0
  20. package/dist/runtime/components/ATagsEditor.vue.d.ts +19 -0
  21. package/dist/runtime/components/aware/AMedia.d.vue.ts +1 -1
  22. package/dist/runtime/components/aware/AMedia.vue.d.ts +1 -1
  23. package/dist/runtime/components/chat/AChatInput.d.vue.ts +11 -6
  24. package/dist/runtime/components/chat/AChatInput.vue +33 -2
  25. package/dist/runtime/components/chat/AChatInput.vue.d.ts +11 -6
  26. package/dist/runtime/components/chat/AChatList.d.vue.ts +12 -0
  27. package/dist/runtime/components/chat/AChatList.vue +76 -32
  28. package/dist/runtime/components/chat/AChatList.vue.d.ts +12 -0
  29. package/dist/runtime/components/chat/AChatMessages.d.vue.ts +4 -0
  30. package/dist/runtime/components/chat/AChatMessages.vue +57 -4
  31. package/dist/runtime/components/chat/AChatMessages.vue.d.ts +4 -0
  32. package/dist/runtime/components/chat/AChatPanel.d.vue.ts +6 -2
  33. package/dist/runtime/components/chat/AChatPanel.vue +17 -1
  34. package/dist/runtime/components/chat/AChatPanel.vue.d.ts +6 -2
  35. package/dist/runtime/components/chat/ANodeChatPanel.vue +1 -1
  36. package/dist/runtime/components/docs/ADocsSearch.d.vue.ts +1 -1
  37. package/dist/runtime/components/docs/ADocsSearch.vue.d.ts +1 -1
  38. package/dist/runtime/components/editor/ADocSuggestMenu.d.vue.ts +7 -0
  39. package/dist/runtime/components/editor/ADocSuggestMenu.vue +68 -0
  40. package/dist/runtime/components/editor/ADocSuggestMenu.vue.d.ts +7 -0
  41. package/dist/runtime/components/renderers/AChartRenderer.client.d.vue.ts +17 -0
  42. package/dist/runtime/components/renderers/AChartRenderer.client.vue +622 -0
  43. package/dist/runtime/components/renderers/AChartRenderer.client.vue.d.ts +17 -0
  44. package/dist/runtime/components/renderers/AGraphRenderer.vue +64 -15
  45. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.d.vue.ts +2 -2
  46. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.vue.d.ts +2 -2
  47. package/dist/runtime/components/renderers/media/MediaTransportBar.d.vue.ts +2 -2
  48. package/dist/runtime/components/renderers/media/MediaTransportBar.vue.d.ts +2 -2
  49. package/dist/runtime/components/renderers/sheets/ASheetsGrid.d.vue.ts +2 -2
  50. package/dist/runtime/components/renderers/sheets/ASheetsGrid.vue.d.ts +2 -2
  51. package/dist/runtime/components/settings/ASettingsAppearancePanel.d.vue.ts +3 -0
  52. package/dist/runtime/components/settings/ASettingsAppearancePanel.vue +67 -0
  53. package/dist/runtime/components/settings/ASettingsAppearancePanel.vue.d.ts +3 -0
  54. package/dist/runtime/components/settings/ASettingsGroup.d.vue.ts +24 -0
  55. package/dist/runtime/components/settings/ASettingsGroup.vue +31 -0
  56. package/dist/runtime/components/settings/ASettingsGroup.vue.d.ts +24 -0
  57. package/dist/runtime/components/settings/ASettingsModal.vue +84 -53
  58. package/dist/runtime/components/settings/ASettingsPlaceholder.d.vue.ts +20 -0
  59. package/dist/runtime/components/settings/ASettingsPlaceholder.vue +32 -0
  60. package/dist/runtime/components/settings/ASettingsPlaceholder.vue.d.ts +20 -0
  61. package/dist/runtime/components/settings/ASettingsRow.d.vue.ts +34 -0
  62. package/dist/runtime/components/settings/ASettingsRow.vue +34 -0
  63. package/dist/runtime/components/settings/ASettingsRow.vue.d.ts +34 -0
  64. package/dist/runtime/components/settings/sections.d.ts +37 -0
  65. package/dist/runtime/components/settings/sections.js +45 -0
  66. package/dist/runtime/components/shell/AUserMenu.d.vue.ts +2 -2
  67. package/dist/runtime/components/shell/AUserMenu.vue.d.ts +2 -2
  68. package/dist/runtime/components/shell/AUserProfilePopover.d.vue.ts +1 -1
  69. package/dist/runtime/components/shell/AUserProfilePopover.vue.d.ts +1 -1
  70. package/dist/runtime/composables/useChat.d.ts +22 -1
  71. package/dist/runtime/composables/useChat.js +79 -8
  72. package/dist/runtime/composables/useDocLinkPick.d.ts +9 -8
  73. package/dist/runtime/composables/useDocLinkPick.js +7 -18
  74. package/dist/runtime/composables/useDocSuggest.d.ts +34 -0
  75. package/dist/runtime/composables/useDocSuggest.js +56 -0
  76. package/dist/runtime/composables/useNodeContextMenu.d.ts +4 -0
  77. package/dist/runtime/composables/useNodeContextMenu.js +18 -0
  78. package/dist/runtime/composables/useSettingsModal.d.ts +1 -1
  79. package/dist/runtime/extensions/doc-link-drop.js +2 -2
  80. package/dist/runtime/extensions/doc-suggest.d.ts +28 -0
  81. package/dist/runtime/extensions/doc-suggest.js +85 -0
  82. package/dist/runtime/locale.d.ts +8 -0
  83. package/dist/runtime/locale.js +9 -1
  84. package/dist/runtime/utils/chatContent.d.ts +20 -2
  85. package/dist/runtime/utils/chatContent.js +20 -1
  86. package/dist/runtime/utils/codeHighlightStyle.d.ts +15 -0
  87. package/dist/runtime/utils/codeHighlightStyle.js +34 -0
  88. package/dist/runtime/utils/docTypes.js +43 -0
  89. package/dist/runtime/utils/loadCodeMirror.d.ts +1 -0
  90. package/dist/runtime/utils/loadCodeMirror.js +6 -3
  91. package/dist/runtime/utils/titleSync.d.ts +130 -0
  92. package/dist/runtime/utils/titleSync.js +53 -0
  93. package/package.json +12 -1
@@ -1,20 +1,21 @@
1
1
  <script setup>
2
- import { ref, computed, watch } from "vue";
2
+ import { ref, computed, watch, defineAsyncComponent } from "vue";
3
3
  import { useAbracadabra } from "../composables/useAbracadabra";
4
4
  import { useSyncedMap } from "../composables/useYDoc";
5
- import { navigateTo } from "#imports";
5
+ import { navigateTo, useRuntimeConfig } from "#imports";
6
6
  import { useChildTree } from "../composables/useChildTree";
7
7
  import { useAbraLocale } from "../composables/useAbraLocale";
8
+ import { useDocSlugs } from "../composables/useDocSlugs";
8
9
  import { useResizableWidth } from "../composables/useResizableWidth";
9
- import ANodePanelHeader from "./ANodePanelHeader.vue";
10
- import { defineAsyncComponent } from "vue";
11
- import { useRuntimeConfig } from "#imports";
10
+ import AConnectionBadge from "./AConnectionBadge.vue";
11
+ import AEditorUndoButton from "./editor/AEditorUndoButton.vue";
12
+ import AEditorRedoButton from "./editor/AEditorRedoButton.vue";
12
13
  import { useChat } from "../composables/useChat";
14
+ import { META_FIELD_DEFINITIONS } from "../utils/metaFieldDefinitions";
15
+ import { resolveDocType } from "../utils/docTypes";
13
16
  const ANodeChatPanel = defineAsyncComponent(() => import("./chat/ANodeChatPanel.vue"));
14
17
  const ANodeSettingsPanel = defineAsyncComponent(() => import("./ANodeSettingsPanel.vue"));
15
18
  const ADocPanelServerSettings = defineAsyncComponent(() => import("./shell/ADocPanelServerSettings.vue"));
16
- import { META_FIELD_DEFINITIONS } from "../utils/metaFieldDefinitions";
17
- import { resolveDocType } from "../utils/docTypes";
18
19
  const props = defineProps({
19
20
  nodeId: { type: [String, null], required: true },
20
21
  nodeLabel: { type: String, required: false },
@@ -22,7 +23,8 @@ const props = defineProps({
22
23
  docType: { type: String, required: false },
23
24
  labels: { type: Object, required: false },
24
25
  initialTab: { type: String, required: false, default: "editor" },
25
- tabs: { type: Array, required: false }
26
+ tabs: { type: Array, required: false },
27
+ docHref: { type: Function, required: false }
26
28
  });
27
29
  const emit = defineEmits(["close"]);
28
30
  const open = computed({
@@ -43,7 +45,12 @@ const entry = computed(() => {
43
45
  return treeMap.data[props.nodeId] ?? null;
44
46
  });
45
47
  const meta = computed(() => entry.value?.meta ?? {});
48
+ const resolvedLabel = computed(() => entry.value?.label || props.nodeLabel || "");
46
49
  const currentType = computed(() => entry.value?.type ?? "doc");
50
+ const { getDocUrl } = useDocSlugs();
51
+ function hrefFor(id) {
52
+ return props.docHref ? props.docHref(id) : getDocUrl(id);
53
+ }
47
54
  function patchMeta(patch) {
48
55
  if (!props.nodeId) return;
49
56
  const e = treeMap.data[props.nodeId];
@@ -61,13 +68,29 @@ function patchType(type) {
61
68
  if (e) treeMap.set(props.nodeId, { ...e, type, updatedAt: Date.now() });
62
69
  }
63
70
  const activeTab = ref(props.initialTab);
71
+ const userPickedTab = ref(false);
64
72
  watch(() => props.nodeId, (id) => {
65
- if (id) activeTab.value = props.initialTab;
73
+ if (id) {
74
+ userPickedTab.value = false;
75
+ activeTab.value = props.initialTab;
76
+ }
66
77
  });
78
+ function setTab(tab) {
79
+ userPickedTab.value = true;
80
+ activeTab.value = tab;
81
+ }
67
82
  const docTypeDef = computed(() => resolveDocType(currentType.value, registry));
68
83
  const usesPageRenderer = computed(
69
84
  () => docTypeDef.value.key !== "doc" && !!docTypeDef.value.component
70
85
  );
86
+ watch([usesPageRenderer, () => props.nodeId], () => {
87
+ if (userPickedTab.value || props.initialTab !== "editor") return;
88
+ activeTab.value = usesPageRenderer.value ? "pageType" : "editor";
89
+ }, { immediate: true });
90
+ const CANONICAL_TABS = ["pageType", "editor", "chat", "settings"];
91
+ const toggleValue = computed(
92
+ () => CANONICAL_TABS.includes(activeTab.value) ? activeTab.value : null
93
+ );
71
94
  const editorRef = ref(null);
72
95
  const tiptapEditor = computed(() => editorRef.value?.editor ?? null);
73
96
  const resize = useResizableWidth({
@@ -183,558 +206,607 @@ const addFieldMenuItems = computed(
183
206
  <USlideover
184
207
  v-model:open="open"
185
208
  side="right"
186
- :title="nodeLabel || 'Document'"
209
+ :title="resolvedLabel || 'Document'"
210
+ :close="false"
187
211
  :content="{ style: resize.style.value }"
188
- :ui="{ body: 'p-0' }"
189
212
  @update:open="(v) => !v && emit('close')"
190
213
  >
191
- <template #header>
192
- <!-- Default header uses <ANodePanelHeader>. Consumers can replace the
193
- whole header via the #header slot, or replace individual pieces
194
- (presence, actions-right, etc.) by importing <ANodePanelHeader>
195
- and supplying its own slots. -->
196
- <slot
197
- name="header"
198
- :node-id="nodeId"
199
- :node-label="nodeLabel"
200
- :editor="tiptapEditor"
201
- :meta="meta"
202
- :doc-type-def="docTypeDef"
203
- >
204
- <ANodePanelHeader
214
+ <!-- We take full control of the panel chrome via #content (rather than
215
+ USlideover's default header/body slots) so the layout matches cou-sh's
216
+ DocSlideover: a tight 40px control bar with icon-only tabs, then a
217
+ flush content area. -->
218
+ <template #content>
219
+ <div class="flex flex-col h-full relative">
220
+ <!-- Resize handle: left edge of slideover, desktop only -->
221
+ <div
222
+ class="absolute left-0 top-0 bottom-0 w-1.5 z-10 cursor-col-resize hidden sm:block"
223
+ :class="resize.isResizing.value ? 'bg-(--ui-primary)/20' : 'hover:bg-(--ui-primary)/10'"
224
+ @mousedown.stop.prevent="resize.onMouseDown"
225
+ @touchstart.stop.prevent="resize.onTouchStart"
226
+ @dblclick="resize.onDoubleClick"
227
+ />
228
+
229
+ <!-- Header bar — close + expand on the left, icon-only tab controls on
230
+ the right (cou-sh DocSlideover parity). Consumers can replace the
231
+ whole bar via the #header slot. -->
232
+ <slot
233
+ name="header"
205
234
  :node-id="nodeId"
206
- :node-label="nodeLabel"
207
- :icon="meta.icon ?? docTypeDef.icon"
208
- :icon-color="meta.color"
209
- :provider="childProvider"
210
- :editor="activeTab === 'editor' && !usesPageRenderer ? tiptapEditor : null"
211
- @expand="navigateTo(`/app/${nodeId}`)"
235
+ :node-label="resolvedLabel"
236
+ :editor="tiptapEditor"
237
+ :meta="meta"
238
+ :active-tab="activeTab"
239
+ :doc-type-def="docTypeDef"
212
240
  >
213
- <!-- Tabs inline with the title single-row header layout.
214
- Built-in tabs honour the `tabs` prop; plugin-registered tabs
215
- always render. -->
216
- <template #actions-left>
217
- <div class="flex items-center gap-1 ml-2">
218
- <UButton
219
- v-if="showEditorTab"
220
- icon="i-lucide-file-text"
221
- :label="locale.editorTab"
222
- size="xs"
223
- :color="activeTab === 'editor' ? 'primary' : 'neutral'"
224
- :variant="activeTab === 'editor' ? 'soft' : 'ghost'"
225
- @click="activeTab = 'editor'"
226
- />
227
- <UButton
228
- v-if="showPropertiesTab"
229
- icon="i-lucide-sliders-horizontal"
230
- :label="locale.propertiesTab"
231
- size="xs"
232
- :color="activeTab === 'properties' ? 'primary' : 'neutral'"
233
- :variant="activeTab === 'properties' ? 'soft' : 'ghost'"
234
- @click="activeTab = 'properties'"
235
- />
236
- <UChip
237
- v-if="showChatTab"
238
- :text="chatUnread > 0 && activeTab !== 'chat' ? String(chatUnread) : void 0"
239
- color="error"
240
- size="lg"
241
- :show="chatUnread > 0 && activeTab !== 'chat'"
242
- :inset="true"
241
+ <div class="flex items-center justify-between gap-2 px-3 h-10 border-b border-(--ui-border) shrink-0 bg-elevated/75 select-none">
242
+ <!-- Left: close · expand · connection badge -->
243
+ <div class="flex items-center gap-0.5">
244
+ <UTooltip
245
+ :text="locale.close"
246
+ :content="{ side: 'bottom' }"
243
247
  >
244
248
  <UButton
245
- icon="i-lucide-message-circle"
246
- label="Chat"
249
+ icon="i-lucide-x"
247
250
  size="xs"
248
- :color="activeTab === 'chat' ? 'primary' : 'neutral'"
249
- :variant="activeTab === 'chat' ? 'soft' : 'ghost'"
250
- @click="activeTab = 'chat'"
251
+ color="neutral"
252
+ variant="ghost"
253
+ @click="emit('close')"
251
254
  />
252
- </UChip>
253
- <UButton
254
- v-if="showSettingsTab"
255
- icon="i-lucide-settings-2"
256
- label="Settings"
257
- size="xs"
258
- :color="activeTab === 'settings' ? 'primary' : 'neutral'"
259
- :variant="activeTab === 'settings' ? 'soft' : 'ghost'"
260
- @click="activeTab = 'settings'"
261
- />
262
- <UButton
263
- v-if="showServerSettingsTab"
264
- icon="i-lucide-server-cog"
265
- label="Server"
266
- size="xs"
267
- :color="activeTab === 'serverSettings' ? 'primary' : 'neutral'"
268
- :variant="activeTab === 'serverSettings' ? 'soft' : 'ghost'"
269
- @click="activeTab = 'serverSettings'"
270
- />
271
- <UButton
272
- v-for="slot in pluginSlots"
273
- :key="slot.id"
274
- :icon="slot.icon"
275
- :label="slot.label"
276
- size="xs"
277
- :color="activeTab === slot.id ? 'primary' : 'neutral'"
278
- :variant="activeTab === slot.id ? 'soft' : 'ghost'"
279
- @click="activeTab = slot.id"
255
+ </UTooltip>
256
+ <UTooltip
257
+ :text="locale.openAsFullPage"
258
+ :content="{ side: 'bottom' }"
259
+ >
260
+ <UButton
261
+ icon="i-lucide-expand"
262
+ size="xs"
263
+ color="neutral"
264
+ variant="ghost"
265
+ @click="nodeId && navigateTo(hrefFor(nodeId))"
266
+ />
267
+ </UTooltip>
268
+ <AConnectionBadge
269
+ v-if="childProvider"
270
+ :provider="childProvider"
271
+ class="ml-1"
280
272
  />
281
273
  </div>
282
- </template>
283
- </ANodePanelHeader>
284
- </slot>
285
- </template>
286
274
 
287
- <template #body>
288
- <!-- Resize handle: left edge of slideover, desktop only -->
289
- <div
290
- class="absolute left-0 top-0 bottom-0 w-1.5 z-10 cursor-col-resize hidden sm:block group"
291
- :class="resize.isResizing.value ? 'bg-(--ui-primary)/20' : 'hover:bg-(--ui-primary)/10'"
292
- @mousedown.stop.prevent="resize.onMouseDown"
293
- @touchstart.stop.prevent="resize.onTouchStart"
294
- @dblclick="resize.onDoubleClick"
295
- />
296
-
297
- <!-- Editor tab — swaps to the registered page-type renderer when the
298
- doc has a non-'doc' type (kanban / table / calendar / etc.). -->
299
- <div
300
- v-show="activeTab === 'editor'"
301
- class="min-h-[60vh]"
302
- >
303
- <component
304
- :is="docTypeDef.component"
305
- v-if="nodeId && usesPageRenderer"
306
- :key="`${nodeId}-${currentType}`"
307
- :doc-id="nodeId"
308
- :doc-label="nodeLabel || ''"
309
- :child-provider="childProvider"
310
- />
311
- <AEditor
312
- v-else-if="nodeId"
313
- ref="editorRef"
314
- :doc-id="nodeId"
315
- :child-provider="childProvider"
316
- :parent-type="docType"
317
- :doc-meta="meta"
318
- class="h-full overflow-auto"
319
- @update-meta="patchMeta"
320
- />
321
- </div>
322
-
323
- <!-- Chat tab -->
324
- <div
325
- v-if="showChatTab"
326
- v-show="activeTab === 'chat'"
327
- class="min-h-[60vh] -mx-4 -mb-4 sm:-mx-6"
328
- >
329
- <ANodeChatPanel
330
- v-if="nodeId"
331
- :doc-id="nodeId"
332
- :label="nodeLabel || 'Chat'"
333
- />
334
- </div>
335
-
336
- <!-- Settings tab — snapshots wired by default; permissions and
337
- encryption are app-supplied via slots. -->
338
- <div
339
- v-if="showSettingsTab"
340
- v-show="activeTab === 'settings'"
341
- class="min-h-[60vh]"
342
- >
343
- <ANodeSettingsPanel
344
- v-if="nodeId"
345
- :doc-id="nodeId"
346
- :doc-type-key="currentType"
347
- :doc-label="nodeLabel"
348
- :doc-meta="meta"
349
- @update-meta="patchMeta"
350
- />
351
- </div>
352
-
353
- <!-- Server-settings tab (opt-in via `tabs`) — hub-doc server status. -->
354
- <div
355
- v-if="showServerSettingsTab"
356
- v-show="activeTab === 'serverSettings'"
357
- class="min-h-[60vh] -mx-4 -mb-4 sm:-mx-6"
358
- >
359
- <ADocPanelServerSettings
360
- v-if="nodeId"
361
- :doc-id="nodeId"
362
- />
363
- </div>
275
+ <!-- Right: undo/redo (editor tab) · unified view toggle · extras -->
276
+ <div class="flex items-center gap-0.5">
277
+ <template v-if="activeTab === 'editor' && tiptapEditor">
278
+ <AEditorUndoButton :editor="tiptapEditor" />
279
+ <AEditorRedoButton :editor="tiptapEditor" />
280
+ <USeparator
281
+ orientation="vertical"
282
+ class="h-4 mx-0.5"
283
+ />
284
+ </template>
364
285
 
365
- <!-- Properties tab -->
366
- <div
367
- v-show="activeTab === 'properties'"
368
- class="space-y-4"
369
- >
370
- <!-- Document type -->
371
- <div class="space-y-1">
372
- <label class="text-xs font-medium text-muted">{{ locale.docType }}</label>
373
- <ADocTypeSelect
374
- :model-value="currentType"
375
- @update:model-value="patchType"
376
- />
377
- </div>
286
+ <!-- Canonical four-slot toggle: pageType · editor · chat · settings -->
287
+ <ADocViewToggle
288
+ :model-value="toggleValue"
289
+ :doc-id="nodeId || ''"
290
+ :doc-type="currentType"
291
+ :chat-unread="chatUnread"
292
+ :editor-disabled="!showEditorTab"
293
+ :page-type-disabled="!childProvider"
294
+ :show-chat="showChatTab"
295
+ :show-settings="showSettingsTab"
296
+ @update:model-value="setTab"
297
+ />
378
298
 
379
- <!-- Icon + Color (row) -->
380
- <div class="grid grid-cols-2 gap-3">
381
- <div class="space-y-1">
382
- <label class="text-xs font-medium text-muted">{{ locale.icon }}</label>
383
- <AIconPicker
384
- :model-value="meta.icon"
385
- @update:model-value="patchMeta({ icon: $event })"
386
- />
387
- </div>
388
- <div class="space-y-1">
389
- <label class="text-xs font-medium text-muted">{{ locale.color }}</label>
390
- <AColorPicker
391
- :field-key="`node-panel:meta:color:${props.nodeId}`"
392
- :model-value="meta.color"
393
- @update:model-value="patchMeta({ color: $event })"
394
- />
299
+ <!-- Module extras kept outside the toggle (cou-sh parity: extras
300
+ sit beside the canonical four, never inside them). -->
301
+ <template v-if="showPropertiesTab || showServerSettingsTab || pluginSlots.length">
302
+ <USeparator
303
+ orientation="vertical"
304
+ class="h-4 mx-0.5"
305
+ />
306
+ <UTooltip
307
+ v-if="showPropertiesTab"
308
+ :text="locale.propertiesTab"
309
+ :content="{ side: 'bottom' }"
310
+ >
311
+ <UButton
312
+ icon="i-lucide-sliders-horizontal"
313
+ size="xs"
314
+ :color="activeTab === 'properties' ? 'primary' : 'neutral'"
315
+ :variant="activeTab === 'properties' ? 'soft' : 'ghost'"
316
+ @click="setTab('properties')"
317
+ />
318
+ </UTooltip>
319
+ <UTooltip
320
+ v-if="showServerSettingsTab"
321
+ :text="locale.serverTab"
322
+ :content="{ side: 'bottom' }"
323
+ >
324
+ <UButton
325
+ icon="i-lucide-bolt"
326
+ size="xs"
327
+ :color="activeTab === 'serverSettings' ? 'primary' : 'neutral'"
328
+ :variant="activeTab === 'serverSettings' ? 'soft' : 'ghost'"
329
+ @click="setTab('serverSettings')"
330
+ />
331
+ </UTooltip>
332
+ <UTooltip
333
+ v-for="slot in pluginSlots"
334
+ :key="slot.id"
335
+ :text="slot.label"
336
+ :content="{ side: 'bottom' }"
337
+ >
338
+ <UButton
339
+ :icon="slot.icon"
340
+ size="xs"
341
+ :color="activeTab === slot.id ? 'primary' : 'neutral'"
342
+ :variant="activeTab === slot.id ? 'soft' : 'ghost'"
343
+ @click="setTab(slot.id)"
344
+ />
345
+ </UTooltip>
346
+ </template>
347
+ </div>
395
348
  </div>
396
- </div>
349
+ </slot>
397
350
 
398
- <!-- Status + Priority (row) -->
399
- <div class="grid grid-cols-2 gap-3">
400
- <div class="space-y-1">
401
- <label class="text-xs font-medium text-muted">{{ locale.status }}</label>
402
- <USelect
403
- :model-value="meta.status || 'none'"
404
- :items="statusOptions"
405
- value-key="value"
406
- label-key="label"
407
- size="sm"
408
- @update:model-value="patchMeta({ status: $event === 'none' ? void 0 : $event })"
409
- />
410
- </div>
411
- <div class="space-y-1">
412
- <label class="text-xs font-medium text-muted">{{ locale.priority }}</label>
413
- <USelect
414
- :model-value="meta.priority ? String(meta.priority) : 'none'"
415
- :items="priorityOptions"
416
- value-key="value"
417
- label-key="label"
418
- size="sm"
419
- @update:model-value="patchMeta({ priority: $event === 'none' ? void 0 : Number($event) })"
351
+ <!-- Content area -->
352
+ <div class="flex flex-col flex-1 min-h-0 overflow-hidden">
353
+ <!-- Page-type tab — the registered renderer for non-'doc' types
354
+ (kanban / table / calendar / …). Now its own slot, so the editor
355
+ tab below always exposes the underlying prose. -->
356
+ <div
357
+ v-show="activeTab === 'pageType'"
358
+ class="flex flex-col flex-1 min-h-0"
359
+ >
360
+ <component
361
+ :is="docTypeDef.component"
362
+ v-if="nodeId && usesPageRenderer"
363
+ :key="`${nodeId}-${currentType}`"
364
+ :doc-id="nodeId"
365
+ :doc-label="resolvedLabel"
366
+ :child-provider="childProvider"
367
+ class="flex-1 overflow-hidden"
420
368
  />
421
369
  </div>
422
- </div>
423
370
 
424
- <!-- Rating -->
425
- <div class="space-y-1">
426
- <label class="text-xs font-medium text-muted">{{ locale.rating }}</label>
427
- <div class="flex items-center gap-1">
428
- <button
429
- v-for="n in 5"
430
- :key="n"
431
- type="button"
432
- class="transition-colors focus:outline-none"
433
- @click="patchMeta({ rating: meta.rating === n ? void 0 : n })"
434
- >
435
- <UIcon
436
- name="i-lucide-star"
437
- class="size-5"
438
- :class="(meta.rating ?? 0) >= n ? 'text-warning' : 'text-muted'"
439
- />
440
- </button>
441
- <button
442
- v-if="meta.rating"
443
- type="button"
444
- class="ml-1 text-xs text-muted hover:text-default"
445
- @click="patchMeta({ rating: void 0 })"
446
- >
447
- <UIcon
448
- name="i-lucide-x"
449
- class="size-3"
450
- />
451
- </button>
452
- </div>
453
- </div>
454
-
455
- <!-- URL -->
456
- <div class="space-y-1">
457
- <label class="text-xs font-medium text-muted">{{ locale.url }}</label>
458
- <UInput
459
- type="url"
460
- :model-value="meta.url ?? ''"
461
- placeholder="https://…"
462
- size="sm"
463
- @update:model-value="patchMeta({ url: $event || void 0 })"
464
- />
465
- </div>
466
-
467
- <!-- Dates -->
468
- <div class="space-y-1">
469
- <label class="text-xs font-medium text-muted">{{ locale.dates }}</label>
470
- <div class="grid grid-cols-2 gap-2">
471
- <UInput
472
- type="date"
473
- :model-value="meta.dateStart ?? ''"
474
- size="sm"
475
- @update:model-value="patchMeta({ dateStart: $event || void 0 })"
476
- />
477
- <UInput
478
- type="date"
479
- :model-value="meta.dateEnd ?? ''"
480
- size="sm"
481
- @update:model-value="patchMeta({ dateEnd: $event || void 0 })"
371
+ <!-- Editor tab — always the TipTap prose editor for the doc body. -->
372
+ <div
373
+ v-show="activeTab === 'editor'"
374
+ class="flex flex-col flex-1 min-h-0"
375
+ >
376
+ <AEditor
377
+ v-if="nodeId"
378
+ ref="editorRef"
379
+ :doc-id="nodeId"
380
+ :doc-label="resolvedLabel"
381
+ :child-provider="childProvider"
382
+ :parent-type="docType"
383
+ :doc-meta="meta"
384
+ class="flex-1 overflow-y-auto"
385
+ @update-meta="patchMeta"
482
386
  />
483
387
  </div>
484
- </div>
485
388
 
486
- <!-- Tags -->
487
- <div class="space-y-1">
488
- <label class="text-xs font-medium text-muted">Tags</label>
489
- <div class="flex flex-wrap gap-1 mb-1">
490
- <UBadge
491
- v-for="tag in tags"
492
- :key="tag"
493
- variant="soft"
494
- class="cursor-pointer"
495
- @click="removeTag(tag)"
496
- >
497
- {{ tag }}
498
- <UIcon
499
- name="i-lucide-x"
500
- class="size-3 ml-1"
501
- />
502
- </UBadge>
389
+ <!-- Chat tab -->
390
+ <div
391
+ v-if="showChatTab"
392
+ v-show="activeTab === 'chat'"
393
+ class="flex flex-col flex-1 min-h-0"
394
+ >
395
+ <ANodeChatPanel
396
+ v-if="nodeId"
397
+ :doc-id="nodeId"
398
+ :label="resolvedLabel || 'Chat'"
399
+ class="flex-1 min-h-0"
400
+ />
503
401
  </div>
504
- <UInput
505
- v-model="tagInput"
506
- placeholder="Add tag…"
507
- size="sm"
508
- @keydown.enter.prevent="addTag"
509
- @keydown="($event2) => {
510
- if ($event2.key === ',') {
511
- $event2.preventDefault();
512
- addTag();
513
- }
514
- }"
515
- />
516
- </div>
517
402
 
518
- <!-- Cover image -->
519
- <div
520
- v-if="meta.coverUploadId || meta.coverDocId"
521
- class="space-y-1"
522
- >
523
- <label class="text-xs font-medium text-muted">{{ locale.cover }}</label>
524
- <div class="flex items-center gap-2 text-xs text-muted">
525
- <UIcon
526
- name="i-lucide-image"
527
- class="size-4 shrink-0"
528
- />
529
- <span class="truncate">{{ meta.coverUploadId ?? meta.coverDocId }}</span>
530
- <UButton
531
- icon="i-lucide-x"
532
- size="xs"
533
- variant="ghost"
534
- color="neutral"
535
- @click="patchMeta({ coverUploadId: void 0, coverDocId: void 0, coverMimeType: void 0 })"
403
+ <!-- Settings tab — snapshots wired by default; permissions and
404
+ encryption are app-supplied via slots. -->
405
+ <div
406
+ v-if="showSettingsTab"
407
+ v-show="activeTab === 'settings'"
408
+ class="flex-1 min-h-0 overflow-y-auto"
409
+ >
410
+ <ANodeSettingsPanel
411
+ v-if="nodeId"
412
+ :doc-id="nodeId"
413
+ :doc-type-key="currentType"
414
+ :doc-label="resolvedLabel"
415
+ :doc-meta="meta"
416
+ @update-meta="patchMeta"
536
417
  />
537
418
  </div>
538
- </div>
539
419
 
540
- <!-- Custom fields -->
541
- <div class="space-y-2">
542
- <div class="flex items-center justify-between">
543
- <label class="text-xs font-medium text-muted">{{ locale.customFields }}</label>
544
- <UDropdownMenu :items="addFieldMenuItems">
545
- <UButton
546
- :label="locale.addField"
547
- icon="i-lucide-plus"
548
- size="xs"
549
- variant="ghost"
550
- color="neutral"
551
- />
552
- </UDropdownMenu>
420
+ <!-- Server-settings tab (opt-in via `tabs`) — hub-doc server status. -->
421
+ <div
422
+ v-if="showServerSettingsTab"
423
+ v-show="activeTab === 'serverSettings'"
424
+ class="flex-1 min-h-0 overflow-y-auto"
425
+ >
426
+ <ADocPanelServerSettings
427
+ v-if="nodeId"
428
+ :doc-id="nodeId"
429
+ />
553
430
  </div>
554
431
 
432
+ <!-- Properties tab -->
555
433
  <div
556
- v-for="field in customFields"
557
- :key="field.id"
558
- class="space-y-1"
434
+ v-show="activeTab === 'properties'"
435
+ class="flex-1 min-h-0 overflow-y-auto p-4 space-y-4"
559
436
  >
560
- <div class="flex items-center justify-between">
561
- <label class="text-xs font-medium text-muted">{{ field.label || field.type }}</label>
562
- <UButton
563
- icon="i-lucide-trash-2"
564
- size="xs"
565
- variant="ghost"
566
- color="neutral"
567
- class="opacity-40 hover:opacity-100"
568
- @click="removeCustomField(field.id)"
437
+ <!-- Document type -->
438
+ <div class="space-y-1">
439
+ <label class="text-xs font-medium text-muted">{{ locale.docType }}</label>
440
+ <ADocTypeSelect
441
+ :model-value="currentType"
442
+ @update:model-value="patchType"
569
443
  />
570
444
  </div>
571
445
 
572
- <!-- Toggle -->
573
- <template v-if="field.type === 'toggle'">
574
- <USwitch
575
- :model-value="!!getCustomFieldValue(field)"
576
- @update:model-value="setCustomFieldValue(field, $event)"
577
- />
578
- </template>
446
+ <!-- Icon + Color (row) -->
447
+ <div class="grid grid-cols-2 gap-3">
448
+ <div class="space-y-1">
449
+ <label class="text-xs font-medium text-muted">{{ locale.icon }}</label>
450
+ <AIconPicker
451
+ :model-value="meta.icon"
452
+ @update:model-value="patchMeta({ icon: $event })"
453
+ />
454
+ </div>
455
+ <div class="space-y-1">
456
+ <label class="text-xs font-medium text-muted">{{ locale.color }}</label>
457
+ <AColorPicker
458
+ :field-key="`node-panel:meta:color:${props.nodeId}`"
459
+ :model-value="meta.color"
460
+ @update:model-value="patchMeta({ color: $event })"
461
+ />
462
+ </div>
463
+ </div>
579
464
 
580
- <!-- Number -->
581
- <template v-else-if="field.type === 'number'">
582
- <UInput
583
- type="number"
584
- :model-value="String(getCustomFieldValue(field) ?? '')"
585
- size="sm"
586
- @update:model-value="setCustomFieldValue(field, $event ? Number($event) : void 0)"
587
- />
588
- </template>
589
-
590
- <!-- Slider / Progress -->
591
- <template v-else-if="field.type === 'slider'">
592
- <div class="flex items-center gap-2">
593
- <USlider
594
- :model-value="Number(getCustomFieldValue(field) ?? field.min ?? 0)"
595
- :min="field.min ?? 0"
596
- :max="field.max ?? 100"
465
+ <!-- Status + Priority (row) -->
466
+ <div class="grid grid-cols-2 gap-3">
467
+ <div class="space-y-1">
468
+ <label class="text-xs font-medium text-muted">{{ locale.status }}</label>
469
+ <USelect
470
+ :model-value="meta.status || 'none'"
471
+ :items="statusOptions"
472
+ value-key="value"
473
+ label-key="label"
474
+ size="sm"
475
+ @update:model-value="patchMeta({ status: $event === 'none' ? void 0 : $event })"
476
+ />
477
+ </div>
478
+ <div class="space-y-1">
479
+ <label class="text-xs font-medium text-muted">{{ locale.priority }}</label>
480
+ <USelect
481
+ :model-value="meta.priority ? String(meta.priority) : 'none'"
482
+ :items="priorityOptions"
483
+ value-key="value"
484
+ label-key="label"
597
485
  size="sm"
598
- class="flex-1"
599
- @update:model-value="setCustomFieldValue(field, $event)"
486
+ @update:model-value="patchMeta({ priority: $event === 'none' ? void 0 : Number($event) })"
600
487
  />
601
- <span class="text-xs text-muted w-8 text-right">{{ getCustomFieldValue(field) ?? field.min ?? 0 }}</span>
602
488
  </div>
603
- </template>
489
+ </div>
604
490
 
605
491
  <!-- Rating -->
606
- <template v-else-if="field.type === 'rating'">
492
+ <div class="space-y-1">
493
+ <label class="text-xs font-medium text-muted">{{ locale.rating }}</label>
607
494
  <div class="flex items-center gap-1">
608
495
  <button
609
- v-for="n in field.max ?? 5"
496
+ v-for="n in 5"
610
497
  :key="n"
611
498
  type="button"
612
- class="focus:outline-none"
613
- @click="setCustomFieldValue(field, getCustomFieldValue(field) === n ? void 0 : n)"
499
+ class="transition-colors focus:outline-none"
500
+ @click="patchMeta({ rating: meta.rating === n ? void 0 : n })"
614
501
  >
615
502
  <UIcon
616
503
  name="i-lucide-star"
617
- class="size-4"
618
- :class="Number(getCustomFieldValue(field) ?? 0) >= n ? 'text-warning' : 'text-muted'"
504
+ class="size-5"
505
+ :class="(meta.rating ?? 0) >= n ? 'text-warning' : 'text-muted'"
506
+ />
507
+ </button>
508
+ <button
509
+ v-if="meta.rating"
510
+ type="button"
511
+ class="ml-1 text-xs text-muted hover:text-default"
512
+ @click="patchMeta({ rating: void 0 })"
513
+ >
514
+ <UIcon
515
+ name="i-lucide-x"
516
+ class="size-3"
619
517
  />
620
518
  </button>
621
519
  </div>
622
- </template>
520
+ </div>
623
521
 
624
- <!-- Date / Datetime / Time (single) -->
625
- <template v-else-if="['date', 'datetime', 'time'].includes(field.type)">
522
+ <!-- URL -->
523
+ <div class="space-y-1">
524
+ <label class="text-xs font-medium text-muted">{{ locale.url }}</label>
626
525
  <UInput
627
- :type="field.type === 'datetime' ? 'datetime-local' : field.type"
628
- :model-value="String(getCustomFieldValue(field) ?? '')"
526
+ type="url"
527
+ :model-value="meta.url ?? ''"
528
+ placeholder="https://…"
629
529
  size="sm"
630
- @update:model-value="setCustomFieldValue(field, $event || void 0)"
530
+ @update:model-value="patchMeta({ url: $event || void 0 })"
631
531
  />
632
- </template>
532
+ </div>
633
533
 
634
- <!-- Date range / Time range / Datetime range -->
635
- <template v-else-if="['daterange', 'timerange', 'datetimerange'].includes(field.type)">
534
+ <!-- Dates -->
535
+ <div class="space-y-1">
536
+ <label class="text-xs font-medium text-muted">{{ locale.dates }}</label>
636
537
  <div class="grid grid-cols-2 gap-2">
637
538
  <UInput
638
- :type="field.type === 'datetimerange' ? 'datetime-local' : field.type === 'timerange' ? 'time' : 'date'"
639
- :model-value="field.startKey ? String(meta[field.startKey] ?? '') : ''"
539
+ type="date"
540
+ :model-value="meta.dateStart ?? ''"
640
541
  size="sm"
641
- @update:model-value="field.startKey && patchMeta({ [field.startKey]: $event || void 0 })"
542
+ @update:model-value="patchMeta({ dateStart: $event || void 0 })"
642
543
  />
643
544
  <UInput
644
- :type="field.type === 'datetimerange' ? 'datetime-local' : field.type === 'timerange' ? 'time' : 'date'"
645
- :model-value="field.endKey ? String(meta[field.endKey] ?? '') : ''"
545
+ type="date"
546
+ :model-value="meta.dateEnd ?? ''"
646
547
  size="sm"
647
- @update:model-value="field.endKey && patchMeta({ [field.endKey]: $event || void 0 })"
548
+ @update:model-value="patchMeta({ dateEnd: $event || void 0 })"
648
549
  />
649
550
  </div>
650
- </template>
651
-
652
- <!-- Textarea -->
653
- <template v-else-if="field.type === 'textarea'">
654
- <UTextarea
655
- :model-value="String(getCustomFieldValue(field) ?? '')"
656
- :rows="3"
657
- size="sm"
658
- @update:model-value="setCustomFieldValue(field, $event || void 0)"
659
- />
660
- </template>
661
-
662
- <!-- URL -->
663
- <template v-else-if="field.type === 'url'">
664
- <UInput
665
- type="url"
666
- :model-value="String(getCustomFieldValue(field) ?? '')"
667
- placeholder="https://…"
668
- size="sm"
669
- @update:model-value="setCustomFieldValue(field, $event || void 0)"
670
- />
671
- </template>
551
+ </div>
672
552
 
673
553
  <!-- Tags -->
674
- <template v-else-if="field.type === 'tags'">
675
- <div class="flex flex-wrap gap-1">
554
+ <div class="space-y-1">
555
+ <label class="text-xs font-medium text-muted">Tags</label>
556
+ <div class="flex flex-wrap gap-1 mb-1">
676
557
  <UBadge
677
- v-for="t in Array.isArray(getCustomFieldValue(field)) ? getCustomFieldValue(field) : []"
678
- :key="t"
558
+ v-for="tag in tags"
559
+ :key="tag"
679
560
  variant="soft"
680
561
  class="cursor-pointer"
681
- @click="setCustomFieldValue(field, getCustomFieldValue(field).filter((x) => x !== t))"
562
+ @click="removeTag(tag)"
682
563
  >
683
- {{ t }}<UIcon
564
+ {{ tag }}
565
+ <UIcon
684
566
  name="i-lucide-x"
685
567
  class="size-3 ml-1"
686
568
  />
687
569
  </UBadge>
688
570
  </div>
689
- </template>
690
-
691
- <!-- Icon -->
692
- <template v-else-if="field.type === 'icon'">
693
- <AIconPicker
694
- :model-value="String(getCustomFieldValue(field) ?? '')"
695
- @update:model-value="setCustomFieldValue(field, $event)"
696
- />
697
- </template>
698
-
699
- <!-- Color Preset / Color Picker -->
700
- <template v-else-if="['colorPreset', 'colorPicker'].includes(field.type)">
701
- <AColorPicker
702
- :field-key="`node-panel:custom:${props.nodeId}:${field.key}`"
703
- :model-value="String(getCustomFieldValue(field) ?? '')"
704
- @update:model-value="setCustomFieldValue(field, $event)"
705
- />
706
- </template>
707
-
708
- <!-- Fallback: text input -->
709
- <template v-else>
710
571
  <UInput
711
- :model-value="String(getCustomFieldValue(field) ?? '')"
572
+ v-model="tagInput"
573
+ placeholder="Add tag…"
712
574
  size="sm"
713
- @update:model-value="setCustomFieldValue(field, $event || void 0)"
575
+ @keydown.enter.prevent="addTag"
576
+ @keydown="($event2) => {
577
+ if ($event2.key === ',') {
578
+ $event2.preventDefault();
579
+ addTag();
580
+ }
581
+ }"
714
582
  />
715
- </template>
583
+ </div>
584
+
585
+ <!-- Cover image -->
586
+ <div
587
+ v-if="meta.coverUploadId || meta.coverDocId"
588
+ class="space-y-1"
589
+ >
590
+ <label class="text-xs font-medium text-muted">{{ locale.cover }}</label>
591
+ <div class="flex items-center gap-2 text-xs text-muted">
592
+ <UIcon
593
+ name="i-lucide-image"
594
+ class="size-4 shrink-0"
595
+ />
596
+ <span class="truncate">{{ meta.coverUploadId ?? meta.coverDocId }}</span>
597
+ <UButton
598
+ icon="i-lucide-x"
599
+ size="xs"
600
+ variant="ghost"
601
+ color="neutral"
602
+ @click="patchMeta({ coverUploadId: void 0, coverDocId: void 0, coverMimeType: void 0 })"
603
+ />
604
+ </div>
605
+ </div>
606
+
607
+ <!-- Custom fields -->
608
+ <div class="space-y-2">
609
+ <div class="flex items-center justify-between">
610
+ <label class="text-xs font-medium text-muted">{{ locale.customFields }}</label>
611
+ <UDropdownMenu :items="addFieldMenuItems">
612
+ <UButton
613
+ :label="locale.addField"
614
+ icon="i-lucide-plus"
615
+ size="xs"
616
+ variant="ghost"
617
+ color="neutral"
618
+ />
619
+ </UDropdownMenu>
620
+ </div>
621
+
622
+ <div
623
+ v-for="field in customFields"
624
+ :key="field.id"
625
+ class="space-y-1"
626
+ >
627
+ <div class="flex items-center justify-between">
628
+ <label class="text-xs font-medium text-muted">{{ field.label || field.type }}</label>
629
+ <UButton
630
+ icon="i-lucide-trash-2"
631
+ size="xs"
632
+ variant="ghost"
633
+ color="neutral"
634
+ class="opacity-40 hover:opacity-100"
635
+ @click="removeCustomField(field.id)"
636
+ />
637
+ </div>
638
+
639
+ <!-- Toggle -->
640
+ <template v-if="field.type === 'toggle'">
641
+ <USwitch
642
+ :model-value="!!getCustomFieldValue(field)"
643
+ @update:model-value="setCustomFieldValue(field, $event)"
644
+ />
645
+ </template>
646
+
647
+ <!-- Number -->
648
+ <template v-else-if="field.type === 'number'">
649
+ <UInput
650
+ type="number"
651
+ :model-value="String(getCustomFieldValue(field) ?? '')"
652
+ size="sm"
653
+ @update:model-value="setCustomFieldValue(field, $event ? Number($event) : void 0)"
654
+ />
655
+ </template>
656
+
657
+ <!-- Slider / Progress -->
658
+ <template v-else-if="field.type === 'slider'">
659
+ <div class="flex items-center gap-2">
660
+ <USlider
661
+ :model-value="Number(getCustomFieldValue(field) ?? field.min ?? 0)"
662
+ :min="field.min ?? 0"
663
+ :max="field.max ?? 100"
664
+ size="sm"
665
+ class="flex-1"
666
+ @update:model-value="setCustomFieldValue(field, $event)"
667
+ />
668
+ <span class="text-xs text-muted w-8 text-right">{{ getCustomFieldValue(field) ?? field.min ?? 0 }}</span>
669
+ </div>
670
+ </template>
671
+
672
+ <!-- Rating -->
673
+ <template v-else-if="field.type === 'rating'">
674
+ <div class="flex items-center gap-1">
675
+ <button
676
+ v-for="n in field.max ?? 5"
677
+ :key="n"
678
+ type="button"
679
+ class="focus:outline-none"
680
+ @click="setCustomFieldValue(field, getCustomFieldValue(field) === n ? void 0 : n)"
681
+ >
682
+ <UIcon
683
+ name="i-lucide-star"
684
+ class="size-4"
685
+ :class="Number(getCustomFieldValue(field) ?? 0) >= n ? 'text-warning' : 'text-muted'"
686
+ />
687
+ </button>
688
+ </div>
689
+ </template>
690
+
691
+ <!-- Date / Datetime / Time (single) -->
692
+ <template v-else-if="['date', 'datetime', 'time'].includes(field.type)">
693
+ <UInput
694
+ :type="field.type === 'datetime' ? 'datetime-local' : field.type"
695
+ :model-value="String(getCustomFieldValue(field) ?? '')"
696
+ size="sm"
697
+ @update:model-value="setCustomFieldValue(field, $event || void 0)"
698
+ />
699
+ </template>
700
+
701
+ <!-- Date range / Time range / Datetime range -->
702
+ <template v-else-if="['daterange', 'timerange', 'datetimerange'].includes(field.type)">
703
+ <div class="grid grid-cols-2 gap-2">
704
+ <UInput
705
+ :type="field.type === 'datetimerange' ? 'datetime-local' : field.type === 'timerange' ? 'time' : 'date'"
706
+ :model-value="field.startKey ? String(meta[field.startKey] ?? '') : ''"
707
+ size="sm"
708
+ @update:model-value="field.startKey && patchMeta({ [field.startKey]: $event || void 0 })"
709
+ />
710
+ <UInput
711
+ :type="field.type === 'datetimerange' ? 'datetime-local' : field.type === 'timerange' ? 'time' : 'date'"
712
+ :model-value="field.endKey ? String(meta[field.endKey] ?? '') : ''"
713
+ size="sm"
714
+ @update:model-value="field.endKey && patchMeta({ [field.endKey]: $event || void 0 })"
715
+ />
716
+ </div>
717
+ </template>
718
+
719
+ <!-- Textarea -->
720
+ <template v-else-if="field.type === 'textarea'">
721
+ <UTextarea
722
+ :model-value="String(getCustomFieldValue(field) ?? '')"
723
+ :rows="3"
724
+ size="sm"
725
+ @update:model-value="setCustomFieldValue(field, $event || void 0)"
726
+ />
727
+ </template>
728
+
729
+ <!-- URL -->
730
+ <template v-else-if="field.type === 'url'">
731
+ <UInput
732
+ type="url"
733
+ :model-value="String(getCustomFieldValue(field) ?? '')"
734
+ placeholder="https://…"
735
+ size="sm"
736
+ @update:model-value="setCustomFieldValue(field, $event || void 0)"
737
+ />
738
+ </template>
739
+
740
+ <!-- Tags -->
741
+ <template v-else-if="field.type === 'tags'">
742
+ <div class="flex flex-wrap gap-1">
743
+ <UBadge
744
+ v-for="t in Array.isArray(getCustomFieldValue(field)) ? getCustomFieldValue(field) : []"
745
+ :key="t"
746
+ variant="soft"
747
+ class="cursor-pointer"
748
+ @click="setCustomFieldValue(field, getCustomFieldValue(field).filter((x) => x !== t))"
749
+ >
750
+ {{ t }}<UIcon
751
+ name="i-lucide-x"
752
+ class="size-3 ml-1"
753
+ />
754
+ </UBadge>
755
+ </div>
756
+ </template>
757
+
758
+ <!-- Icon -->
759
+ <template v-else-if="field.type === 'icon'">
760
+ <AIconPicker
761
+ :model-value="String(getCustomFieldValue(field) ?? '')"
762
+ @update:model-value="setCustomFieldValue(field, $event)"
763
+ />
764
+ </template>
765
+
766
+ <!-- Color Preset / Color Picker -->
767
+ <template v-else-if="['colorPreset', 'colorPicker'].includes(field.type)">
768
+ <AColorPicker
769
+ :field-key="`node-panel:custom:${props.nodeId}:${field.key}`"
770
+ :model-value="String(getCustomFieldValue(field) ?? '')"
771
+ @update:model-value="setCustomFieldValue(field, $event)"
772
+ />
773
+ </template>
774
+
775
+ <!-- Fallback: text input -->
776
+ <template v-else>
777
+ <UInput
778
+ :model-value="String(getCustomFieldValue(field) ?? '')"
779
+ size="sm"
780
+ @update:model-value="setCustomFieldValue(field, $event || void 0)"
781
+ />
782
+ </template>
783
+ </div>
784
+ </div>
716
785
  </div>
717
- </div>
718
- </div>
719
786
 
720
- <!-- Plugin slots -->
721
- <template
722
- v-for="slot in pluginSlots"
723
- :key="slot.id"
724
- >
725
- <div v-show="activeTab === slot.id">
726
- <component
727
- :is="slot.component"
728
- v-if="nodeId"
729
- :child-id="nodeId"
730
- :child-doc="childProvider?.document ?? null"
731
- :parent-doc-id="nodeId"
732
- :parent-type="docType"
733
- :meta="meta"
734
- :tree="tree"
735
- />
787
+ <!-- Plugin slots -->
788
+ <template
789
+ v-for="slot in pluginSlots"
790
+ :key="slot.id"
791
+ >
792
+ <div
793
+ v-show="activeTab === slot.id"
794
+ class="flex-1 min-h-0 overflow-y-auto p-4"
795
+ >
796
+ <component
797
+ :is="slot.component"
798
+ v-if="nodeId"
799
+ :child-id="nodeId"
800
+ :child-doc="childProvider?.document ?? null"
801
+ :parent-doc-id="nodeId"
802
+ :parent-type="docType"
803
+ :meta="meta"
804
+ :tree="tree"
805
+ />
806
+ </div>
807
+ </template>
736
808
  </div>
737
- </template>
809
+ </div>
738
810
  </template>
739
811
  </USlideover>
740
812
  </template>