@abraca/nuxt 2.10.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 (127) hide show
  1. package/dist/module.d.mts +14 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +9 -0
  4. package/dist/runtime/assets/editor.css +1 -1
  5. package/dist/runtime/components/AConnectionBadge.d.vue.ts +29 -0
  6. package/dist/runtime/components/AConnectionBadge.vue +79 -0
  7. package/dist/runtime/components/AConnectionBadge.vue.d.ts +29 -0
  8. package/dist/runtime/components/ADocPickerModal.d.vue.ts +31 -0
  9. package/dist/runtime/components/ADocPickerModal.vue +191 -0
  10. package/dist/runtime/components/ADocPickerModal.vue.d.ts +31 -0
  11. package/dist/runtime/components/ADocumentTree.vue +65 -0
  12. package/dist/runtime/components/AEditor.d.vue.ts +19 -12
  13. package/dist/runtime/components/AEditor.vue +243 -165
  14. package/dist/runtime/components/AEditor.vue.d.ts +19 -12
  15. package/dist/runtime/components/AEncryptionModePicker.d.vue.ts +33 -0
  16. package/dist/runtime/components/AEncryptionModePicker.vue +211 -0
  17. package/dist/runtime/components/AEncryptionModePicker.vue.d.ts +33 -0
  18. package/dist/runtime/components/AModalShell.d.vue.ts +48 -0
  19. package/dist/runtime/components/AModalShell.vue +105 -0
  20. package/dist/runtime/components/AModalShell.vue.d.ts +48 -0
  21. package/dist/runtime/components/ANodePanel.d.vue.ts +17 -7
  22. package/dist/runtime/components/ANodePanel.vue +550 -451
  23. package/dist/runtime/components/ANodePanel.vue.d.ts +17 -7
  24. package/dist/runtime/components/ANodePanelHeader.d.vue.ts +20 -10
  25. package/dist/runtime/components/ANodePanelHeader.vue +17 -3
  26. package/dist/runtime/components/ANodePanelHeader.vue.d.ts +20 -10
  27. package/dist/runtime/components/ANodeSettingsPanel.d.vue.ts +2 -0
  28. package/dist/runtime/components/ANodeSettingsPanel.vue +21 -1
  29. package/dist/runtime/components/ANodeSettingsPanel.vue.d.ts +2 -0
  30. package/dist/runtime/components/ASnapshotPreviewModal.d.vue.ts +33 -0
  31. package/dist/runtime/components/ASnapshotPreviewModal.vue +430 -0
  32. package/dist/runtime/components/ASnapshotPreviewModal.vue.d.ts +33 -0
  33. package/dist/runtime/components/ATagsEditor.d.vue.ts +19 -0
  34. package/dist/runtime/components/ATagsEditor.vue +60 -0
  35. package/dist/runtime/components/ATagsEditor.vue.d.ts +19 -0
  36. package/dist/runtime/components/aware/AMedia.d.vue.ts +1 -1
  37. package/dist/runtime/components/aware/AMedia.vue.d.ts +1 -1
  38. package/dist/runtime/components/chat/AChatInput.d.vue.ts +11 -6
  39. package/dist/runtime/components/chat/AChatInput.vue +33 -2
  40. package/dist/runtime/components/chat/AChatInput.vue.d.ts +11 -6
  41. package/dist/runtime/components/chat/AChatList.d.vue.ts +12 -0
  42. package/dist/runtime/components/chat/AChatList.vue +76 -32
  43. package/dist/runtime/components/chat/AChatList.vue.d.ts +12 -0
  44. package/dist/runtime/components/chat/AChatMessages.d.vue.ts +4 -0
  45. package/dist/runtime/components/chat/AChatMessages.vue +57 -4
  46. package/dist/runtime/components/chat/AChatMessages.vue.d.ts +4 -0
  47. package/dist/runtime/components/chat/AChatPanel.d.vue.ts +6 -2
  48. package/dist/runtime/components/chat/AChatPanel.vue +17 -1
  49. package/dist/runtime/components/chat/AChatPanel.vue.d.ts +6 -2
  50. package/dist/runtime/components/chat/ANodeChatPanel.vue +1 -1
  51. package/dist/runtime/components/docs/ADocsSearch.d.vue.ts +1 -1
  52. package/dist/runtime/components/docs/ADocsSearch.vue.d.ts +1 -1
  53. package/dist/runtime/components/editor/ALocationPickerPopover.vue +28 -7
  54. package/dist/runtime/components/registry/APluginDetail.d.vue.ts +2 -2
  55. package/dist/runtime/components/registry/APluginDetail.vue.d.ts +2 -2
  56. package/dist/runtime/components/renderers/AChartRenderer.client.d.vue.ts +17 -0
  57. package/dist/runtime/components/renderers/AChartRenderer.client.vue +622 -0
  58. package/dist/runtime/components/renderers/AChartRenderer.client.vue.d.ts +17 -0
  59. package/dist/runtime/components/renderers/AGraphRenderer.vue +64 -15
  60. package/dist/runtime/components/renderers/AProseRenderer.d.vue.ts +2 -2
  61. package/dist/runtime/components/renderers/AProseRenderer.vue.d.ts +2 -2
  62. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.d.vue.ts +2 -2
  63. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.vue.d.ts +2 -2
  64. package/dist/runtime/components/renderers/media/MediaTransportBar.d.vue.ts +2 -2
  65. package/dist/runtime/components/renderers/media/MediaTransportBar.vue.d.ts +2 -2
  66. package/dist/runtime/components/renderers/sheets/ASheetsGrid.d.vue.ts +2 -2
  67. package/dist/runtime/components/renderers/sheets/ASheetsGrid.vue.d.ts +2 -2
  68. package/dist/runtime/components/settings/ASettingsAppearancePanel.d.vue.ts +3 -0
  69. package/dist/runtime/components/settings/ASettingsAppearancePanel.vue +67 -0
  70. package/dist/runtime/components/settings/ASettingsAppearancePanel.vue.d.ts +3 -0
  71. package/dist/runtime/components/settings/ASettingsGroup.d.vue.ts +24 -0
  72. package/dist/runtime/components/settings/ASettingsGroup.vue +31 -0
  73. package/dist/runtime/components/settings/ASettingsGroup.vue.d.ts +24 -0
  74. package/dist/runtime/components/settings/ASettingsModal.vue +84 -53
  75. package/dist/runtime/components/settings/ASettingsPlaceholder.d.vue.ts +20 -0
  76. package/dist/runtime/components/settings/ASettingsPlaceholder.vue +32 -0
  77. package/dist/runtime/components/settings/ASettingsPlaceholder.vue.d.ts +20 -0
  78. package/dist/runtime/components/settings/ASettingsRow.d.vue.ts +34 -0
  79. package/dist/runtime/components/settings/ASettingsRow.vue +34 -0
  80. package/dist/runtime/components/settings/ASettingsRow.vue.d.ts +34 -0
  81. package/dist/runtime/components/settings/sections.d.ts +37 -0
  82. package/dist/runtime/components/settings/sections.js +45 -0
  83. package/dist/runtime/components/shell/ABreadcrumbForDoc.d.vue.ts +6 -0
  84. package/dist/runtime/components/shell/ABreadcrumbForDoc.vue +75 -3
  85. package/dist/runtime/components/shell/ABreadcrumbForDoc.vue.d.ts +6 -0
  86. package/dist/runtime/components/shell/ADocPanelServerSettings.d.vue.ts +17 -0
  87. package/dist/runtime/components/shell/ADocPanelServerSettings.vue +253 -0
  88. package/dist/runtime/components/shell/ADocPanelServerSettings.vue.d.ts +17 -0
  89. package/dist/runtime/components/shell/ADocPanelSettings.d.vue.ts +2 -0
  90. package/dist/runtime/components/shell/ADocPanelSettings.vue +15 -4
  91. package/dist/runtime/components/shell/ADocPanelSettings.vue.d.ts +2 -0
  92. package/dist/runtime/components/shell/AUserProfilePopover.d.vue.ts +1 -1
  93. package/dist/runtime/components/shell/AUserProfilePopover.vue.d.ts +1 -1
  94. package/dist/runtime/composables/useChat.d.ts +22 -1
  95. package/dist/runtime/composables/useChat.js +79 -8
  96. package/dist/runtime/composables/useDocBreadcrumb.d.ts +17 -2
  97. package/dist/runtime/composables/useDocBreadcrumb.js +17 -3
  98. package/dist/runtime/composables/useDocSnapshots.d.ts +2 -1
  99. package/dist/runtime/composables/useDocSnapshots.js +5 -0
  100. package/dist/runtime/composables/useEditor.d.ts +1 -1
  101. package/dist/runtime/composables/useEditor.js +120 -0
  102. package/dist/runtime/composables/useEditorToolbar.d.ts +12 -4
  103. package/dist/runtime/composables/useEditorToolbar.js +78 -56
  104. package/dist/runtime/composables/useNodeContextMenu.d.ts +14 -0
  105. package/dist/runtime/composables/useNodeContextMenu.js +59 -1
  106. package/dist/runtime/composables/useSettingsModal.d.ts +1 -1
  107. package/dist/runtime/composables/useSwipeGesture.d.ts +48 -0
  108. package/dist/runtime/composables/useSwipeGesture.js +140 -0
  109. package/dist/runtime/extensions/document-header.js +16 -6
  110. package/dist/runtime/extensions/document-meta.js +344 -19
  111. package/dist/runtime/extensions/meta-field.js +42 -0
  112. package/dist/runtime/extensions/views/DocumentMetaView.vue +33 -7
  113. package/dist/runtime/extensions/views/FieldView.vue +51 -19
  114. package/dist/runtime/extensions/views/MetaFieldView.vue +30 -4
  115. package/dist/runtime/locale.d.ts +8 -0
  116. package/dist/runtime/locale.js +9 -1
  117. package/dist/runtime/middleware/abracadabra-auth.d.ts +1 -1
  118. package/dist/runtime/plugin-abracadabra.client.d.ts +1 -1
  119. package/dist/runtime/plugin-abracadabra.client.js +12 -2
  120. package/dist/runtime/plugin-abracadabra.server.d.ts +1 -1
  121. package/dist/runtime/plugin-shared-globals.client.d.ts +1 -1
  122. package/dist/runtime/utils/chatContent.d.ts +20 -2
  123. package/dist/runtime/utils/chatContent.js +20 -1
  124. package/dist/runtime/utils/docTypes.js +43 -0
  125. package/dist/runtime/utils/titleSync.d.ts +130 -0
  126. package/dist/runtime/utils/titleSync.js +53 -0
  127. package/package.json +11 -4
@@ -1,19 +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";
13
- const ANodeChatPanel = defineAsyncComponent(() => import("./chat/ANodeChatPanel.vue"));
14
- const ANodeSettingsPanel = defineAsyncComponent(() => import("./ANodeSettingsPanel.vue"));
15
14
  import { META_FIELD_DEFINITIONS } from "../utils/metaFieldDefinitions";
16
15
  import { resolveDocType } from "../utils/docTypes";
16
+ const ANodeChatPanel = defineAsyncComponent(() => import("./chat/ANodeChatPanel.vue"));
17
+ const ANodeSettingsPanel = defineAsyncComponent(() => import("./ANodeSettingsPanel.vue"));
18
+ const ADocPanelServerSettings = defineAsyncComponent(() => import("./shell/ADocPanelServerSettings.vue"));
17
19
  const props = defineProps({
18
20
  nodeId: { type: [String, null], required: true },
19
21
  nodeLabel: { type: String, required: false },
@@ -21,7 +23,8 @@ const props = defineProps({
21
23
  docType: { type: String, required: false },
22
24
  labels: { type: Object, required: false },
23
25
  initialTab: { type: String, required: false, default: "editor" },
24
- tabs: { type: Array, required: false }
26
+ tabs: { type: Array, required: false },
27
+ docHref: { type: Function, required: false }
25
28
  });
26
29
  const emit = defineEmits(["close"]);
27
30
  const open = computed({
@@ -42,7 +45,12 @@ const entry = computed(() => {
42
45
  return treeMap.data[props.nodeId] ?? null;
43
46
  });
44
47
  const meta = computed(() => entry.value?.meta ?? {});
48
+ const resolvedLabel = computed(() => entry.value?.label || props.nodeLabel || "");
45
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
+ }
46
54
  function patchMeta(patch) {
47
55
  if (!props.nodeId) return;
48
56
  const e = treeMap.data[props.nodeId];
@@ -89,6 +97,7 @@ const showChatTab = computed(() => {
89
97
  const showEditorTab = computed(() => !props.tabs || props.tabs.includes("editor"));
90
98
  const showPropertiesTab = computed(() => !props.tabs || props.tabs.includes("properties"));
91
99
  const showSettingsTab = computed(() => !props.tabs || props.tabs.includes("settings"));
100
+ const showServerSettingsTab = computed(() => !!props.tabs && props.tabs.includes("serverSettings"));
92
101
  const _chat = useChat();
93
102
  const chatUnread = computed(() => {
94
103
  if (!props.nodeId) return 0;
@@ -181,55 +190,109 @@ const addFieldMenuItems = computed(
181
190
  <USlideover
182
191
  v-model:open="open"
183
192
  side="right"
184
- :title="nodeLabel || 'Document'"
193
+ :title="resolvedLabel || 'Document'"
194
+ :close="false"
185
195
  :content="{ style: resize.style.value }"
186
- :ui="{ body: 'p-0' }"
187
196
  @update:open="(v) => !v && emit('close')"
188
197
  >
189
- <template #header>
190
- <!-- Default header uses <ANodePanelHeader>. Consumers can replace the
191
- whole header via the #header slot, or replace individual pieces
192
- (presence, actions-right, etc.) by importing <ANodePanelHeader>
193
- and supplying its own slots. -->
194
- <slot
195
- name="header"
196
- :node-id="nodeId"
197
- :node-label="nodeLabel"
198
- :editor="tiptapEditor"
199
- :meta="meta"
200
- :doc-type-def="docTypeDef"
201
- >
202
- <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"
203
218
  :node-id="nodeId"
204
- :node-label="nodeLabel"
205
- :icon="meta.icon ?? docTypeDef.icon"
206
- :icon-color="meta.color"
207
- :editor="activeTab === 'editor' && !usesPageRenderer ? tiptapEditor : null"
208
- @expand="navigateTo(`/app/${nodeId}`)"
219
+ :node-label="resolvedLabel"
220
+ :editor="tiptapEditor"
221
+ :meta="meta"
222
+ :active-tab="activeTab"
223
+ :doc-type-def="docTypeDef"
209
224
  >
210
- <!-- Tabs inline with the title single-row header layout.
211
- Built-in tabs honour the `tabs` prop; plugin-registered tabs
212
- always render. -->
213
- <template #actions-left>
214
- <div class="flex items-center gap-1 ml-2">
215
- <UButton
216
- v-if="showEditorTab"
217
- icon="i-lucide-file-text"
218
- :label="locale.editorTab"
219
- size="xs"
220
- :color="activeTab === 'editor' ? 'primary' : 'neutral'"
221
- :variant="activeTab === 'editor' ? 'soft' : 'ghost'"
222
- @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"
223
256
  />
224
- <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
225
284
  v-if="showPropertiesTab"
226
- icon="i-lucide-sliders-horizontal"
227
- :label="locale.propertiesTab"
228
- size="xs"
229
- :color="activeTab === 'properties' ? 'primary' : 'neutral'"
230
- :variant="activeTab === 'properties' ? 'soft' : 'ghost'"
231
- @click="activeTab = 'properties'"
232
- />
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>
233
296
  <UChip
234
297
  v-if="showChatTab"
235
298
  :text="chatUnread > 0 && activeTab !== 'chat' ? String(chatUnread) : void 0"
@@ -237,479 +300,515 @@ const addFieldMenuItems = computed(
237
300
  size="lg"
238
301
  :show="chatUnread > 0 && activeTab !== 'chat'"
239
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' }"
240
321
  >
241
322
  <UButton
242
- icon="i-lucide-message-circle"
243
- label="Chat"
323
+ icon="i-lucide-settings-2"
244
324
  size="xs"
245
- :color="activeTab === 'chat' ? 'primary' : 'neutral'"
246
- :variant="activeTab === 'chat' ? 'soft' : 'ghost'"
247
- @click="activeTab = 'chat'"
325
+ :color="activeTab === 'settings' ? 'primary' : 'neutral'"
326
+ :variant="activeTab === 'settings' ? 'soft' : 'ghost'"
327
+ @click="activeTab = 'settings'"
248
328
  />
249
- </UChip>
250
- <UButton
251
- v-if="showSettingsTab"
252
- icon="i-lucide-settings-2"
253
- label="Settings"
254
- size="xs"
255
- :color="activeTab === 'settings' ? 'primary' : 'neutral'"
256
- :variant="activeTab === 'settings' ? 'soft' : 'ghost'"
257
- @click="activeTab = 'settings'"
258
- />
259
- <UButton
329
+ </UTooltip>
330
+ <UTooltip
331
+ v-if="showServerSettingsTab"
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
260
344
  v-for="slot in pluginSlots"
261
345
  :key="slot.id"
262
- :icon="slot.icon"
263
- :label="slot.label"
264
- size="xs"
265
- :color="activeTab === slot.id ? 'primary' : 'neutral'"
266
- :variant="activeTab === slot.id ? 'soft' : 'ghost'"
267
- @click="activeTab = slot.id"
268
- />
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>
269
357
  </div>
270
- </template>
271
- </ANodePanelHeader>
272
- </slot>
273
- </template>
274
-
275
- <template #body>
276
- <!-- Resize handle: left edge of slideover, desktop only -->
277
- <div
278
- class="absolute left-0 top-0 bottom-0 w-1.5 z-10 cursor-col-resize hidden sm:block group"
279
- :class="resize.isResizing.value ? 'bg-(--ui-primary)/20' : 'hover:bg-(--ui-primary)/10'"
280
- @mousedown.stop.prevent="resize.onMouseDown"
281
- @touchstart.stop.prevent="resize.onTouchStart"
282
- @dblclick="resize.onDoubleClick"
283
- />
284
-
285
- <!-- Editor tab — swaps to the registered page-type renderer when the
286
- doc has a non-'doc' type (kanban / table / calendar / etc.). -->
287
- <div
288
- v-show="activeTab === 'editor'"
289
- class="min-h-[60vh]"
290
- >
291
- <component
292
- :is="docTypeDef.component"
293
- v-if="nodeId && usesPageRenderer"
294
- :key="`${nodeId}-${currentType}`"
295
- :doc-id="nodeId"
296
- :doc-label="nodeLabel || ''"
297
- :child-provider="childProvider"
298
- />
299
- <AEditor
300
- v-else-if="nodeId"
301
- ref="editorRef"
302
- :doc-id="nodeId"
303
- :child-provider="childProvider"
304
- :parent-type="docType"
305
- :doc-meta="meta"
306
- class="h-full overflow-auto"
307
- @update-meta="patchMeta"
308
- />
309
- </div>
310
-
311
- <!-- Chat tab -->
312
- <div
313
- v-if="showChatTab"
314
- v-show="activeTab === 'chat'"
315
- class="min-h-[60vh] -mx-4 -mb-4 sm:-mx-6"
316
- >
317
- <ANodeChatPanel
318
- v-if="nodeId"
319
- :doc-id="nodeId"
320
- :label="nodeLabel || 'Chat'"
321
- />
322
- </div>
323
-
324
- <!-- Settings tab — snapshots wired by default; permissions and
325
- encryption are app-supplied via slots. -->
326
- <div
327
- v-if="showSettingsTab"
328
- v-show="activeTab === 'settings'"
329
- class="min-h-[60vh]"
330
- >
331
- <ANodeSettingsPanel
332
- v-if="nodeId"
333
- :doc-id="nodeId"
334
- :doc-type-key="currentType"
335
- :doc-meta="meta"
336
- @update-meta="patchMeta"
337
- />
338
- </div>
339
-
340
- <!-- Properties tab -->
341
- <div
342
- v-show="activeTab === 'properties'"
343
- class="space-y-4"
344
- >
345
- <!-- Document type -->
346
- <div class="space-y-1">
347
- <label class="text-xs font-medium text-muted">{{ locale.docType }}</label>
348
- <ADocTypeSelect
349
- :model-value="currentType"
350
- @update:model-value="patchType"
351
- />
352
- </div>
353
-
354
- <!-- Icon + Color (row) -->
355
- <div class="grid grid-cols-2 gap-3">
356
- <div class="space-y-1">
357
- <label class="text-xs font-medium text-muted">{{ locale.icon }}</label>
358
- <AIconPicker
359
- :model-value="meta.icon"
360
- @update:model-value="patchMeta({ icon: $event })"
361
- />
362
358
  </div>
363
- <div class="space-y-1">
364
- <label class="text-xs font-medium text-muted">{{ locale.color }}</label>
365
- <AColorPicker
366
- :field-key="`node-panel:meta:color:${props.nodeId}`"
367
- :model-value="meta.color"
368
- @update:model-value="patchMeta({ color: $event })"
369
- />
370
- </div>
371
- </div>
359
+ </slot>
372
360
 
373
- <!-- Status + Priority (row) -->
374
- <div class="grid grid-cols-2 gap-3">
375
- <div class="space-y-1">
376
- <label class="text-xs font-medium text-muted">{{ locale.status }}</label>
377
- <USelect
378
- :model-value="meta.status || 'none'"
379
- :items="statusOptions"
380
- value-key="value"
381
- label-key="label"
382
- size="sm"
383
- @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"
384
377
  />
385
- </div>
386
- <div class="space-y-1">
387
- <label class="text-xs font-medium text-muted">{{ locale.priority }}</label>
388
- <USelect
389
- :model-value="meta.priority ? String(meta.priority) : 'none'"
390
- :items="priorityOptions"
391
- value-key="value"
392
- label-key="label"
393
- size="sm"
394
- @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"
395
388
  />
396
389
  </div>
397
- </div>
398
-
399
- <!-- Rating -->
400
- <div class="space-y-1">
401
- <label class="text-xs font-medium text-muted">{{ locale.rating }}</label>
402
- <div class="flex items-center gap-1">
403
- <button
404
- v-for="n in 5"
405
- :key="n"
406
- type="button"
407
- class="transition-colors focus:outline-none"
408
- @click="patchMeta({ rating: meta.rating === n ? void 0 : n })"
409
- >
410
- <UIcon
411
- name="i-lucide-star"
412
- class="size-5"
413
- :class="(meta.rating ?? 0) >= n ? 'text-warning' : 'text-muted'"
414
- />
415
- </button>
416
- <button
417
- v-if="meta.rating"
418
- type="button"
419
- class="ml-1 text-xs text-muted hover:text-default"
420
- @click="patchMeta({ rating: void 0 })"
421
- >
422
- <UIcon
423
- name="i-lucide-x"
424
- class="size-3"
425
- />
426
- </button>
427
- </div>
428
- </div>
429
-
430
- <!-- URL -->
431
- <div class="space-y-1">
432
- <label class="text-xs font-medium text-muted">{{ locale.url }}</label>
433
- <UInput
434
- type="url"
435
- :model-value="meta.url ?? ''"
436
- placeholder="https://…"
437
- size="sm"
438
- @update:model-value="patchMeta({ url: $event || void 0 })"
439
- />
440
- </div>
441
390
 
442
- <!-- Dates -->
443
- <div class="space-y-1">
444
- <label class="text-xs font-medium text-muted">{{ locale.dates }}</label>
445
- <div class="grid grid-cols-2 gap-2">
446
- <UInput
447
- type="date"
448
- :model-value="meta.dateStart ?? ''"
449
- size="sm"
450
- @update:model-value="patchMeta({ dateStart: $event || void 0 })"
451
- />
452
- <UInput
453
- type="date"
454
- :model-value="meta.dateEnd ?? ''"
455
- size="sm"
456
- @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"
457
402
  />
458
403
  </div>
459
- </div>
460
404
 
461
- <!-- Tags -->
462
- <div class="space-y-1">
463
- <label class="text-xs font-medium text-muted">Tags</label>
464
- <div class="flex flex-wrap gap-1 mb-1">
465
- <UBadge
466
- v-for="tag in tags"
467
- :key="tag"
468
- variant="soft"
469
- class="cursor-pointer"
470
- @click="removeTag(tag)"
471
- >
472
- {{ tag }}
473
- <UIcon
474
- name="i-lucide-x"
475
- class="size-3 ml-1"
476
- />
477
- </UBadge>
478
- </div>
479
- <UInput
480
- v-model="tagInput"
481
- placeholder="Add tag…"
482
- size="sm"
483
- @keydown.enter.prevent="addTag"
484
- @keydown="($event2) => {
485
- if ($event2.key === ',') {
486
- $event2.preventDefault();
487
- addTag();
488
- }
489
- }"
490
- />
491
- </div>
492
-
493
- <!-- Cover image -->
494
- <div
495
- v-if="meta.coverUploadId || meta.coverDocId"
496
- class="space-y-1"
497
- >
498
- <label class="text-xs font-medium text-muted">{{ locale.cover }}</label>
499
- <div class="flex items-center gap-2 text-xs text-muted">
500
- <UIcon
501
- name="i-lucide-image"
502
- class="size-4 shrink-0"
503
- />
504
- <span class="truncate">{{ meta.coverUploadId ?? meta.coverDocId }}</span>
505
- <UButton
506
- icon="i-lucide-x"
507
- size="xs"
508
- variant="ghost"
509
- color="neutral"
510
- @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"
511
419
  />
512
420
  </div>
513
- </div>
514
421
 
515
- <!-- Custom fields -->
516
- <div class="space-y-2">
517
- <div class="flex items-center justify-between">
518
- <label class="text-xs font-medium text-muted">{{ locale.customFields }}</label>
519
- <UDropdownMenu :items="addFieldMenuItems">
520
- <UButton
521
- :label="locale.addField"
522
- icon="i-lucide-plus"
523
- size="xs"
524
- variant="ghost"
525
- color="neutral"
526
- />
527
- </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
+ />
528
432
  </div>
529
433
 
434
+ <!-- Properties tab -->
530
435
  <div
531
- v-for="field in customFields"
532
- :key="field.id"
533
- class="space-y-1"
436
+ v-show="activeTab === 'properties'"
437
+ class="flex-1 min-h-0 overflow-y-auto p-4 space-y-4"
534
438
  >
535
- <div class="flex items-center justify-between">
536
- <label class="text-xs font-medium text-muted">{{ field.label || field.type }}</label>
537
- <UButton
538
- icon="i-lucide-trash-2"
539
- size="xs"
540
- variant="ghost"
541
- color="neutral"
542
- class="opacity-40 hover:opacity-100"
543
- @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"
544
445
  />
545
446
  </div>
546
447
 
547
- <!-- Toggle -->
548
- <template v-if="field.type === 'toggle'">
549
- <USwitch
550
- :model-value="!!getCustomFieldValue(field)"
551
- @update:model-value="setCustomFieldValue(field, $event)"
552
- />
553
- </template>
554
-
555
- <!-- Number -->
556
- <template v-else-if="field.type === 'number'">
557
- <UInput
558
- type="number"
559
- :model-value="String(getCustomFieldValue(field) ?? '')"
560
- size="sm"
561
- @update:model-value="setCustomFieldValue(field, $event ? Number($event) : void 0)"
562
- />
563
- </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>
564
466
 
565
- <!-- Slider / Progress -->
566
- <template v-else-if="field.type === 'slider'">
567
- <div class="flex items-center gap-2">
568
- <USlider
569
- :model-value="Number(getCustomFieldValue(field) ?? field.min ?? 0)"
570
- :min="field.min ?? 0"
571
- :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"
476
+ size="sm"
477
+ @update:model-value="patchMeta({ status: $event === 'none' ? void 0 : $event })"
478
+ />
479
+ </div>
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"
572
487
  size="sm"
573
- class="flex-1"
574
- @update:model-value="setCustomFieldValue(field, $event)"
488
+ @update:model-value="patchMeta({ priority: $event === 'none' ? void 0 : Number($event) })"
575
489
  />
576
- <span class="text-xs text-muted w-8 text-right">{{ getCustomFieldValue(field) ?? field.min ?? 0 }}</span>
577
490
  </div>
578
- </template>
491
+ </div>
579
492
 
580
493
  <!-- Rating -->
581
- <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>
582
496
  <div class="flex items-center gap-1">
583
497
  <button
584
- v-for="n in field.max ?? 5"
498
+ v-for="n in 5"
585
499
  :key="n"
586
500
  type="button"
587
- class="focus:outline-none"
588
- @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 })"
589
503
  >
590
504
  <UIcon
591
505
  name="i-lucide-star"
592
- class="size-4"
593
- :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"
594
519
  />
595
520
  </button>
596
521
  </div>
597
- </template>
522
+ </div>
598
523
 
599
- <!-- Date / Datetime / Time (single) -->
600
- <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>
601
527
  <UInput
602
- :type="field.type === 'datetime' ? 'datetime-local' : field.type"
603
- :model-value="String(getCustomFieldValue(field) ?? '')"
528
+ type="url"
529
+ :model-value="meta.url ?? ''"
530
+ placeholder="https://…"
604
531
  size="sm"
605
- @update:model-value="setCustomFieldValue(field, $event || void 0)"
532
+ @update:model-value="patchMeta({ url: $event || void 0 })"
606
533
  />
607
- </template>
534
+ </div>
608
535
 
609
- <!-- Date range / Time range / Datetime range -->
610
- <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>
611
539
  <div class="grid grid-cols-2 gap-2">
612
540
  <UInput
613
- :type="field.type === 'datetimerange' ? 'datetime-local' : field.type === 'timerange' ? 'time' : 'date'"
614
- :model-value="field.startKey ? String(meta[field.startKey] ?? '') : ''"
541
+ type="date"
542
+ :model-value="meta.dateStart ?? ''"
615
543
  size="sm"
616
- @update:model-value="field.startKey && patchMeta({ [field.startKey]: $event || void 0 })"
544
+ @update:model-value="patchMeta({ dateStart: $event || void 0 })"
617
545
  />
618
546
  <UInput
619
- :type="field.type === 'datetimerange' ? 'datetime-local' : field.type === 'timerange' ? 'time' : 'date'"
620
- :model-value="field.endKey ? String(meta[field.endKey] ?? '') : ''"
547
+ type="date"
548
+ :model-value="meta.dateEnd ?? ''"
621
549
  size="sm"
622
- @update:model-value="field.endKey && patchMeta({ [field.endKey]: $event || void 0 })"
550
+ @update:model-value="patchMeta({ dateEnd: $event || void 0 })"
623
551
  />
624
552
  </div>
625
- </template>
626
-
627
- <!-- Textarea -->
628
- <template v-else-if="field.type === 'textarea'">
629
- <UTextarea
630
- :model-value="String(getCustomFieldValue(field) ?? '')"
631
- :rows="3"
632
- size="sm"
633
- @update:model-value="setCustomFieldValue(field, $event || void 0)"
634
- />
635
- </template>
636
-
637
- <!-- URL -->
638
- <template v-else-if="field.type === 'url'">
639
- <UInput
640
- type="url"
641
- :model-value="String(getCustomFieldValue(field) ?? '')"
642
- placeholder="https://…"
643
- size="sm"
644
- @update:model-value="setCustomFieldValue(field, $event || void 0)"
645
- />
646
- </template>
553
+ </div>
647
554
 
648
555
  <!-- Tags -->
649
- <template v-else-if="field.type === 'tags'">
650
- <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">
651
559
  <UBadge
652
- v-for="t in Array.isArray(getCustomFieldValue(field)) ? getCustomFieldValue(field) : []"
653
- :key="t"
560
+ v-for="tag in tags"
561
+ :key="tag"
654
562
  variant="soft"
655
563
  class="cursor-pointer"
656
- @click="setCustomFieldValue(field, getCustomFieldValue(field).filter((x) => x !== t))"
564
+ @click="removeTag(tag)"
657
565
  >
658
- {{ t }}<UIcon
566
+ {{ tag }}
567
+ <UIcon
659
568
  name="i-lucide-x"
660
569
  class="size-3 ml-1"
661
570
  />
662
571
  </UBadge>
663
572
  </div>
664
- </template>
665
-
666
- <!-- Icon -->
667
- <template v-else-if="field.type === 'icon'">
668
- <AIconPicker
669
- :model-value="String(getCustomFieldValue(field) ?? '')"
670
- @update:model-value="setCustomFieldValue(field, $event)"
671
- />
672
- </template>
673
-
674
- <!-- Color Preset / Color Picker -->
675
- <template v-else-if="['colorPreset', 'colorPicker'].includes(field.type)">
676
- <AColorPicker
677
- :field-key="`node-panel:custom:${props.nodeId}:${field.key}`"
678
- :model-value="String(getCustomFieldValue(field) ?? '')"
679
- @update:model-value="setCustomFieldValue(field, $event)"
680
- />
681
- </template>
682
-
683
- <!-- Fallback: text input -->
684
- <template v-else>
685
573
  <UInput
686
- :model-value="String(getCustomFieldValue(field) ?? '')"
574
+ v-model="tagInput"
575
+ placeholder="Add tag…"
687
576
  size="sm"
688
- @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
+ }"
689
584
  />
690
- </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>
691
787
  </div>
692
- </div>
693
- </div>
694
788
 
695
- <!-- Plugin slots -->
696
- <template
697
- v-for="slot in pluginSlots"
698
- :key="slot.id"
699
- >
700
- <div v-show="activeTab === slot.id">
701
- <component
702
- :is="slot.component"
703
- v-if="nodeId"
704
- :child-id="nodeId"
705
- :child-doc="childProvider?.document ?? null"
706
- :parent-doc-id="nodeId"
707
- :parent-type="docType"
708
- :meta="meta"
709
- :tree="tree"
710
- />
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>
711
810
  </div>
712
- </template>
811
+ </div>
713
812
  </template>
714
813
  </USlideover>
715
814
  </template>