@abraca/nuxt 1.6.0 → 1.8.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 (43) hide show
  1. package/dist/module.d.mts +6 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +16 -2
  4. package/dist/runtime/assets/sources.css +1 -0
  5. package/dist/runtime/components/ADocumentTree.d.vue.ts +11 -1
  6. package/dist/runtime/components/ADocumentTree.vue +13 -6
  7. package/dist/runtime/components/ADocumentTree.vue.d.ts +11 -1
  8. package/dist/runtime/components/renderers/AChecklistRenderer.vue +22 -4
  9. package/dist/runtime/components/renderers/ADashboardRenderer.vue +4 -2
  10. package/dist/runtime/components/renderers/AGalleryRenderer.vue +97 -70
  11. package/dist/runtime/components/renderers/AGraphRenderer.vue +209 -58
  12. package/dist/runtime/components/renderers/AKanbanRenderer.vue +145 -34
  13. package/dist/runtime/components/renderers/AMediaRenderer.vue +27 -17
  14. package/dist/runtime/components/renderers/AOutlineRenderer.vue +38 -23
  15. package/dist/runtime/components/renderers/ASlidesRenderer.d.vue.ts +21 -0
  16. package/dist/runtime/components/renderers/ASlidesRenderer.vue +591 -0
  17. package/dist/runtime/components/renderers/ASlidesRenderer.vue.d.ts +21 -0
  18. package/dist/runtime/components/renderers/ASpatialRenderer.vue +23 -0
  19. package/dist/runtime/components/renderers/ATableRenderer.vue +20 -391
  20. package/dist/runtime/components/renderers/gallery/AGalleryItemCard.d.vue.ts +40 -0
  21. package/dist/runtime/components/renderers/gallery/AGalleryItemCard.vue +227 -0
  22. package/dist/runtime/components/renderers/gallery/AGalleryItemCard.vue.d.ts +40 -0
  23. package/dist/runtime/components/renderers/spatial/SpatialTransformInputs.d.vue.ts +16 -0
  24. package/dist/runtime/components/renderers/spatial/SpatialTransformInputs.vue +66 -0
  25. package/dist/runtime/components/renderers/spatial/SpatialTransformInputs.vue.d.ts +16 -0
  26. package/dist/runtime/components/renderers/table/ATableFlatMode.d.vue.ts +2 -0
  27. package/dist/runtime/components/renderers/table/ATableFlatMode.vue +184 -21
  28. package/dist/runtime/components/renderers/table/ATableFlatMode.vue.d.ts +2 -0
  29. package/dist/runtime/components/renderers/table/ATableHierarchyMode.d.vue.ts +26 -0
  30. package/dist/runtime/components/renderers/table/ATableHierarchyMode.vue +662 -0
  31. package/dist/runtime/components/renderers/table/ATableHierarchyMode.vue.d.ts +26 -0
  32. package/dist/runtime/composables/useAwareness.js +14 -3
  33. package/dist/runtime/composables/useBackgroundSync.js +19 -1
  34. package/dist/runtime/composables/useFileIndex.js +38 -17
  35. package/dist/runtime/composables/useSearchIndex.js +41 -16
  36. package/dist/runtime/composables/useSlidesNavigation.d.ts +45 -0
  37. package/dist/runtime/composables/useSlidesNavigation.js +185 -0
  38. package/dist/runtime/composables/useYDoc.d.ts +1 -1
  39. package/dist/runtime/composables/useYDoc.js +47 -9
  40. package/dist/runtime/locale.d.ts +38 -0
  41. package/dist/runtime/locale.js +41 -3
  42. package/dist/runtime/utils/docTypes.js +17 -0
  43. package/package.json +3 -3
@@ -229,10 +229,86 @@ const labelScale = computed(() => {
229
229
  return vb.value.w / w;
230
230
  });
231
231
  const labelOpacity = computed(() => {
232
+ if (!showLabels.value) return 0;
232
233
  const pxPerUnit = (svgRef.value?.clientWidth ?? 1e3) / vb.value.w;
233
234
  const apparentR = 16 * pxPerUnit;
234
235
  return Math.max(0, Math.min(1, (apparentR - 8) / 8));
235
236
  });
237
+ const SPACING_PRESETS = {
238
+ compact: { charge: -70, linkDist: 50, centerStrength: 0.1, collideExtra: 2 },
239
+ default: { charge: -350, linkDist: 180, centerStrength: 0.03, collideExtra: 15 },
240
+ spacious: { charge: -700, linkDist: 320, centerStrength: 0.01, collideExtra: 30 }
241
+ };
242
+ const SPACING_CYCLE = ["compact", "default", "spacious"];
243
+ const EDGE_THICKNESS_SCALE = { thin: 0.6, normal: 1, thick: 1.8 };
244
+ const savedDocMeta = computed(() => tree.treeMap.data?.[props.docId]?.meta);
245
+ const spacingLevel = ref(savedDocMeta.value?.graphSpacing ?? "default");
246
+ watch(() => savedDocMeta.value?.graphSpacing, (v) => {
247
+ if (v && v !== spacingLevel.value) spacingLevel.value = v;
248
+ });
249
+ const showLabels = ref(savedDocMeta.value?.graphShowLabels ?? true);
250
+ watch(() => savedDocMeta.value?.graphShowLabels, (v) => {
251
+ if (typeof v === "boolean" && v !== showLabels.value) showLabels.value = v;
252
+ });
253
+ const showRefEdges = ref(savedDocMeta.value?.showRefEdges ?? true);
254
+ watch(() => savedDocMeta.value?.showRefEdges, (v) => {
255
+ if (typeof v === "boolean" && v !== showRefEdges.value) showRefEdges.value = v;
256
+ });
257
+ const edgeThickness = ref(savedDocMeta.value?.graphEdgeThickness ?? "normal");
258
+ watch(() => savedDocMeta.value?.graphEdgeThickness, (v) => {
259
+ if (v && v !== edgeThickness.value) edgeThickness.value = v;
260
+ });
261
+ const edgeScale = computed(() => EDGE_THICKNESS_SCALE[edgeThickness.value]);
262
+ const spacingIcon = computed(() => {
263
+ const icons = {
264
+ compact: "i-lucide-dot",
265
+ default: "i-lucide-circle-dot",
266
+ spacious: "i-lucide-target"
267
+ };
268
+ return icons[spacingLevel.value];
269
+ });
270
+ function applySpacingForces() {
271
+ if (!sim) return;
272
+ const p = SPACING_PRESETS[spacingLevel.value];
273
+ try {
274
+ sim.force("charge")?.strength(p.charge);
275
+ const linkForce = sim.force("link");
276
+ if (linkForce) linkForce.distance(p.linkDist).strength(0.5);
277
+ sim.force("x")?.strength(p.centerStrength);
278
+ sim.force("y")?.strength(p.centerStrength);
279
+ sim.force("collide")?.radius((n) => nodeRadius(n) + p.collideExtra);
280
+ } catch {
281
+ }
282
+ }
283
+ function setSpacing(level) {
284
+ spacingLevel.value = level;
285
+ tree.updateMeta(props.docId, { graphSpacing: level });
286
+ applySpacingForces();
287
+ if (sim) {
288
+ sim.alpha(0.5);
289
+ resumeSim();
290
+ }
291
+ }
292
+ function cycleSpacing() {
293
+ if (!props.editable) return;
294
+ const idx = SPACING_CYCLE.indexOf(spacingLevel.value);
295
+ setSpacing(SPACING_CYCLE[(idx + 1) % SPACING_CYCLE.length]);
296
+ }
297
+ function setShowLabels(v) {
298
+ showLabels.value = v;
299
+ tree.updateMeta(props.docId, { graphShowLabels: v });
300
+ }
301
+ function setShowRefEdges(v) {
302
+ showRefEdges.value = v;
303
+ tree.updateMeta(props.docId, { showRefEdges: v });
304
+ }
305
+ function setEdgeThickness(v) {
306
+ edgeThickness.value = v;
307
+ tree.updateMeta(props.docId, { graphEdgeThickness: v });
308
+ }
309
+ const viewSettingsActive = computed(
310
+ () => !showLabels.value || !showRefEdges.value || edgeThickness.value !== "normal"
311
+ );
236
312
  const simNodes = ref([]);
237
313
  const renderTick = ref(0);
238
314
  let sim = null;
@@ -278,9 +354,9 @@ const { pause: pauseSim, resume: resumeSim } = useRafFn(() => {
278
354
  }
279
355
  renderTick.value++;
280
356
  }, { immediate: false });
281
- function restartSim() {
357
+ function restartSim(alpha = 0.12) {
282
358
  if (!sim) return;
283
- sim.alpha(Math.max(sim.alpha(), 0.12));
359
+ sim.alpha(Math.max(sim.alpha(), alpha));
284
360
  resumeSim();
285
361
  }
286
362
  function nodeRadius(n) {
@@ -343,7 +419,8 @@ function buildSimulation(oldPos = /* @__PURE__ */ new Map()) {
343
419
  const edges = allEdges.value.filter((e) => nodeIds.has(e.source) && nodeIds.has(e.target)).map((e) => ({ source: e.source, target: e.target }));
344
420
  pauseSim();
345
421
  sim?.stop();
346
- sim = forceSimulation(nodes).force("charge", forceManyBody().strength(-120)).force("x", forceX(0).strength(0.06)).force("y", forceY(0).strength(0.06)).force("link", forceLink(edges).id((dd) => dd.id).distance(80).strength(0.5)).force("collide", forceCollide().radius((n) => nodeRadius(n) + 6).strength(0.85)).alphaDecay(0.025).velocityDecay(0.55).stop();
422
+ const p = SPACING_PRESETS[spacingLevel.value];
423
+ sim = forceSimulation(nodes).force("charge", forceManyBody().strength(p.charge)).force("x", forceX(0).strength(p.centerStrength)).force("y", forceY(0).strength(p.centerStrength)).force("link", forceLink(edges).id((dd) => dd.id).distance(p.linkDist).strength(0.5)).force("collide", forceCollide().radius((n) => nodeRadius(n) + p.collideExtra).strength(0.85)).alphaDecay(0.025).velocityDecay(0.55).stop();
347
424
  simNodes.value = nodes;
348
425
  const hasUnpositioned = nodes.some((n) => !getGraphPos(n.id) && !oldPos.has(n.id));
349
426
  if (hasUnpositioned) {
@@ -669,61 +746,131 @@ defineExpose({ connectedUsers });
669
746
  </div>
670
747
 
671
748
  <template v-else-if="d3">
672
- <!-- Toolbar -->
673
- <div class="absolute top-3 left-3 z-10 flex gap-1">
674
- <UButton
749
+ <!-- Floating toolbar (bottom-center) -->
750
+ <div class="graph-toolbar-bar">
751
+ <!-- Create -->
752
+ <div
675
753
  v-if="editable"
676
- icon="i-lucide-plus"
677
- size="xs"
678
- variant="soft"
679
- color="neutral"
680
- :label="locale.addNode"
681
- @click="addNodeAtCenter"
682
- />
683
- <UTooltip
684
- text="Zoom out"
685
- :content="{ side: 'bottom' }"
686
- >
687
- <UButton
688
- icon="i-lucide-minus"
689
- size="xs"
690
- variant="soft"
691
- color="neutral"
692
- @click="zoomOut"
693
- />
694
- </UTooltip>
695
- <UTooltip
696
- text="Zoom in"
697
- :content="{ side: 'bottom' }"
754
+ class="toolbar-group"
698
755
  >
699
- <UButton
700
- icon="i-lucide-zoom-in"
701
- size="xs"
702
- variant="soft"
703
- color="neutral"
704
- @click="zoomIn"
705
- />
706
- </UTooltip>
707
- <UTooltip
708
- text="Fit view"
709
- :content="{ side: 'bottom' }"
710
- >
711
- <UButton
712
- icon="i-lucide-focus"
713
- size="xs"
714
- variant="soft"
715
- color="neutral"
716
- @click="resetView"
717
- />
718
- </UTooltip>
719
- <UButton
720
- icon="i-lucide-wind"
721
- size="xs"
722
- variant="soft"
723
- color="neutral"
724
- label="Shake"
725
- @click="restartSim"
756
+ <UTooltip :text="locale.addNode">
757
+ <UButton
758
+ icon="i-lucide-plus"
759
+ size="sm"
760
+ variant="ghost"
761
+ color="neutral"
762
+ @click="addNodeAtCenter"
763
+ />
764
+ </UTooltip>
765
+ </div>
766
+ <div
767
+ v-if="editable"
768
+ class="toolbar-divider"
726
769
  />
770
+
771
+ <!-- View -->
772
+ <div class="toolbar-group">
773
+ <UTooltip :text="locale.zoomOut">
774
+ <UButton
775
+ icon="i-lucide-minus"
776
+ size="sm"
777
+ variant="ghost"
778
+ color="neutral"
779
+ @click="zoomOut"
780
+ />
781
+ </UTooltip>
782
+ <UTooltip :text="locale.zoomIn">
783
+ <UButton
784
+ icon="i-lucide-zoom-in"
785
+ size="sm"
786
+ variant="ghost"
787
+ color="neutral"
788
+ @click="zoomIn"
789
+ />
790
+ </UTooltip>
791
+ <UTooltip :text="locale.fitView">
792
+ <UButton
793
+ icon="i-lucide-focus"
794
+ size="sm"
795
+ variant="ghost"
796
+ color="neutral"
797
+ @click="resetView"
798
+ />
799
+ </UTooltip>
800
+ </div>
801
+ <div class="toolbar-divider" />
802
+
803
+ <!-- Simulation -->
804
+ <div class="toolbar-group">
805
+ <UTooltip :text="locale.shake">
806
+ <UButton
807
+ icon="i-lucide-wind"
808
+ size="sm"
809
+ variant="ghost"
810
+ color="neutral"
811
+ @click="() => restartSim(0.15)"
812
+ />
813
+ </UTooltip>
814
+ <UTooltip :text="`${locale.spacing}: ${spacingLevel}`">
815
+ <UButton
816
+ :icon="spacingIcon"
817
+ size="sm"
818
+ variant="ghost"
819
+ color="neutral"
820
+ :disabled="!editable"
821
+ @click="cycleSpacing"
822
+ />
823
+ </UTooltip>
824
+ </div>
825
+ <div class="toolbar-divider" />
826
+
827
+ <!-- View settings popover -->
828
+ <UPopover :content="{ side: 'top', align: 'end' }">
829
+ <UTooltip :text="locale.viewSettings">
830
+ <UButton
831
+ icon="i-lucide-sliders-horizontal"
832
+ size="sm"
833
+ :variant="viewSettingsActive ? 'soft' : 'ghost'"
834
+ :color="viewSettingsActive ? 'primary' : 'neutral'"
835
+ />
836
+ </UTooltip>
837
+ <template #content>
838
+ <div class="flex flex-col gap-2 p-2 w-56">
839
+ <div class="flex items-center justify-between">
840
+ <span class="text-xs text-(--ui-text-muted)">{{ locale.showLabels }}</span>
841
+ <USwitch
842
+ :model-value="showLabels"
843
+ size="xs"
844
+ @update:model-value="setShowLabels($event)"
845
+ />
846
+ </div>
847
+ <div class="flex items-center justify-between">
848
+ <span class="text-xs text-(--ui-text-muted)">{{ locale.showRefEdges }}</span>
849
+ <USwitch
850
+ :model-value="showRefEdges"
851
+ size="xs"
852
+ @update:model-value="setShowRefEdges($event)"
853
+ />
854
+ </div>
855
+ <div class="flex items-center justify-between">
856
+ <span class="text-xs text-(--ui-text-muted)">{{ locale.edgeThickness }}</span>
857
+ <div class="flex gap-0.5">
858
+ <UButton
859
+ v-for="th in ['thin', 'normal', 'thick']"
860
+ :key="th"
861
+ size="xs"
862
+ :variant="edgeThickness === th ? 'soft' : 'ghost'"
863
+ :color="edgeThickness === th ? 'primary' : 'neutral'"
864
+ class="capitalize text-[10px] px-1.5"
865
+ @click="setEdgeThickness(th)"
866
+ >
867
+ {{ locale[`edge${th.charAt(0).toUpperCase() + th.slice(1)}`] }}
868
+ </UButton>
869
+ </div>
870
+ </div>
871
+ </div>
872
+ </template>
873
+ </UPopover>
727
874
  </div>
728
875
 
729
876
  <!-- Empty state -->
@@ -815,7 +962,7 @@ defineExpose({ connectedUsers });
815
962
  :x2="link.target.x"
816
963
  :y2="link.target.y"
817
964
  stroke="var(--ui-border)"
818
- stroke-width="1.2"
965
+ :stroke-width="1.2 * edgeScale"
819
966
  opacity="0.6"
820
967
  />
821
968
  </g>
@@ -980,8 +1127,8 @@ defineExpose({ connectedUsers });
980
1127
  </svg>
981
1128
 
982
1129
  <!-- Hint -->
983
- <p class="absolute bottom-3 right-3 text-[10px] text-(--ui-text-muted) select-none pointer-events-none">
984
- Drag to reposition · Right-click to pin/create · Scroll to zoom · Click to open · Dbl-click canvas to create
1130
+ <p class="absolute top-3 right-3 text-[10px] text-(--ui-text-muted) select-none pointer-events-none">
1131
+ Drag · Right-click · Scroll · Click · Dbl-click
985
1132
  </p>
986
1133
 
987
1134
  <!-- Context menu -->
@@ -1028,3 +1175,7 @@ defineExpose({ connectedUsers });
1028
1175
  />
1029
1176
  </div>
1030
1177
  </template>
1178
+
1179
+ <style scoped>
1180
+ .graph-toolbar-bar{align-items:center;backdrop-filter:blur(8px);background:var(--ui-bg);border:1px solid var(--ui-border);border-radius:10px;bottom:2rem;display:flex;gap:3px;left:50%;padding:3px;position:absolute;transform:translateX(-50%);z-index:20}.toolbar-group{align-items:center;display:flex;gap:1px}.toolbar-divider{background:hsla(0,0%,100%,.15);flex-shrink:0;height:20px;margin:0 2px;width:1px}
1181
+ </style>
@@ -30,6 +30,11 @@ const {
30
30
  closePanel
31
31
  } = useNodePanel(childProviderRef);
32
32
  const columns = computed(() => tree.childrenOf(null));
33
+ const columnWidthClass = computed(() => {
34
+ const v = tree.treeMap.data?.[props.docId]?.meta?.kanbanColumnWidth;
35
+ const map = { narrow: "w-52", default: "w-64", wide: "w-80" };
36
+ return map[v ?? ""] ?? "w-64";
37
+ });
33
38
  function orderBetween(list, targetIdx) {
34
39
  const prev = list[targetIdx - 1];
35
40
  const next = list[targetIdx];
@@ -145,6 +150,48 @@ function remoteHovers(cardId) {
145
150
  }
146
151
  return result;
147
152
  }
153
+ function columnPresence(colId) {
154
+ const seen = /* @__PURE__ */ new Set();
155
+ const result = [];
156
+ for (const card of tree.childrenOf(colId)) {
157
+ for (const u of remoteHovers(card.id)) {
158
+ if (!seen.has(u.name)) {
159
+ seen.add(u.name);
160
+ result.push(u);
161
+ }
162
+ }
163
+ }
164
+ return result;
165
+ }
166
+ const PRIORITY_LABELS = ["", "Low", "Medium", "High", "Urgent"];
167
+ const PRIORITY_COLORS = {
168
+ 1: "neutral",
169
+ 2: "info",
170
+ 3: "warning",
171
+ 4: "error"
172
+ };
173
+ function cardPriority(card) {
174
+ const p = card.meta?.priority;
175
+ if (!p || p < 1 || p > 4) return null;
176
+ return { label: PRIORITY_LABELS[p], color: PRIORITY_COLORS[p] };
177
+ }
178
+ function cardDueDate(card) {
179
+ const raw = card.meta?.dateEnd ?? card.meta?.datetimeEnd;
180
+ if (!raw) return null;
181
+ try {
182
+ const d = new Date(raw);
183
+ return d.toLocaleDateString(void 0, { month: "short", day: "numeric" });
184
+ } catch {
185
+ return null;
186
+ }
187
+ }
188
+ function cardTags(card) {
189
+ const tags = card.meta?.tags;
190
+ return Array.isArray(tags) ? tags.slice(0, 2) : [];
191
+ }
192
+ function cardHasCover(card) {
193
+ return !!card.meta?.coverUploadId;
194
+ }
148
195
  function cardBorderStyle(card) {
149
196
  const hovers = remoteHovers(card.id);
150
197
  if (hovers.length > 0) return `border-left: 3px solid ${hovers[0].color}`;
@@ -350,8 +397,9 @@ defineExpose({ connectedUsers });
350
397
  v-for="col in columns"
351
398
  :key="col.id"
352
399
  :data-drag-id="col.id"
353
- class="flex-shrink-0 w-64 rounded-lg border transition-colors"
400
+ class="flex-shrink-0 rounded-lg border transition-colors"
354
401
  :class="[
402
+ columnWidthClass,
355
403
  dragOverColumnId === col.id ? 'border-(--ui-primary) bg-(--ui-primary)/5' : remoteDragColor(col.id) ? 'bg-(--ui-primary)/5' : 'border-(--ui-border) bg-(--ui-bg-elevated)',
356
404
  colDragOver === col.id ? 'border-l-4 border-l-(--ui-primary)' : '',
357
405
  colDragId === col.id ? 'opacity-40' : ''
@@ -395,6 +443,17 @@ defineExpose({ connectedUsers });
395
443
  variant="subtle"
396
444
  size="xs"
397
445
  />
446
+ <div
447
+ v-if="columnPresence(col.id).length"
448
+ class="flex -space-x-1"
449
+ >
450
+ <span
451
+ v-for="u in columnPresence(col.id).slice(0, 4)"
452
+ :key="u.name"
453
+ class="size-2 rounded-full ring-1 ring-(--ui-bg)"
454
+ :style="{ backgroundColor: u.color }"
455
+ />
456
+ </div>
398
457
  <UDropdownMenu
399
458
  v-if="editable"
400
459
  :items="colMenuItems(col)"
@@ -425,7 +484,7 @@ defineExpose({ connectedUsers });
425
484
  >
426
485
  <div
427
486
  :data-drag-id="card.id"
428
- class="group bg-(--ui-bg) rounded border px-3 py-2 text-sm cursor-pointer hover:border-(--ui-primary) transition-colors flex items-center justify-between gap-1 relative"
487
+ class="group bg-(--ui-bg) rounded border text-sm cursor-pointer hover:border-(--ui-primary) transition-colors relative overflow-hidden"
429
488
  :class="{
430
489
  'opacity-30': dragCardId === card.id,
431
490
  'border-t-2 border-t-(--ui-primary)': dragOverCardId === card.id,
@@ -440,25 +499,90 @@ defineExpose({ connectedUsers });
440
499
  @pointerleave="onCardPointerLeave"
441
500
  @click="openNode(card.id, card.label)"
442
501
  >
443
- <UInput
444
- v-if="renameId === card.id"
445
- v-model="renameValue"
446
- size="xs"
447
- variant="none"
448
- class="flex-1"
449
- autofocus
450
- @keydown.enter.stop="commitRename"
451
- @keydown.escape.stop="renameId = null"
452
- @blur="commitRename"
453
- @click.stop
502
+ <!-- Cover thumbnail (if set) -->
503
+ <AGalleryCoverImage
504
+ v-if="cardHasCover(card)"
505
+ :upload-id="card.meta.coverUploadId"
506
+ :doc-id="card.meta.coverDocId ?? card.id"
507
+ :mime-type="card.meta.coverMimeType"
508
+ class="w-full aspect-[3/2] object-cover bg-(--ui-bg-elevated)"
454
509
  />
455
- <span
456
- v-else
457
- class="truncate"
458
- @dblclick.stop="editable ? startRename(card.id, card.label) : void 0"
510
+
511
+ <!-- Title row -->
512
+ <div class="flex items-center justify-between gap-1 px-3 py-2">
513
+ <UInput
514
+ v-if="renameId === card.id"
515
+ v-model="renameValue"
516
+ size="xs"
517
+ variant="none"
518
+ class="flex-1"
519
+ autofocus
520
+ @keydown.enter.stop="commitRename"
521
+ @keydown.escape.stop="renameId = null"
522
+ @blur="commitRename"
523
+ @click.stop
524
+ />
525
+ <span
526
+ v-else
527
+ class="truncate flex items-center gap-1"
528
+ @dblclick.stop="editable ? startRename(card.id, card.label) : void 0"
529
+ >
530
+ <UIcon
531
+ v-if="card.meta?.icon"
532
+ :name="`i-lucide-${card.meta.icon}`"
533
+ class="size-3.5 shrink-0 text-(--ui-text-muted)"
534
+ />
535
+ <span class="truncate">{{ card.label }}</span>
536
+ </span>
537
+ <UDropdownMenu
538
+ v-if="editable"
539
+ :items="cardMenuItems(card, col.id)"
540
+ :content="{ align: 'start' }"
541
+ >
542
+ <UButton
543
+ icon="i-lucide-ellipsis"
544
+ size="xs"
545
+ variant="ghost"
546
+ color="neutral"
547
+ class="md:opacity-0 md:group-hover:opacity-100 shrink-0"
548
+ @click.stop
549
+ />
550
+ </UDropdownMenu>
551
+ </div>
552
+
553
+ <!-- Meta chips row -->
554
+ <div
555
+ v-if="cardPriority(card) || cardDueDate(card) || cardTags(card).length"
556
+ class="flex items-center flex-wrap gap-1 px-3 pb-2 -mt-1"
459
557
  >
460
- {{ card.label }}
461
- </span>
558
+ <UBadge
559
+ v-if="cardPriority(card)"
560
+ size="xs"
561
+ variant="subtle"
562
+ :color="cardPriority(card).color"
563
+ >
564
+ {{ cardPriority(card).label }}
565
+ </UBadge>
566
+ <UBadge
567
+ v-if="cardDueDate(card)"
568
+ size="xs"
569
+ variant="subtle"
570
+ color="neutral"
571
+ icon="i-lucide-calendar"
572
+ >
573
+ {{ cardDueDate(card) }}
574
+ </UBadge>
575
+ <UBadge
576
+ v-for="tag in cardTags(card)"
577
+ :key="tag"
578
+ size="xs"
579
+ variant="subtle"
580
+ color="neutral"
581
+ >
582
+ {{ tag }}
583
+ </UBadge>
584
+ </div>
585
+
462
586
  <!-- Remote hover name badge -->
463
587
  <span
464
588
  v-if="remoteHovers(card.id).length > 0"
@@ -467,20 +591,6 @@ defineExpose({ connectedUsers });
467
591
  >
468
592
  {{ remoteHovers(card.id)[0].name }}
469
593
  </span>
470
- <UDropdownMenu
471
- v-if="editable"
472
- :items="cardMenuItems(card, col.id)"
473
- :content="{ align: 'start' }"
474
- >
475
- <UButton
476
- icon="i-lucide-ellipsis"
477
- size="xs"
478
- variant="ghost"
479
- color="neutral"
480
- class="md:opacity-0 md:group-hover:opacity-100"
481
- @click.stop
482
- />
483
- </UDropdownMenu>
484
594
  </div>
485
595
  </UContextMenu>
486
596
  </TransitionGroup>
@@ -506,7 +616,8 @@ defineExpose({ connectedUsers });
506
616
  <button
507
617
  v-if="editable"
508
618
  :key="'__add-col__'"
509
- class="flex-shrink-0 w-64 h-10 rounded-lg border-2 border-dashed border-(--ui-border) hover:border-(--ui-primary) text-xs text-(--ui-text-muted) hover:text-(--ui-primary) transition-colors"
619
+ class="flex-shrink-0 h-10 rounded-lg border-2 border-dashed border-(--ui-border) hover:border-(--ui-primary) text-xs text-(--ui-text-muted) hover:text-(--ui-primary) transition-colors"
620
+ :class="columnWidthClass"
510
621
  @click="addColumn"
511
622
  >
512
623
  + {{ locale.addColumn }}
@@ -5,6 +5,8 @@ import { useRendererBase } from "../../composables/useRendererBase";
5
5
  import { useNodePanel } from "../../composables/useNodePanel";
6
6
  import { useMediaExtractor } from "../../composables/useMediaExtractor";
7
7
  import { useMediaPlayback } from "../../composables/useMediaPlayback";
8
+ import { useFileBlobStore } from "../../composables/useFileBlobStore";
9
+ import { useSyncedMap } from "../../composables/useYDoc";
8
10
  import MediaPlaylist from "./media/MediaPlaylist.vue";
9
11
  import MediaTransportBar from "./media/MediaTransportBar.vue";
10
12
  import MediaSyncBar from "./media/MediaSyncBar.vue";
@@ -39,20 +41,28 @@ const { mediaItems, groupedMedia } = useMediaExtractor(
39
41
  );
40
42
  const mediaElementRef = shallowRef(null);
41
43
  const myClientId = computed(() => props.childProvider?.awareness?.clientID ?? 0);
42
- const getBlobUrl = async (_docId, _uploadId) => null;
43
- const useSyncedMapStub = (doc, mapName) => {
44
- const shared = globalThis.__ABRACA_SHARED__ ?? {};
45
- if (shared.useSyncedMap) return shared.useSyncedMap(doc, mapName);
46
- return {
47
- data: {},
48
- yMap: ref(null),
49
- set: (_k, _v) => {
50
- },
51
- lastUpdateLocal: ref(false)
52
- };
53
- };
54
- const repeatMode = ref("off");
55
- const shuffleEnabled = ref(false);
44
+ const { getBlobUrl } = useFileBlobStore();
45
+ const savedDocMeta = computed(() => tree.treeMap.data?.[props.docId]?.meta);
46
+ const repeatMode = ref(savedDocMeta.value?.mediaRepeat ?? "off");
47
+ watch(() => savedDocMeta.value?.mediaRepeat, (v) => {
48
+ if (v && v !== repeatMode.value) repeatMode.value = v;
49
+ });
50
+ function setRepeatMode(v) {
51
+ repeatMode.value = v;
52
+ tree.updateMeta(props.docId, { mediaRepeat: v });
53
+ }
54
+ function cycleRepeatMode() {
55
+ setRepeatMode(repeatMode.value === "off" ? "all" : repeatMode.value === "all" ? "one" : "off");
56
+ }
57
+ const shuffleEnabled = ref(savedDocMeta.value?.mediaShuffle ?? false);
58
+ watch(() => savedDocMeta.value?.mediaShuffle, (v) => {
59
+ if (typeof v === "boolean" && v !== shuffleEnabled.value) shuffleEnabled.value = v;
60
+ });
61
+ function toggleShuffle() {
62
+ const next = !shuffleEnabled.value;
63
+ shuffleEnabled.value = next;
64
+ tree.updateMeta(props.docId, { mediaShuffle: next });
65
+ }
56
66
  const playback = useMediaPlayback({
57
67
  mediaElementRef,
58
68
  childDoc,
@@ -60,7 +70,7 @@ const playback = useMediaPlayback({
60
70
  repeatMode,
61
71
  shuffle: shuffleEnabled,
62
72
  getBlobUrl,
63
- useSyncedMap: useSyncedMapStub,
73
+ useSyncedMap,
64
74
  onTrackChange: (track) => {
65
75
  setLocalState({ "media:trackId": track.id });
66
76
  }
@@ -146,14 +156,14 @@ defineExpose({ connectedUsers });
146
156
  variant="ghost"
147
157
  :color="shuffleEnabled ? 'primary' : 'neutral'"
148
158
  size="xs"
149
- @click="shuffleEnabled = !shuffleEnabled"
159
+ @click="toggleShuffle"
150
160
  />
151
161
  <UButton
152
162
  :icon="repeatMode === 'one' ? 'i-lucide-repeat-1' : 'i-lucide-repeat'"
153
163
  variant="ghost"
154
164
  :color="repeatMode !== 'off' ? 'primary' : 'neutral'"
155
165
  size="xs"
156
- @click="repeatMode = repeatMode === 'off' ? 'all' : repeatMode === 'all' ? 'one' : 'off'"
166
+ @click="cycleRepeatMode"
157
167
  />
158
168
  </div>
159
169
  </div>