@abraca/nuxt 0.1.1 → 0.3.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.
- package/dist/module.d.mts +46 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +95 -2
- package/dist/runtime/assets/editor.css +1 -0
- package/dist/runtime/components/ACommandPalette.vue +4 -1
- package/dist/runtime/components/ADocRenderer.d.vue.ts +29 -0
- package/dist/runtime/components/ADocRenderer.vue +99 -0
- package/dist/runtime/components/ADocRenderer.vue.d.ts +29 -0
- package/dist/runtime/components/ADocTypeSelect.vue +4 -1
- package/dist/runtime/components/ADocumentTree.vue +78 -19
- package/dist/runtime/components/AEditor.d.vue.ts +9 -4
- package/dist/runtime/components/AEditor.vue +102 -7
- package/dist/runtime/components/AEditor.vue.d.ts +9 -4
- package/dist/runtime/components/AFloatingWindow.vue +1 -1
- package/dist/runtime/components/AIconPicker.vue +8 -2
- package/dist/runtime/components/ANodePanel.vue +100 -61
- package/dist/runtime/components/ANotifications.vue +35 -8
- package/dist/runtime/components/APermissionGuard.vue +3 -1
- package/dist/runtime/components/APresence.vue +14 -3
- package/dist/runtime/components/AProvider.vue +7 -1
- package/dist/runtime/components/AVoiceBar.vue +57 -15
- package/dist/runtime/components/AVoiceTile.vue +4 -1
- package/dist/runtime/components/AWindowLayer.vue +1 -1
- package/dist/runtime/components/aware/AArea.vue +1 -1
- package/dist/runtime/components/aware/AAvatar.vue +85 -16
- package/dist/runtime/components/aware/AButton.vue +5 -1
- package/dist/runtime/components/aware/ACursorLabel.vue +5 -1
- package/dist/runtime/components/aware/ADocBadge.vue +4 -1
- package/dist/runtime/components/aware/AFacepile.vue +13 -3
- package/dist/runtime/components/aware/AInput.vue +5 -1
- package/dist/runtime/components/aware/ATextarea.vue +5 -1
- package/dist/runtime/components/aware/AUserList.vue +8 -2
- package/dist/runtime/components/renderers/ACalendarRenderer.d.vue.ts +12 -1
- package/dist/runtime/components/renderers/ACalendarRenderer.vue +388 -114
- package/dist/runtime/components/renderers/ACalendarRenderer.vue.d.ts +12 -1
- package/dist/runtime/components/renderers/ACallRenderer.d.vue.ts +13 -0
- package/dist/runtime/components/renderers/ACallRenderer.vue +169 -0
- package/dist/runtime/components/renderers/ACallRenderer.vue.d.ts +13 -0
- package/dist/runtime/components/renderers/AChecklistRenderer.d.vue.ts +19 -0
- package/dist/runtime/components/renderers/AChecklistRenderer.vue +581 -0
- package/dist/runtime/components/renderers/AChecklistRenderer.vue.d.ts +19 -0
- package/dist/runtime/components/renderers/ADashboardRenderer.d.vue.ts +19 -0
- package/dist/runtime/components/renderers/ADashboardRenderer.vue +1372 -0
- package/dist/runtime/components/renderers/ADashboardRenderer.vue.d.ts +19 -0
- package/dist/runtime/components/renderers/AGalleryCoverImage.d.vue.ts +8 -0
- package/dist/runtime/components/renderers/AGalleryCoverImage.vue +60 -0
- package/dist/runtime/components/renderers/AGalleryCoverImage.vue.d.ts +8 -0
- package/dist/runtime/components/renderers/AGalleryRenderer.d.vue.ts +12 -1
- package/dist/runtime/components/renderers/AGalleryRenderer.vue +221 -55
- package/dist/runtime/components/renderers/AGalleryRenderer.vue.d.ts +12 -1
- package/dist/runtime/components/renderers/AGraphRenderer.d.vue.ts +19 -0
- package/dist/runtime/components/renderers/AGraphRenderer.vue +1027 -0
- package/dist/runtime/components/renderers/AGraphRenderer.vue.d.ts +19 -0
- package/dist/runtime/components/renderers/AKanbanRenderer.d.vue.ts +13 -1
- package/dist/runtime/components/renderers/AKanbanRenderer.vue +474 -140
- package/dist/runtime/components/renderers/AKanbanRenderer.vue.d.ts +13 -1
- package/dist/runtime/components/renderers/AMapRenderer.d.vue.ts +19 -0
- package/dist/runtime/components/renderers/AMapRenderer.vue +1622 -0
- package/dist/runtime/components/renderers/AMapRenderer.vue.d.ts +19 -0
- package/dist/runtime/components/renderers/AOutlineRenderer.d.vue.ts +12 -1
- package/dist/runtime/components/renderers/AOutlineRenderer.vue +294 -134
- package/dist/runtime/components/renderers/AOutlineRenderer.vue.d.ts +12 -1
- package/dist/runtime/components/renderers/ATableRenderer.d.vue.ts +12 -1
- package/dist/runtime/components/renderers/ATableRenderer.vue +437 -145
- package/dist/runtime/components/renderers/ATableRenderer.vue.d.ts +12 -1
- package/dist/runtime/components/renderers/ATimelineRenderer.d.vue.ts +19 -0
- package/dist/runtime/components/renderers/ATimelineRenderer.vue +446 -0
- package/dist/runtime/components/renderers/ATimelineRenderer.vue.d.ts +19 -0
- package/dist/runtime/composables/useAwareness.js +5 -0
- package/dist/runtime/composables/useBroadcastSync.d.ts +18 -0
- package/dist/runtime/composables/useBroadcastSync.js +26 -0
- package/dist/runtime/composables/useChat.js +4 -2
- package/dist/runtime/composables/useChatUsers.js +2 -1
- package/dist/runtime/composables/useCommandPalette.js +62 -3
- package/dist/runtime/composables/useConnectionStatus.js +7 -0
- package/dist/runtime/composables/useDevicePairing.d.ts +58 -0
- package/dist/runtime/composables/useDevicePairing.js +108 -0
- package/dist/runtime/composables/useDocExport.d.ts +5 -0
- package/dist/runtime/composables/useDocExport.js +2 -2
- package/dist/runtime/composables/useDocImport.js +4 -3
- package/dist/runtime/composables/useDocSeo.d.ts +20 -0
- package/dist/runtime/composables/useDocSeo.js +44 -0
- package/dist/runtime/composables/useDocSlugs.d.ts +7 -0
- package/dist/runtime/composables/useDocSlugs.js +20 -0
- package/dist/runtime/composables/useDocTree.d.ts +34 -0
- package/dist/runtime/composables/useDocTree.js +35 -0
- package/dist/runtime/composables/useEditorDragHandle.js +2 -1
- package/dist/runtime/composables/useEditorMentions.js +4 -2
- package/dist/runtime/composables/useEditorSuggestions.d.ts +1 -0
- package/dist/runtime/composables/useEditorSuggestions.js +9 -2
- package/dist/runtime/composables/useEditorToolbar.js +2 -1
- package/dist/runtime/composables/useFileIndex.js +2 -1
- package/dist/runtime/composables/useFileTransfer.d.ts +112 -0
- package/dist/runtime/composables/useFileTransfer.js +171 -0
- package/dist/runtime/composables/useFollowUser.js +2 -1
- package/dist/runtime/composables/useInvites.d.ts +56 -0
- package/dist/runtime/composables/useInvites.js +77 -0
- package/dist/runtime/composables/useNodePanel.d.ts +14 -0
- package/dist/runtime/composables/useNodePanel.js +52 -0
- package/dist/runtime/composables/useNotifications.js +4 -2
- package/dist/runtime/composables/usePasskeyAccounts.js +4 -2
- package/dist/runtime/composables/useSearchIndex.d.ts +1 -0
- package/dist/runtime/composables/useSearchIndex.js +13 -5
- package/dist/runtime/composables/useServerInfo.d.ts +31 -0
- package/dist/runtime/composables/useServerInfo.js +80 -0
- package/dist/runtime/composables/useSlugRoute.d.ts +6 -0
- package/dist/runtime/composables/useSlugRoute.js +19 -0
- package/dist/runtime/composables/useSpaces.d.ts +37 -0
- package/dist/runtime/composables/useSpaces.js +83 -0
- package/dist/runtime/composables/useTouchDrag.d.ts +34 -0
- package/dist/runtime/composables/useTouchDrag.js +191 -0
- package/dist/runtime/composables/useTrash.d.ts +1 -1
- package/dist/runtime/composables/useTrash.js +6 -3
- package/dist/runtime/composables/useWebRTC.d.ts +50 -0
- package/dist/runtime/composables/useWebRTC.js +177 -0
- package/dist/runtime/extensions/meta-field.d.ts +4 -1
- package/dist/runtime/extensions/steps.js +1 -1
- package/dist/runtime/extensions/views/AccordionItemView.vue +13 -3
- package/dist/runtime/extensions/views/AccordionView.vue +4 -1
- package/dist/runtime/extensions/views/BadgeView.vue +11 -2
- package/dist/runtime/extensions/views/CalloutView.vue +4 -1
- package/dist/runtime/extensions/views/CardGroupView.vue +4 -1
- package/dist/runtime/extensions/views/CardView.vue +17 -3
- package/dist/runtime/extensions/views/CodeGroupView.vue +4 -1
- package/dist/runtime/extensions/views/CollapsibleView.vue +8 -2
- package/dist/runtime/extensions/views/FileNodeView.vue +32 -8
- package/dist/runtime/extensions/views/KbdView.vue +8 -2
- package/dist/runtime/extensions/views/MetaFieldView.vue +208 -46
- package/dist/runtime/extensions/views/ProseIconView.vue +8 -2
- package/dist/runtime/extensions/views/TabsView.vue +17 -4
- package/dist/runtime/locale.d.ts +71 -0
- package/dist/runtime/locale.js +71 -0
- package/dist/runtime/plugin-abracadabra.client.js +29 -3
- package/dist/runtime/plugin-abracadabra.server.js +2 -0
- package/dist/runtime/server/api/_abracadabra/render/[docId].get.d.ts +1 -1
- package/dist/runtime/server/api/_abracadabra/render/[docId].get.js +29 -4
- package/dist/runtime/server/api/_abracadabra/resolve/[...slug].get.d.ts +2 -0
- package/dist/runtime/server/api/_abracadabra/resolve/[...slug].get.js +43 -0
- package/dist/runtime/server/api/_abracadabra/slugs.get.d.ts +2 -0
- package/dist/runtime/server/api/_abracadabra/slugs.get.js +7 -0
- package/dist/runtime/server/plugins/abracadabra-service.js +10 -5
- package/dist/runtime/server/runners/doc-tree-cache.js +4 -0
- package/dist/runtime/server/utils/slugMap.d.ts +32 -0
- package/dist/runtime/server/utils/slugMap.js +58 -0
- package/dist/runtime/types.d.ts +1 -0
- package/dist/runtime/utils/docTypes.d.ts +29 -1
- package/dist/runtime/utils/docTypes.js +129 -1
- package/dist/runtime/utils/markdownToYjs.js +2 -2
- package/dist/runtime/utils/sdkRef.d.ts +2 -0
- package/dist/runtime/utils/sdkRef.js +7 -0
- package/dist/runtime/utils/slugify.d.ts +40 -0
- package/dist/runtime/utils/slugify.js +36 -0
- package/dist/types.d.mts +6 -0
- package/package.json +32 -19
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, watch, onBeforeUnmount } from "vue";
|
|
3
|
+
import { useRendererBase } from "../../composables/useRendererBase";
|
|
4
|
+
import { useTouchDrag } from "../../composables/useTouchDrag";
|
|
5
|
+
import { useNodePanel } from "../../composables/useNodePanel";
|
|
6
|
+
import { DEFAULT_LOCALE } from "../../locale";
|
|
7
|
+
const props = defineProps({
|
|
8
|
+
docId: { type: String, required: true },
|
|
9
|
+
childProvider: { type: null, required: true },
|
|
10
|
+
docLabel: { type: String, required: true },
|
|
11
|
+
pageTypes: { type: Array, required: false },
|
|
12
|
+
labels: { type: Object, required: false },
|
|
13
|
+
editable: { type: Boolean, required: false, default: true }
|
|
14
|
+
});
|
|
15
|
+
const config = useRuntimeConfig();
|
|
16
|
+
const locale = computed(() => ({
|
|
17
|
+
...DEFAULT_LOCALE.renderers.timeline,
|
|
18
|
+
...config.public?.abracadabra?.locale?.renderers?.timeline ?? {},
|
|
19
|
+
...props.labels ?? {}
|
|
20
|
+
}));
|
|
21
|
+
const { tree, childDoc, childProviderRef, states, setLocalState, connectedUsers } = useRendererBase(props);
|
|
22
|
+
const {
|
|
23
|
+
openNodeId,
|
|
24
|
+
openNodeLabel,
|
|
25
|
+
openNodeProvider,
|
|
26
|
+
openNode,
|
|
27
|
+
closePanel
|
|
28
|
+
} = useNodePanel(childProviderRef);
|
|
29
|
+
const myClientId = computed(() => props.childProvider?.awareness?.clientID ?? 0);
|
|
30
|
+
const zoomLevel = ref("month");
|
|
31
|
+
const ZOOM_CONFIG = {
|
|
32
|
+
week: { totalDays: 14, pixelsPerDay: 40, stepDays: 1 },
|
|
33
|
+
month: { totalDays: 60, pixelsPerDay: 14, stepDays: 7 },
|
|
34
|
+
quarter: { totalDays: 180, pixelsPerDay: 4, stepDays: 30 }
|
|
35
|
+
};
|
|
36
|
+
const cfg = computed(() => ZOOM_CONFIG[zoomLevel.value]);
|
|
37
|
+
const epics = computed(() => tree.childrenOf(null));
|
|
38
|
+
function addEpic() {
|
|
39
|
+
if (!props.editable) return;
|
|
40
|
+
const id = tree.createChild(null, locale.value.untitled);
|
|
41
|
+
const now = /* @__PURE__ */ new Date();
|
|
42
|
+
const end = new Date(now);
|
|
43
|
+
end.setDate(end.getDate() + 14);
|
|
44
|
+
tree.updateMeta(id, {
|
|
45
|
+
dateStart: now.toISOString().slice(0, 10),
|
|
46
|
+
dateEnd: end.toISOString().slice(0, 10),
|
|
47
|
+
taskProgress: 0,
|
|
48
|
+
color: "#6366f1"
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function addTask(epicId) {
|
|
52
|
+
if (!props.editable) return;
|
|
53
|
+
const id = tree.createChild(epicId, locale.value.untitled);
|
|
54
|
+
const now = /* @__PURE__ */ new Date();
|
|
55
|
+
const end = new Date(now);
|
|
56
|
+
end.setDate(end.getDate() + 7);
|
|
57
|
+
tree.updateMeta(id, {
|
|
58
|
+
dateStart: now.toISOString().slice(0, 10),
|
|
59
|
+
dateEnd: end.toISOString().slice(0, 10),
|
|
60
|
+
taskProgress: 0,
|
|
61
|
+
color: "#818cf8"
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const startOfAxis = computed(() => {
|
|
65
|
+
const d = /* @__PURE__ */ new Date();
|
|
66
|
+
d.setDate(1);
|
|
67
|
+
d.setMonth(d.getMonth() - 1);
|
|
68
|
+
return d;
|
|
69
|
+
});
|
|
70
|
+
const axisWidth = computed(() => cfg.value.totalDays * cfg.value.pixelsPerDay);
|
|
71
|
+
function dateToOffset(dateStr) {
|
|
72
|
+
const d = new Date(dateStr);
|
|
73
|
+
const diff = Math.floor(
|
|
74
|
+
(d.getTime() - startOfAxis.value.getTime()) / (1e3 * 60 * 60 * 24)
|
|
75
|
+
);
|
|
76
|
+
return diff * cfg.value.pixelsPerDay;
|
|
77
|
+
}
|
|
78
|
+
function spanWidth(start, end) {
|
|
79
|
+
const s = new Date(start);
|
|
80
|
+
const e = new Date(end);
|
|
81
|
+
const days = Math.max(
|
|
82
|
+
1,
|
|
83
|
+
Math.ceil((e.getTime() - s.getTime()) / (1e3 * 60 * 60 * 24))
|
|
84
|
+
);
|
|
85
|
+
return days * cfg.value.pixelsPerDay;
|
|
86
|
+
}
|
|
87
|
+
const axisLabels = computed(() => {
|
|
88
|
+
const labels = [];
|
|
89
|
+
for (let i = 0; i < cfg.value.totalDays; i += cfg.value.stepDays) {
|
|
90
|
+
const d = new Date(startOfAxis.value);
|
|
91
|
+
d.setDate(d.getDate() + i);
|
|
92
|
+
const fmt = zoomLevel.value === "week" ? { weekday: "short", day: "numeric" } : { month: "short", day: "numeric" };
|
|
93
|
+
labels.push({
|
|
94
|
+
label: d.toLocaleDateString("en-US", fmt),
|
|
95
|
+
offset: i * cfg.value.pixelsPerDay
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return labels;
|
|
99
|
+
});
|
|
100
|
+
const todayOffset = computed(() => dateToOffset((/* @__PURE__ */ new Date()).toISOString().slice(0, 10)));
|
|
101
|
+
function orderBetween(list, targetIdx) {
|
|
102
|
+
const prev = list[targetIdx - 1];
|
|
103
|
+
const next = list[targetIdx];
|
|
104
|
+
if (!prev && !next) return Date.now();
|
|
105
|
+
if (!prev) return next.order - 1e3;
|
|
106
|
+
if (!next) return prev.order + 1e3;
|
|
107
|
+
return (prev.order + next.order) / 2;
|
|
108
|
+
}
|
|
109
|
+
const {
|
|
110
|
+
dragId: epicDragId,
|
|
111
|
+
dragOverId: epicDragOver,
|
|
112
|
+
handlePointerDown: handleEpicPointerDown
|
|
113
|
+
} = useTouchDrag({
|
|
114
|
+
onDrop: (srcId, targetId) => {
|
|
115
|
+
if (!props.editable) return;
|
|
116
|
+
const targetIdx = epics.value.findIndex((ep) => ep.id === targetId);
|
|
117
|
+
const newOrder = orderBetween(epics.value, targetIdx);
|
|
118
|
+
tree.moveEntry(srcId, null, newOrder);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
function taskFocusers(taskId) {
|
|
122
|
+
return states.value.filter(
|
|
123
|
+
(s) => s.clientId !== myClientId.value && s["timeline:focused"] === taskId
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
function focusTask(id) {
|
|
127
|
+
setLocalState({ "timeline:focused": id });
|
|
128
|
+
}
|
|
129
|
+
function clearFocus() {
|
|
130
|
+
setLocalState({ "timeline:focused": null });
|
|
131
|
+
}
|
|
132
|
+
const barDragId = ref(null);
|
|
133
|
+
const barDragStart = ref({ clientX: 0, origStart: "", origEnd: "" });
|
|
134
|
+
const barDidMove = ref(false);
|
|
135
|
+
const barHoverEpicId = ref(null);
|
|
136
|
+
let lastDaysShifted = 0;
|
|
137
|
+
function onBarPointerDown(e, id) {
|
|
138
|
+
if (!props.editable) return;
|
|
139
|
+
if (e.button !== 0) return;
|
|
140
|
+
e.stopPropagation();
|
|
141
|
+
const entry = tree.entries.value.find((en) => en.id === id);
|
|
142
|
+
const meta = entry?.meta;
|
|
143
|
+
if (!meta?.dateStart || !meta?.dateEnd) return;
|
|
144
|
+
barDragId.value = id;
|
|
145
|
+
barDidMove.value = false;
|
|
146
|
+
barHoverEpicId.value = null;
|
|
147
|
+
lastDaysShifted = 0;
|
|
148
|
+
barDragStart.value = {
|
|
149
|
+
clientX: e.clientX,
|
|
150
|
+
origStart: meta.dateStart,
|
|
151
|
+
origEnd: meta.dateEnd
|
|
152
|
+
};
|
|
153
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
154
|
+
}
|
|
155
|
+
function onBarPointerMove(e, id) {
|
|
156
|
+
if (barDragId.value !== id) return;
|
|
157
|
+
const entry = tree.entries.value.find((en) => en.id === id);
|
|
158
|
+
if (!entry?.meta?.dateStart) return;
|
|
159
|
+
const dx = e.clientX - barDragStart.value.clientX;
|
|
160
|
+
if (Math.abs(dx) > 4) barDidMove.value = true;
|
|
161
|
+
const els = document.elementsFromPoint(e.clientX, e.clientY);
|
|
162
|
+
const epicEl = els.find((el) => el.dataset?.epicId);
|
|
163
|
+
barHoverEpicId.value = epicEl?.dataset.epicId ?? null;
|
|
164
|
+
const daysShifted = Math.round(dx / cfg.value.pixelsPerDay);
|
|
165
|
+
if (daysShifted === lastDaysShifted) return;
|
|
166
|
+
lastDaysShifted = daysShifted;
|
|
167
|
+
const MS = 24 * 60 * 60 * 1e3;
|
|
168
|
+
const s0 = new Date(barDragStart.value.origStart).getTime();
|
|
169
|
+
const e0 = new Date(barDragStart.value.origEnd).getTime();
|
|
170
|
+
tree.updateMeta(id, {
|
|
171
|
+
dateStart: new Date(s0 + daysShifted * MS).toISOString().slice(0, 10),
|
|
172
|
+
dateEnd: new Date(e0 + daysShifted * MS).toISOString().slice(0, 10)
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function onBarPointerUp(id, epicId) {
|
|
176
|
+
if (barDragId.value !== id) return;
|
|
177
|
+
barDragId.value = null;
|
|
178
|
+
if (epicId && barHoverEpicId.value && barHoverEpicId.value !== epicId) {
|
|
179
|
+
tree.moveEntry(id, barHoverEpicId.value, Date.now());
|
|
180
|
+
}
|
|
181
|
+
barHoverEpicId.value = null;
|
|
182
|
+
}
|
|
183
|
+
function onBarClick(id, label) {
|
|
184
|
+
if (barDidMove.value) return;
|
|
185
|
+
openNode(id, label);
|
|
186
|
+
}
|
|
187
|
+
watch(childDoc, (doc) => {
|
|
188
|
+
if (!doc) return;
|
|
189
|
+
const legacyMap = doc.getMap("task-dates");
|
|
190
|
+
if (legacyMap.size === 0) return;
|
|
191
|
+
legacyMap.forEach((val, id) => {
|
|
192
|
+
if (!tree.treeMap.get(id)?.meta?.dateStart) {
|
|
193
|
+
tree.updateMeta(id, {
|
|
194
|
+
dateStart: val.start,
|
|
195
|
+
dateEnd: val.end,
|
|
196
|
+
taskProgress: val.progress ?? 0,
|
|
197
|
+
color: val.color
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
doc.transact(() => {
|
|
202
|
+
for (const key of [...legacyMap.keys()]) legacyMap.delete(key);
|
|
203
|
+
});
|
|
204
|
+
}, { immediate: true });
|
|
205
|
+
const containerRef = ref(null);
|
|
206
|
+
function onPointerMove(e) {
|
|
207
|
+
const rect = containerRef.value?.getBoundingClientRect();
|
|
208
|
+
if (!rect) return;
|
|
209
|
+
setLocalState({
|
|
210
|
+
pos: {
|
|
211
|
+
x: (e.clientX - rect.left) / rect.width * 100,
|
|
212
|
+
y: (e.clientY - rect.top) / rect.height * 100
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
function clearCursor() {
|
|
217
|
+
setLocalState({ pos: null });
|
|
218
|
+
}
|
|
219
|
+
onBeforeUnmount(() => {
|
|
220
|
+
setLocalState({ "pos": null, "timeline:focused": null });
|
|
221
|
+
});
|
|
222
|
+
defineExpose({ connectedUsers });
|
|
223
|
+
</script>
|
|
224
|
+
|
|
225
|
+
<template>
|
|
226
|
+
<div
|
|
227
|
+
ref="containerRef"
|
|
228
|
+
class="flex-1 min-h-0 flex flex-col relative"
|
|
229
|
+
@pointermove="onPointerMove"
|
|
230
|
+
@pointerleave="clearCursor"
|
|
231
|
+
>
|
|
232
|
+
<!-- Toolbar -->
|
|
233
|
+
<div class="flex items-center justify-between px-4 py-2 border-b border-(--ui-border) shrink-0">
|
|
234
|
+
<div class="flex gap-1">
|
|
235
|
+
<UButton
|
|
236
|
+
v-for="z in ['week', 'month', 'quarter']"
|
|
237
|
+
:key="z"
|
|
238
|
+
:label="locale[z]"
|
|
239
|
+
size="xs"
|
|
240
|
+
:variant="zoomLevel === z ? 'solid' : 'ghost'"
|
|
241
|
+
color="neutral"
|
|
242
|
+
@click="zoomLevel = z"
|
|
243
|
+
/>
|
|
244
|
+
</div>
|
|
245
|
+
<UButton
|
|
246
|
+
v-if="editable"
|
|
247
|
+
icon="i-lucide-plus"
|
|
248
|
+
size="xs"
|
|
249
|
+
variant="ghost"
|
|
250
|
+
color="neutral"
|
|
251
|
+
:label="locale.addEpic"
|
|
252
|
+
@click="addEpic"
|
|
253
|
+
/>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<!-- Content -->
|
|
257
|
+
<div class="flex-1 overflow-auto">
|
|
258
|
+
<!-- Empty state -->
|
|
259
|
+
<div
|
|
260
|
+
v-if="epics.length === 0"
|
|
261
|
+
class="flex flex-col items-center justify-center h-full gap-3 text-center"
|
|
262
|
+
>
|
|
263
|
+
<UIcon
|
|
264
|
+
name="i-lucide-gantt-chart"
|
|
265
|
+
class="size-10 text-(--ui-text-dimmed)"
|
|
266
|
+
/>
|
|
267
|
+
<p class="text-sm text-(--ui-text-muted)">
|
|
268
|
+
{{ locale.empty }}
|
|
269
|
+
</p>
|
|
270
|
+
<UButton
|
|
271
|
+
v-if="editable"
|
|
272
|
+
icon="i-lucide-plus"
|
|
273
|
+
:label="locale.addEpic"
|
|
274
|
+
size="sm"
|
|
275
|
+
@click="addEpic"
|
|
276
|
+
/>
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
<div
|
|
280
|
+
v-else
|
|
281
|
+
class="flex h-full"
|
|
282
|
+
>
|
|
283
|
+
<!-- Label panel -->
|
|
284
|
+
<div class="w-48 flex-shrink-0 border-r border-(--ui-border) sticky left-0 bg-(--ui-bg) z-10">
|
|
285
|
+
<div class="h-8 border-b border-(--ui-border)" />
|
|
286
|
+
<TransitionGroup
|
|
287
|
+
name="tepic"
|
|
288
|
+
tag="div"
|
|
289
|
+
>
|
|
290
|
+
<div
|
|
291
|
+
v-for="epic in epics"
|
|
292
|
+
:key="epic.id"
|
|
293
|
+
>
|
|
294
|
+
<button
|
|
295
|
+
:data-drag-id="epic.id"
|
|
296
|
+
class="h-9 w-full flex items-center px-3 text-sm font-medium border-b border-(--ui-border) cursor-grab hover:bg-(--ui-bg-elevated) text-left transition-colors touch-none"
|
|
297
|
+
:class="{
|
|
298
|
+
'border-t-2 border-(--ui-primary)': epicDragOver === epic.id,
|
|
299
|
+
'opacity-40': epicDragId === epic.id
|
|
300
|
+
}"
|
|
301
|
+
@pointerdown="editable && handleEpicPointerDown($event, epic.id)"
|
|
302
|
+
@click="openNode(epic.id, epic.label)"
|
|
303
|
+
>
|
|
304
|
+
<UIcon
|
|
305
|
+
name="i-lucide-chevron-right"
|
|
306
|
+
class="size-3 mr-1.5 text-(--ui-text-muted) shrink-0"
|
|
307
|
+
/>
|
|
308
|
+
<span class="truncate">{{ epic.label }}</span>
|
|
309
|
+
</button>
|
|
310
|
+
|
|
311
|
+
<button
|
|
312
|
+
v-for="task in tree.childrenOf(epic.id)"
|
|
313
|
+
:key="task.id"
|
|
314
|
+
class="h-8 w-full flex items-center px-6 text-xs border-b border-(--ui-border) hover:bg-(--ui-bg-elevated) text-left"
|
|
315
|
+
@click="openNode(task.id, task.label)"
|
|
316
|
+
>
|
|
317
|
+
<span class="truncate text-(--ui-text-dimmed)">
|
|
318
|
+
{{ task.label }}
|
|
319
|
+
</span>
|
|
320
|
+
</button>
|
|
321
|
+
|
|
322
|
+
<button
|
|
323
|
+
v-if="editable"
|
|
324
|
+
class="h-7 w-full flex items-center px-6 text-xs text-(--ui-text-dimmed) hover:text-(--ui-text) border-b border-(--ui-border) text-left"
|
|
325
|
+
@click="addTask(epic.id)"
|
|
326
|
+
>
|
|
327
|
+
+ {{ locale.addTask.toLowerCase() }}
|
|
328
|
+
</button>
|
|
329
|
+
</div>
|
|
330
|
+
</TransitionGroup>
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
<!-- Timeline panel -->
|
|
334
|
+
<div class="flex-1 overflow-x-auto">
|
|
335
|
+
<div :style="{ width: `${axisWidth}px`, minWidth: '100%', position: 'relative' }">
|
|
336
|
+
<!-- Axis labels -->
|
|
337
|
+
<div class="h-8 relative border-b border-(--ui-border) sticky top-0 bg-(--ui-bg) z-10">
|
|
338
|
+
<div
|
|
339
|
+
v-for="lbl in axisLabels"
|
|
340
|
+
:key="lbl.offset"
|
|
341
|
+
class="absolute top-1.5 text-[10px] text-(--ui-text-muted) whitespace-nowrap px-0.5"
|
|
342
|
+
:style="{ left: `${lbl.offset}px` }"
|
|
343
|
+
>
|
|
344
|
+
{{ lbl.label }}
|
|
345
|
+
</div>
|
|
346
|
+
<!-- Today line -->
|
|
347
|
+
<div
|
|
348
|
+
class="absolute top-0 bottom-0 w-px bg-(--ui-primary) opacity-60"
|
|
349
|
+
:style="{ left: `${todayOffset}px` }"
|
|
350
|
+
/>
|
|
351
|
+
</div>
|
|
352
|
+
|
|
353
|
+
<!-- Bars -->
|
|
354
|
+
<TransitionGroup
|
|
355
|
+
name="tepic"
|
|
356
|
+
tag="div"
|
|
357
|
+
>
|
|
358
|
+
<div
|
|
359
|
+
v-for="epic in epics"
|
|
360
|
+
:key="epic.id"
|
|
361
|
+
:data-epic-id="epic.id"
|
|
362
|
+
:class="barHoverEpicId === epic.id && barDragId ? 'bg-(--ui-primary)/5' : ''"
|
|
363
|
+
>
|
|
364
|
+
<!-- Epic bar row -->
|
|
365
|
+
<div class="h-9 relative border-b border-(--ui-border)">
|
|
366
|
+
<div
|
|
367
|
+
v-if="epic.meta?.dateStart && epic.meta?.dateEnd"
|
|
368
|
+
class="absolute top-2 h-5 rounded text-white text-[10px] flex items-center px-2 font-medium select-none transition-opacity"
|
|
369
|
+
:class="barDragId === epic.id ? 'opacity-60 cursor-grabbing' : 'cursor-grab hover:opacity-80'"
|
|
370
|
+
:style="{
|
|
371
|
+
left: `${dateToOffset(epic.meta.dateStart)}px`,
|
|
372
|
+
width: `${spanWidth(epic.meta.dateStart, epic.meta.dateEnd)}px`,
|
|
373
|
+
background: epic.meta.color ?? '#6366f1',
|
|
374
|
+
maxWidth: '100%',
|
|
375
|
+
...taskFocusers(epic.id).length ? { outline: `2px solid ${taskFocusers(epic.id)[0].user?.color}`, outlineOffset: '1px' } : {}
|
|
376
|
+
}"
|
|
377
|
+
@pointerdown="onBarPointerDown($event, epic.id)"
|
|
378
|
+
@pointermove="onBarPointerMove($event, epic.id)"
|
|
379
|
+
@pointerup="onBarPointerUp(epic.id)"
|
|
380
|
+
@pointerenter="focusTask(epic.id)"
|
|
381
|
+
@pointerleave="clearFocus"
|
|
382
|
+
@click.stop="onBarClick(epic.id, epic.label)"
|
|
383
|
+
>
|
|
384
|
+
<span class="truncate">{{ epic.label }}</span>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
<!-- Task bar rows -->
|
|
389
|
+
<div
|
|
390
|
+
v-for="task in tree.childrenOf(epic.id)"
|
|
391
|
+
:key="task.id"
|
|
392
|
+
class="h-8 relative border-b border-(--ui-border)"
|
|
393
|
+
>
|
|
394
|
+
<div
|
|
395
|
+
v-if="task.meta?.dateStart && task.meta?.dateEnd"
|
|
396
|
+
class="absolute top-1.5 h-5 rounded text-white text-[10px] flex items-center px-2 select-none opacity-85 transition-opacity"
|
|
397
|
+
:class="barDragId === task.id ? 'opacity-60 cursor-grabbing' : 'cursor-grab hover:opacity-80'"
|
|
398
|
+
:style="{
|
|
399
|
+
left: `${dateToOffset(task.meta.dateStart)}px`,
|
|
400
|
+
width: `${spanWidth(task.meta.dateStart, task.meta.dateEnd)}px`,
|
|
401
|
+
background: task.meta.color ?? '#818cf8',
|
|
402
|
+
maxWidth: '100%',
|
|
403
|
+
...taskFocusers(task.id).length ? { outline: `2px solid ${taskFocusers(task.id)[0].user?.color}`, outlineOffset: '1px' } : {}
|
|
404
|
+
}"
|
|
405
|
+
@pointerdown="onBarPointerDown($event, task.id)"
|
|
406
|
+
@pointermove="onBarPointerMove($event, task.id)"
|
|
407
|
+
@pointerup="onBarPointerUp(task.id, epic.id)"
|
|
408
|
+
@pointerenter="focusTask(task.id)"
|
|
409
|
+
@pointerleave="clearFocus"
|
|
410
|
+
@click.stop="onBarClick(task.id, task.label)"
|
|
411
|
+
>
|
|
412
|
+
<span class="truncate">{{ task.label }}</span>
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
<!-- Add task spacer -->
|
|
417
|
+
<div
|
|
418
|
+
v-if="editable"
|
|
419
|
+
class="h-7 border-b border-(--ui-border)"
|
|
420
|
+
/>
|
|
421
|
+
</div>
|
|
422
|
+
</TransitionGroup>
|
|
423
|
+
|
|
424
|
+
<!-- Today vertical line across bars -->
|
|
425
|
+
<div
|
|
426
|
+
class="absolute top-8 bottom-0 w-px bg-(--ui-primary) opacity-20 pointer-events-none"
|
|
427
|
+
:style="{ left: `${todayOffset}px` }"
|
|
428
|
+
/>
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
|
|
434
|
+
<!-- Node panel -->
|
|
435
|
+
<ANodePanel
|
|
436
|
+
:node-id="openNodeId"
|
|
437
|
+
:node-label="openNodeLabel"
|
|
438
|
+
:child-provider="openNodeProvider"
|
|
439
|
+
@close="closePanel"
|
|
440
|
+
/>
|
|
441
|
+
</div>
|
|
442
|
+
</template>
|
|
443
|
+
|
|
444
|
+
<style scoped>
|
|
445
|
+
.tepic-move{transition:transform .25s ease}.tepic-enter-active{transition:opacity .18s ease,transform .18s ease}.tepic-enter-from{opacity:0;transform:translateY(-6px) scale(.97)}.tepic-leave-active{transition:opacity .15s ease}.tepic-leave-to{opacity:0}
|
|
446
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type RendererBaseProps } from '../../composables/useRendererBase.js';
|
|
2
|
+
import type { AbracadabraLocale } from '../../locale.js';
|
|
3
|
+
type __VLS_Props = RendererBaseProps & {
|
|
4
|
+
labels?: Partial<AbracadabraLocale['renderers']['timeline']>;
|
|
5
|
+
editable?: boolean;
|
|
6
|
+
};
|
|
7
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
|
|
8
|
+
connectedUsers: import("vue").ComputedRef<{
|
|
9
|
+
clientId: number;
|
|
10
|
+
name: string;
|
|
11
|
+
color: string;
|
|
12
|
+
avatar: string | undefined;
|
|
13
|
+
publicKey: any;
|
|
14
|
+
}[]>;
|
|
15
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
16
|
+
editable: boolean;
|
|
17
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
18
|
+
declare const _default: typeof __VLS_export;
|
|
19
|
+
export default _default;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { shallowRef, computed, watch } from "vue";
|
|
2
|
+
import { tryOnScopeDispose } from "@vueuse/core";
|
|
2
3
|
export function useAwareness() {
|
|
3
4
|
const { provider } = useAbracadabra();
|
|
4
5
|
const states = shallowRef(/* @__PURE__ */ new Map());
|
|
@@ -22,6 +23,10 @@ export function useAwareness() {
|
|
|
22
23
|
_unsub = () => awareness.off("change", handler);
|
|
23
24
|
}
|
|
24
25
|
watch(provider, () => _subscribe(), { immediate: true });
|
|
26
|
+
tryOnScopeDispose(() => {
|
|
27
|
+
_unsub?.();
|
|
28
|
+
_unsub = null;
|
|
29
|
+
});
|
|
25
30
|
const localState = computed(() => {
|
|
26
31
|
const awareness = provider.value?.awareness;
|
|
27
32
|
if (!awareness) return {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useBroadcastSync
|
|
3
|
+
*
|
|
4
|
+
* Wraps BroadcastChannelSync from @abraca/dabra for cross-tab Y.js sync.
|
|
5
|
+
* Initialized by the main plugin after root provider syncs.
|
|
6
|
+
* No user action needed — automatically syncs across browser tabs on the same origin.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const { isActive } = useBroadcastSync()
|
|
10
|
+
*/
|
|
11
|
+
import type * as Y from 'yjs';
|
|
12
|
+
import type { Awareness } from 'y-protocols/awareness';
|
|
13
|
+
export declare function _initBroadcastSync(BroadcastChannelSync: any, doc: Y.Doc, docId: string, awareness?: Awareness | null): void;
|
|
14
|
+
export declare function _destroyBroadcastSync(): void;
|
|
15
|
+
export declare function useBroadcastSync(): {
|
|
16
|
+
/** Whether cross-tab BroadcastChannel sync is currently active. */
|
|
17
|
+
isActive: import("vue").Ref<boolean, boolean>;
|
|
18
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ref } from "vue";
|
|
2
|
+
const isActive = ref(false);
|
|
3
|
+
let _sync = null;
|
|
4
|
+
export function _initBroadcastSync(BroadcastChannelSync, doc, docId, awareness) {
|
|
5
|
+
_destroyBroadcastSync();
|
|
6
|
+
try {
|
|
7
|
+
_sync = BroadcastChannelSync.forDoc(doc, docId, awareness ?? void 0);
|
|
8
|
+
_sync.connect();
|
|
9
|
+
isActive.value = true;
|
|
10
|
+
} catch (e) {
|
|
11
|
+
console.error("[abracadabra] BroadcastChannelSync init failed:", e);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function _destroyBroadcastSync() {
|
|
15
|
+
if (_sync) {
|
|
16
|
+
_sync.destroy();
|
|
17
|
+
_sync = null;
|
|
18
|
+
}
|
|
19
|
+
isActive.value = false;
|
|
20
|
+
}
|
|
21
|
+
export function useBroadcastSync() {
|
|
22
|
+
return {
|
|
23
|
+
/** Whether cross-tab BroadcastChannel sync is currently active. */
|
|
24
|
+
isActive
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -32,7 +32,8 @@ function resolveChannelLabel(channelId, senderName, senderId) {
|
|
|
32
32
|
try {
|
|
33
33
|
const entry = doc.value?.getMap("doc-tree")?.get(docId);
|
|
34
34
|
if (entry?.label) return `${entry.label} Chat`;
|
|
35
|
-
} catch {
|
|
35
|
+
} catch (e) {
|
|
36
|
+
if (import.meta.dev) console.warn("[abracadabra] chat: failed to resolve channel label:", e);
|
|
36
37
|
}
|
|
37
38
|
return "Group Chat";
|
|
38
39
|
}
|
|
@@ -134,7 +135,8 @@ export function _handleStatelessChat(payload) {
|
|
|
134
135
|
break;
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
|
-
} catch {
|
|
138
|
+
} catch (e) {
|
|
139
|
+
if (import.meta.dev) console.warn("[abracadabra] chat: failed to handle stateless message:", e);
|
|
138
140
|
}
|
|
139
141
|
}
|
|
140
142
|
function sendMessage(channelId, content) {
|
|
@@ -5,6 +5,7 @@ const searchTerm = ref("");
|
|
|
5
5
|
export function useCommandPalette() {
|
|
6
6
|
const { doc, isReady, provider } = useAbracadabra();
|
|
7
7
|
const registry = usePluginRegistry();
|
|
8
|
+
const { getDocUrl } = useDocSlugs();
|
|
8
9
|
const recentDocIds = useLocalStorage("abra_recent_docs", []);
|
|
9
10
|
function open(term = "") {
|
|
10
11
|
searchTerm.value = term;
|
|
@@ -43,7 +44,7 @@ export function useCommandPalette() {
|
|
|
43
44
|
suffix: entry.type,
|
|
44
45
|
onClick: () => {
|
|
45
46
|
trackRecentDoc(id);
|
|
46
|
-
navigateTo(
|
|
47
|
+
navigateTo(getDocUrl(id));
|
|
47
48
|
close();
|
|
48
49
|
}
|
|
49
50
|
});
|
|
@@ -52,7 +53,64 @@ export function useCommandPalette() {
|
|
|
52
53
|
if (sorted.length > 0) {
|
|
53
54
|
result.push({ id: "docs", label: q ? "Documents" : "Recent", items: sorted });
|
|
54
55
|
}
|
|
55
|
-
} catch {
|
|
56
|
+
} catch (e) {
|
|
57
|
+
if (import.meta.dev) console.warn("[abracadabra] command palette: failed to scan document tree:", e);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
{
|
|
61
|
+
const actionItems = [
|
|
62
|
+
{
|
|
63
|
+
id: "action:new-doc",
|
|
64
|
+
label: "New Document",
|
|
65
|
+
icon: "i-lucide-file-plus",
|
|
66
|
+
shortcut: ["meta", "N"],
|
|
67
|
+
onClick: () => {
|
|
68
|
+
if (!doc.value || !isReady.value) return;
|
|
69
|
+
const id = crypto.randomUUID();
|
|
70
|
+
doc.value.getMap("doc-tree").set(id, { label: "Untitled", parentId: null, order: Date.now(), type: "doc" });
|
|
71
|
+
navigateTo(getDocUrl(id));
|
|
72
|
+
close();
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "action:home",
|
|
77
|
+
label: "Go to Home",
|
|
78
|
+
icon: "i-lucide-home",
|
|
79
|
+
onClick: () => {
|
|
80
|
+
navigateTo("/app");
|
|
81
|
+
close();
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "action:chat",
|
|
86
|
+
label: "Go to Chat",
|
|
87
|
+
icon: "i-lucide-message-circle",
|
|
88
|
+
onClick: () => {
|
|
89
|
+
navigateTo("/chat");
|
|
90
|
+
close();
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "action:inbox",
|
|
95
|
+
label: "Go to Inbox",
|
|
96
|
+
icon: "i-lucide-inbox",
|
|
97
|
+
onClick: () => {
|
|
98
|
+
navigateTo("/inbox");
|
|
99
|
+
close();
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: "action:settings",
|
|
104
|
+
label: "Go to Settings",
|
|
105
|
+
icon: "i-lucide-settings",
|
|
106
|
+
onClick: () => {
|
|
107
|
+
navigateTo("/settings");
|
|
108
|
+
close();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
].filter((item) => !q || item.label.toLowerCase().includes(q));
|
|
112
|
+
if (actionItems.length > 0) {
|
|
113
|
+
result.push({ id: "actions", label: "Actions", items: actionItems });
|
|
56
114
|
}
|
|
57
115
|
}
|
|
58
116
|
try {
|
|
@@ -75,7 +133,8 @@ export function useCommandPalette() {
|
|
|
75
133
|
result.push({ id: group.id ?? "plugin", label: group.label, items: filtered });
|
|
76
134
|
}
|
|
77
135
|
}
|
|
78
|
-
} catch {
|
|
136
|
+
} catch (e) {
|
|
137
|
+
if (import.meta.dev) console.warn("[abracadabra] command palette: failed to load plugin commands:", e);
|
|
79
138
|
}
|
|
80
139
|
groups.value = result;
|
|
81
140
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { computed, ref, toValue, watch } from "vue";
|
|
2
|
+
import { tryOnScopeDispose } from "@vueuse/core";
|
|
2
3
|
import { useAbraLocale } from "./useAbraLocale.js";
|
|
3
4
|
import { useAbracadabra } from "./useAbracadabra.js";
|
|
4
5
|
export function useConnectionStatus(status, synced) {
|
|
@@ -40,6 +41,12 @@ export function useConnectionStatus(status, synced) {
|
|
|
40
41
|
stableStatus.value = next;
|
|
41
42
|
}
|
|
42
43
|
}, { immediate: true });
|
|
44
|
+
tryOnScopeDispose(() => {
|
|
45
|
+
if (offlineTimer) {
|
|
46
|
+
clearTimeout(offlineTimer);
|
|
47
|
+
offlineTimer = null;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
43
50
|
const label = computed(() => {
|
|
44
51
|
const s = stableStatus.value;
|
|
45
52
|
if (s === "connected") return locale.connected;
|