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