mulmoclaude 0.6.4 → 0.7.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/bin/mulmoclaude.js +1 -1
- package/client/assets/{html2canvas-CDGcmOD3-XVrO-eyz.js → html2canvas-CDGcmOD3-CKJ6vKPo.js} +1 -1
- package/client/assets/{index-zZIqEbNX.js → index-BG_JJcKI.js} +193 -197
- package/client/assets/index-DCoo3kpR.css +2 -0
- package/client/assets/{index.es-DqtpmBm8-DHT6q10o.js → index.es-DqtpmBm8-DFXjJgCa.js} +1 -1
- package/client/assets/lib-Dpph7PBN.js +114 -0
- package/client/assets/marp-cCGismx0.js +3452 -0
- package/client/assets/schemas-DuYzyHQc.js +64 -0
- package/client/index.html +5 -4
- package/package.json +7 -5
- package/server/agent/backend/claude-code.ts +44 -10
- package/server/agent/backend/fake-echo.ts +5 -1
- package/server/agent/config.ts +49 -12
- package/server/agent/mcp-server.ts +13 -2
- package/server/agent/mcp-tools/handlePermission.ts +115 -0
- package/server/agent/mcp-tools/index.ts +2 -1
- package/server/agent/mcp-tools/x.ts +18 -2
- package/server/agent/prompt.ts +3 -45
- package/server/api/csrfGuard.ts +122 -32
- package/server/api/routes/collections.ts +320 -0
- package/server/api/routes/config.ts +45 -0
- package/server/api/routes/feeds.ts +70 -0
- package/server/api/routes/files.ts +167 -0
- package/server/api/routes/marp-themes.ts +15 -0
- package/server/api/routes/mulmo-script.ts +139 -0
- package/server/api/routes/pdf.ts +108 -0
- package/server/api/routes/plugins.ts +10 -0
- package/server/api/routes/shortcuts.ts +49 -0
- package/server/api/routes/wiki.ts +35 -0
- package/server/build/dispatcher.mjs +40 -22
- package/server/events/notifications.ts +14 -9
- package/server/index.ts +49 -14
- package/server/plugins/preset-list.ts +18 -10
- package/server/plugins/preset-loader.ts +11 -4
- package/server/prompts/index.ts +1 -3
- package/server/prompts/system/system.md +7 -2
- package/server/system/env.ts +14 -0
- package/server/utils/clientDir.ts +7 -0
- package/server/utils/files/index.ts +0 -2
- package/server/utils/files/shortcuts-io.ts +63 -0
- package/server/utils/slug.ts +3 -4
- package/server/workspace/billing-migration.ts +69 -0
- package/server/workspace/collections/delete.ts +186 -0
- package/server/workspace/collections/discovery.ts +730 -0
- package/server/workspace/collections/index.ts +23 -0
- package/server/workspace/collections/io.ts +287 -0
- package/server/workspace/collections/notifications.ts +404 -0
- package/server/workspace/collections/paths.ts +125 -0
- package/server/workspace/collections/spawn.ts +213 -0
- package/server/workspace/collections/templatePath.ts +36 -0
- package/server/workspace/collections/types.ts +334 -0
- package/server/workspace/collections/watcher.ts +398 -0
- package/server/workspace/feeds/engine.ts +135 -0
- package/server/workspace/feeds/fetch/httpClient.ts +127 -0
- package/server/workspace/feeds/fetch/rssParser.ts +117 -0
- package/server/workspace/feeds/index.ts +8 -0
- package/server/workspace/feeds/ingestTypes.ts +66 -0
- package/server/workspace/feeds/pathResolver.ts +74 -0
- package/server/workspace/feeds/paths.ts +30 -0
- package/server/workspace/feeds/projectItem.ts +92 -0
- package/server/workspace/feeds/registry.ts +43 -0
- package/server/workspace/feeds/retrievers/httpJson.ts +19 -0
- package/server/workspace/feeds/retrievers/index.ts +27 -0
- package/server/workspace/feeds/retrievers/registerAll.ts +5 -0
- package/server/workspace/feeds/retrievers/rss.ts +24 -0
- package/server/workspace/feeds/state.ts +55 -0
- package/server/workspace/helps/billing-clients-worklog.md +215 -0
- package/server/workspace/helps/billing-invoice.md +457 -0
- package/server/workspace/helps/collection-skills.md +664 -0
- package/server/workspace/helps/feeds.md +110 -0
- package/server/workspace/helps/index.md +9 -3
- package/server/workspace/helps/portfolio-tracker.md +211 -0
- package/server/workspace/helps/presentation-deck.md +828 -0
- package/server/workspace/helps/todo-collection.md +140 -0
- package/server/workspace/helps/vocabulary.md +106 -0
- package/server/workspace/hooks/handlers/skillBridge.ts +101 -40
- package/server/workspace/marp-themes.ts +46 -0
- package/server/workspace/paths.ts +46 -11
- package/server/workspace/skills-preset/mc-manage-skills/SKILL.md +13 -0
- package/server/workspace/skills-preset/mc-wiki-deep-lint/SKILL.md +108 -0
- package/server/workspace/skills-preset/mc-wiki-health-check/SKILL.md +61 -0
- package/server/workspace/skills-preset/mc-wiki-ingest/SKILL.md +182 -0
- package/server/workspace/skills-preset/mc-wiki-promote/SKILL.md +175 -0
- package/server/workspace/skills-preset.ts +376 -2
- package/server/workspace/wiki-pages/io.ts +34 -2
- package/server/workspace/workspace.ts +20 -1
- package/src/App.vue +70 -41
- package/src/components/BackendOfflineBanner.vue +56 -0
- package/src/components/ChatInput.vue +72 -6
- package/src/components/CollectionCalendarView.vue +243 -0
- package/src/components/CollectionDashboardView.vue +181 -0
- package/src/components/CollectionDayView.vue +308 -0
- package/src/components/CollectionEmbedView.vue +69 -0
- package/src/components/CollectionKanbanView.vue +196 -0
- package/src/components/CollectionRecordModal.vue +93 -0
- package/src/components/CollectionRecordPanel.vue +567 -0
- package/src/components/CollectionView.vue +1748 -0
- package/src/components/CollectionsIndexView.vue +152 -0
- package/src/components/ConfirmModal.vue +344 -0
- package/src/components/FeedsView.vue +225 -0
- package/src/components/FileContentRenderer.vue +122 -30
- package/src/components/FileTree.vue +218 -2
- package/src/components/FileTreePane.vue +2 -0
- package/src/components/FilesView.vue +95 -17
- package/src/components/PinToggle.vue +52 -0
- package/src/components/PluginLauncher.vue +97 -37
- package/src/components/RightSidebar.vue +74 -3
- package/src/components/RolesView.vue +1 -1
- package/src/components/SettingsModal.vue +146 -72
- package/src/components/SlashCommandMenu.vue +56 -0
- package/src/components/StackView.vue +128 -48
- package/src/components/collectionEmbed.ts +29 -0
- package/src/components/collectionTypes.ts +177 -0
- package/src/composables/collections/useCollectionRendering.ts +350 -0
- package/src/composables/useConfirm.ts +70 -0
- package/src/composables/useFileTree.ts +35 -3
- package/src/composables/useImeAwareEnter.ts +14 -0
- package/src/composables/usePdfDownload.ts +6 -1
- package/src/composables/useShortcuts.ts +163 -0
- package/src/composables/useSlashCommandMenu.ts +138 -0
- package/src/config/apiRoutes.ts +46 -13
- package/src/config/createFilePolicy.ts +82 -0
- package/src/config/roles.ts +46 -47
- package/src/config/systemFileDescriptors.ts +0 -30
- package/src/config/toolNames.ts +1 -5
- package/src/config/workspacePaths.ts +4 -9
- package/src/lang/de.ts +154 -221
- package/src/lang/en.ts +153 -218
- package/src/lang/es.ts +154 -219
- package/src/lang/fr.ts +155 -221
- package/src/lang/index.ts +55 -0
- package/src/lang/ja.ts +153 -219
- package/src/lang/ko.ts +153 -218
- package/src/lang/pt-BR.ts +154 -219
- package/src/lang/zh.ts +152 -218
- package/src/lib/vue-i18n.ts +15 -45
- package/src/lib/wiki-page/graph.ts +108 -0
- package/src/main.ts +2 -5
- package/src/plugins/_generated/metas.ts +2 -8
- package/src/plugins/_generated/registrations.ts +2 -4
- package/src/plugins/_generated/server-bindings.ts +5 -12
- package/src/plugins/manageSkills/View.vue +36 -68
- package/src/plugins/manageSkills/presetDetection.ts +25 -0
- package/src/plugins/markdown/MarpView.vue +301 -0
- package/src/plugins/markdown/Preview.vue +26 -3
- package/src/plugins/markdown/View.vue +230 -1
- package/src/plugins/markdown/definition.ts +21 -1
- package/src/plugins/presentCollection/Preview.vue +30 -0
- package/src/plugins/presentCollection/View.vue +78 -0
- package/src/plugins/presentCollection/definition.ts +30 -0
- package/src/plugins/presentCollection/index.ts +25 -0
- package/src/plugins/presentCollection/meta.ts +15 -0
- package/src/plugins/presentCollection/plugin.ts +39 -0
- package/src/plugins/presentCollection/types.ts +13 -0
- package/src/plugins/presentForm/View.vue +56 -6
- package/src/plugins/presentForm/types.ts +3 -3
- package/src/plugins/presentMulmoScript/View.vue +252 -5
- package/src/plugins/presentMulmoScript/helpers.ts +18 -0
- package/src/plugins/presentMulmoScript/meta.ts +11 -0
- package/src/plugins/scheduler/AutomationsView.vue +13 -11
- package/src/plugins/scheduler/automationsDefinition.ts +7 -10
- package/src/plugins/scheduler/automationsMeta.ts +27 -0
- package/src/plugins/scheduler/index.ts +19 -38
- package/src/plugins/wiki/View.vue +120 -27
- package/src/plugins/wiki/components/WikiGraphView.vue +75 -0
- package/src/plugins/wiki/components/WikiPageBody.vue +18 -0
- package/src/plugins/wiki/index.ts +6 -0
- package/src/plugins/wiki/route.ts +8 -1
- package/src/router/index.ts +31 -34
- package/src/router/pageRoutes.ts +2 -7
- package/src/tools/types.ts +4 -3
- package/src/types/notification.ts +5 -9
- package/src/types/session.ts +11 -0
- package/src/types/shortcuts.ts +37 -0
- package/src/utils/agent/eventDispatch.ts +4 -0
- package/src/utils/api.ts +47 -1
- package/src/utils/canvas/stackGrouping.ts +96 -0
- package/src/utils/chat/permalink.ts +20 -0
- package/src/utils/collections/actionVisible.ts +55 -0
- package/src/utils/collections/calendarGrid.ts +328 -0
- package/src/utils/collections/collectionViewMode.ts +42 -0
- package/src/utils/collections/derivedFormula.ts +364 -0
- package/src/utils/collections/draft.ts +160 -0
- package/src/utils/collections/enumColors.ts +130 -0
- package/src/utils/collections/itemLabel.ts +42 -0
- package/src/utils/confirmDelete.ts +13 -0
- package/src/utils/markdown/marpAspect.ts +28 -0
- package/src/utils/markdown/marpCustomSize.ts +120 -0
- package/src/utils/markdown/marpDetect.ts +15 -0
- package/src/utils/markdown/marpTheme.ts +109 -0
- package/src/utils/markdown/wikiEmbedHandlers.ts +1 -1
- package/src/utils/path/workspaceLinkRouter.ts +126 -9
- package/src/utils/session/sessionEntries.ts +1 -0
- package/src/utils/session/sessionFactory.ts +1 -0
- package/src/utils/session/sessionHelpers.ts +6 -1
- package/client/assets/index-CyBr8Mkr.css +0 -2
- package/server/api/routes/encore.ts +0 -55
- package/server/api/routes/news.ts +0 -133
- package/server/api/routes/sources.ts +0 -550
- package/server/encore/INVARIANTS.md +0 -272
- package/server/encore/boot.ts +0 -39
- package/server/encore/closure.ts +0 -36
- package/server/encore/cycle.ts +0 -276
- package/server/encore/dispatch.ts +0 -103
- package/server/encore/handlers/amend.ts +0 -99
- package/server/encore/handlers/appendNote.ts +0 -74
- package/server/encore/handlers/defineEncore.ts +0 -42
- package/server/encore/handlers/listTickets.ts +0 -107
- package/server/encore/handlers/markStepDone.ts +0 -41
- package/server/encore/handlers/markTargetSkipped.ts +0 -33
- package/server/encore/handlers/query.ts +0 -138
- package/server/encore/handlers/recordValues.ts +0 -44
- package/server/encore/handlers/resolveNotification.ts +0 -121
- package/server/encore/handlers/setup.ts +0 -81
- package/server/encore/handlers/shared.ts +0 -137
- package/server/encore/handlers/snooze.ts +0 -87
- package/server/encore/handlers/startObligationChat.ts +0 -64
- package/server/encore/handlers/startSetupChat.ts +0 -50
- package/server/encore/lock.ts +0 -61
- package/server/encore/notifier.ts +0 -123
- package/server/encore/obligation.ts +0 -25
- package/server/encore/paths.ts +0 -78
- package/server/encore/reconcile.ts +0 -661
- package/server/encore/tick.ts +0 -191
- package/server/encore/yaml-fm.ts +0 -63
- package/server/prompts/system/news-concierge.md +0 -24
- package/server/prompts/system/sources-context.md +0 -16
- package/server/utils/files/encore-io.ts +0 -111
- package/server/workspace/helps/encore-dsl.md +0 -482
- package/server/workspace/helps/sources.md +0 -42
- package/server/workspace/news/reader.ts +0 -247
- package/server/workspace/skills-preset/mc-manage-sources/SKILL.md +0 -106
- package/server/workspace/sources/arxivDiscovery.ts +0 -182
- package/server/workspace/sources/classifier.ts +0 -268
- package/server/workspace/sources/fetchers/arxiv.ts +0 -170
- package/server/workspace/sources/fetchers/github.ts +0 -106
- package/server/workspace/sources/fetchers/githubIssues.ts +0 -210
- package/server/workspace/sources/fetchers/githubReleases.ts +0 -186
- package/server/workspace/sources/fetchers/index.ts +0 -71
- package/server/workspace/sources/fetchers/registerAll.ts +0 -15
- package/server/workspace/sources/fetchers/rss.ts +0 -141
- package/server/workspace/sources/fetchers/rssParser.ts +0 -295
- package/server/workspace/sources/httpFetcher.ts +0 -230
- package/server/workspace/sources/interests.ts +0 -120
- package/server/workspace/sources/paths.ts +0 -110
- package/server/workspace/sources/pipeline/dedup.ts +0 -60
- package/server/workspace/sources/pipeline/fetch.ts +0 -182
- package/server/workspace/sources/pipeline/index.ts +0 -301
- package/server/workspace/sources/pipeline/notify.ts +0 -80
- package/server/workspace/sources/pipeline/plan.ts +0 -68
- package/server/workspace/sources/pipeline/summarize.ts +0 -189
- package/server/workspace/sources/pipeline/write.ts +0 -185
- package/server/workspace/sources/rateLimiter.ts +0 -148
- package/server/workspace/sources/registry.ts +0 -304
- package/server/workspace/sources/robots.ts +0 -271
- package/server/workspace/sources/sourceState.ts +0 -142
- package/server/workspace/sources/taxonomy.ts +0 -74
- package/server/workspace/sources/types.ts +0 -153
- package/server/workspace/sources/urls.ts +0 -112
- package/src/components/NewsView.vue +0 -267
- package/src/components/SourcesManager.vue +0 -915
- package/src/components/SourcesView.vue +0 -45
- package/src/components/TodoExplorer.vue +0 -423
- package/src/components/todo/TodoAddDialog.vue +0 -135
- package/src/components/todo/TodoEditDialog.vue +0 -51
- package/src/components/todo/TodoEditPanel.vue +0 -117
- package/src/components/todo/TodoKanbanView.vue +0 -290
- package/src/components/todo/TodoListView.vue +0 -88
- package/src/components/todo/TodoTableView.vue +0 -210
- package/src/composables/useNewsItems.ts +0 -38
- package/src/composables/useNewsReadState.ts +0 -75
- package/src/plugins/encore/EncoreDashboard.vue +0 -504
- package/src/plugins/encore/EncoreRedirect.vue +0 -116
- package/src/plugins/encore/View.vue +0 -36
- package/src/plugins/encore/defineEncoreDefinition.ts +0 -74
- package/src/plugins/encore/defineEncoreMeta.ts +0 -13
- package/src/plugins/encore/index.ts +0 -93
- package/src/plugins/encore/manageEncoreDefinition.ts +0 -100
- package/src/plugins/encore/manageEncoreMeta.ts +0 -36
- package/src/plugins/manageSource/Preview.vue +0 -33
- package/src/plugins/manageSource/View.vue +0 -13
- package/src/plugins/manageSource/definition.ts +0 -66
- package/src/plugins/manageSource/index.ts +0 -75
- package/src/plugins/manageSource/meta.ts +0 -21
- package/src/plugins/scheduler/CalendarView.vue +0 -23
- package/src/plugins/scheduler/Preview.vue +0 -73
- package/src/plugins/scheduler/View.vue +0 -608
- package/src/plugins/scheduler/calendarDefinition.ts +0 -47
- package/src/plugins/scheduler/calendarMeta.ts +0 -28
- package/src/plugins/scheduler/multiDayHelpers.ts +0 -95
- package/src/plugins/scheduler/viewModes.ts +0 -26
- package/src/types/encore-dsl/at-expression.ts +0 -120
- package/src/types/encore-dsl/at-resolver.ts +0 -32
- package/src/types/encore-dsl/cadence.ts +0 -289
- package/src/types/encore-dsl/schema.ts +0 -288
- package/src/utils/filesPreview/schedulerPreview.ts +0 -44
- package/src/utils/filesPreview/todoPreview.ts +0 -51
- package/src/utils/sources/filter.ts +0 -69
- /package/client/assets/{JsonEditor-D6WBWLoa.js → JsonEditor-C_RDoefj.js} +0 -0
- /package/client/assets/{chunk-D8eiyYIV-LcKZGJv5.js → chunk-D8eiyYIV-BY16KEZc.js} +0 -0
- /package/client/assets/{purify.es-Fx1Nqyry-Dwtk-9WZ.js → purify.es-Fx1Nqyry-BufT4RJl.js} +0 -0
- /package/client/assets/{typeof-DBp4T-Ny-CSr8wx1e.js → typeof-DBp4T-Ny-z2wCIsir.js} +0 -0
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
class="w-full flex items-center gap-1 px-2 py-1 text-left text-sm hover:bg-gray-100 rounded"
|
|
6
6
|
:data-testid="`file-tree-dir-${node.name || 'root'}`"
|
|
7
7
|
@click="onToggle"
|
|
8
|
+
@contextmenu="onFolderContextMenu"
|
|
8
9
|
>
|
|
9
10
|
<span class="material-icons text-sm text-gray-400 shrink-0">{{ expanded ? "folder_open" : "folder" }}</span>
|
|
10
11
|
<span class="text-gray-700 truncate">{{ node.name || t("fileTree.workspace") }}</span>
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
class="w-full flex items-center gap-1 px-2 py-1 text-left text-sm rounded transition-colors"
|
|
15
16
|
:class="selectedPath === node.path ? 'bg-blue-100 text-blue-700' : 'text-gray-700 hover:bg-gray-100'"
|
|
16
17
|
:data-testid="`file-tree-file-${node.name}`"
|
|
18
|
+
:data-selected="selectedPath === node.path ? 'true' : undefined"
|
|
17
19
|
:title="node.path"
|
|
18
20
|
@click="emit('select', node.path)"
|
|
19
21
|
>
|
|
@@ -22,6 +24,28 @@
|
|
|
22
24
|
<span v-if="isRecent" class="ml-auto w-1.5 h-1.5 rounded-full bg-green-500 shrink-0" :title="t('fileTree.recentlyChanged')" />
|
|
23
25
|
</button>
|
|
24
26
|
<div v-if="node.type === 'dir' && expanded" class="pl-4">
|
|
27
|
+
<!-- New-file inline input (#1598). Shown when the user picked
|
|
28
|
+
"New file" from the folder's context menu. Mounted as the
|
|
29
|
+
first child so the new entry sits where the user expects
|
|
30
|
+
it (top of the folder). Esc / blur close; Enter submits. -->
|
|
31
|
+
<div v-if="createPending" class="flex items-center gap-1 px-2 py-1 text-sm" :data-testid="`file-tree-new-file-input-${node.name || 'root'}`">
|
|
32
|
+
<span class="material-icons text-sm text-gray-400 shrink-0">description</span>
|
|
33
|
+
<input
|
|
34
|
+
ref="newFileInputRef"
|
|
35
|
+
v-model="newFileSlug"
|
|
36
|
+
type="text"
|
|
37
|
+
class="flex-1 min-w-0 px-1 py-0.5 text-sm border border-blue-400 rounded focus:outline-none focus:ring-1 focus:ring-blue-400"
|
|
38
|
+
:placeholder="placeholderText"
|
|
39
|
+
:aria-label="t('fileTree.newFileInputAria')"
|
|
40
|
+
data-testid="file-tree-new-file-input"
|
|
41
|
+
@keydown="onInputKeydown"
|
|
42
|
+
@compositionstart="onCompositionStart"
|
|
43
|
+
@compositionend="onCompositionEnd"
|
|
44
|
+
@blur="onInputBlur"
|
|
45
|
+
/>
|
|
46
|
+
<span class="text-xs text-gray-400 font-mono shrink-0 select-none">{{ createPolicy?.extension }}</span>
|
|
47
|
+
</div>
|
|
48
|
+
<div v-if="createError" class="px-2 py-1 text-xs text-red-600" data-testid="file-tree-new-file-error">{{ createError }}</div>
|
|
25
49
|
<!-- Loading state: children not in the cache yet. Rendered
|
|
26
50
|
once per dir so a slow network shows where the wait is,
|
|
27
51
|
not as a global overlay. -->
|
|
@@ -36,17 +60,42 @@
|
|
|
36
60
|
:sort-mode="sortMode"
|
|
37
61
|
@select="(p) => emit('select', p)"
|
|
38
62
|
@load-children="(p) => emit('loadChildren', p)"
|
|
63
|
+
@create-file="(args) => emit('createFile', args)"
|
|
39
64
|
/>
|
|
40
65
|
</div>
|
|
66
|
+
<!-- Floating context menu (#1598). Position-fixed near the
|
|
67
|
+
click point so it floats above the tree row regardless of
|
|
68
|
+
the scrollable pane. One-shot; clicking the option starts
|
|
69
|
+
the inline input flow above. -->
|
|
70
|
+
<Teleport to="body">
|
|
71
|
+
<div
|
|
72
|
+
v-if="menuOpen"
|
|
73
|
+
class="fixed z-50 min-w-32 bg-white border border-gray-200 rounded shadow-md py-1 text-sm"
|
|
74
|
+
:style="{ top: `${menuY}px`, left: `${menuX}px` }"
|
|
75
|
+
data-testid="file-tree-context-menu"
|
|
76
|
+
@click.stop
|
|
77
|
+
>
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
class="w-full text-left px-3 py-1.5 hover:bg-gray-100 flex items-center gap-2"
|
|
81
|
+
data-testid="file-tree-context-new-file"
|
|
82
|
+
@click="onContextNewFile"
|
|
83
|
+
>
|
|
84
|
+
<span class="material-icons text-sm text-gray-400">note_add</span>
|
|
85
|
+
{{ t("fileTree.newFileMenuItem") }}
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
</Teleport>
|
|
41
89
|
</div>
|
|
42
90
|
</template>
|
|
43
91
|
|
|
44
92
|
<script setup lang="ts">
|
|
45
|
-
import { computed, watch } from "vue";
|
|
93
|
+
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
|
|
46
94
|
import { useI18n } from "vue-i18n";
|
|
47
95
|
import { useExpandedDirs } from "../composables/useExpandedDirs";
|
|
48
96
|
import { sortChildren } from "../utils/files/sortChildren";
|
|
49
97
|
import { descriptorForPath, EDIT_POLICY_ICON_COLOR } from "../config/systemFileDescriptors";
|
|
98
|
+
import { normaliseNewFileSlug, policyForFolder } from "../config/createFilePolicy";
|
|
50
99
|
import type { FileSortMode } from "../composables/useFileSortMode";
|
|
51
100
|
import type { TreeNode } from "../types/fileTree";
|
|
52
101
|
|
|
@@ -74,13 +123,18 @@ const props = defineProps<{
|
|
|
74
123
|
const emit = defineEmits<{
|
|
75
124
|
select: [path: string];
|
|
76
125
|
loadChildren: [path: string];
|
|
126
|
+
// Bubbled up to FilesView, which performs the PUT and refreshes
|
|
127
|
+
// the tree. Per-instance FileTree state is reset by the parent
|
|
128
|
+
// setting `childrenByPath` for this folder; the inline input here
|
|
129
|
+
// closes itself when its commit resolves.
|
|
130
|
+
createFile: [args: { folder: string; filename: string; resolve: (ok: boolean, error?: string) => void }];
|
|
77
131
|
}>();
|
|
78
132
|
|
|
79
133
|
// Expand/collapse state lives in a module-level singleton so every
|
|
80
134
|
// recursive FileTree instance shares it, and survives remounts (e.g.
|
|
81
135
|
// the agent-run refresh that bumps filesRefreshToken in FilesView).
|
|
82
136
|
// Default on first run: only the workspace root ("") is expanded.
|
|
83
|
-
const { isExpanded, toggle } = useExpandedDirs();
|
|
137
|
+
const { isExpanded, toggle, expand } = useExpandedDirs();
|
|
84
138
|
const expanded = computed(() => isExpanded(props.node.path));
|
|
85
139
|
|
|
86
140
|
const cached = computed(() => props.childrenByPath.get(props.node.path));
|
|
@@ -142,4 +196,166 @@ const iconColorClass = computed(() => {
|
|
|
142
196
|
const descriptor = descriptorForPath(props.node.path);
|
|
143
197
|
return descriptor ? EDIT_POLICY_ICON_COLOR[descriptor.editPolicy] : DEFAULT_FILE_ICON_COLOR;
|
|
144
198
|
});
|
|
199
|
+
|
|
200
|
+
// --- Context menu + inline new-file input (#1598) -------------------
|
|
201
|
+
|
|
202
|
+
const createPolicy = computed(() => (props.node.type === "dir" ? policyForFolder(props.node.path) : null));
|
|
203
|
+
const placeholderText = computed(() => (createPolicy.value ? t(createPolicy.value.placeholderKey) : ""));
|
|
204
|
+
|
|
205
|
+
const menuOpen = ref(false);
|
|
206
|
+
const menuX = ref(0);
|
|
207
|
+
const menuY = ref(0);
|
|
208
|
+
const createPending = ref(false);
|
|
209
|
+
const newFileSlug = ref("");
|
|
210
|
+
const createError = ref<string | null>(null);
|
|
211
|
+
const newFileInputRef = ref<HTMLInputElement | null>(null);
|
|
212
|
+
// `blur` would close the input on every focus change. We mute it for
|
|
213
|
+
// a single tick when the user clicks the trailing extension label or
|
|
214
|
+
// re-focuses the field programmatically, so the cancel-on-blur path
|
|
215
|
+
// doesn't fight legitimate re-focus.
|
|
216
|
+
let suppressBlur = false;
|
|
217
|
+
|
|
218
|
+
function onFolderContextMenu(event: MouseEvent): void {
|
|
219
|
+
if (!createPolicy.value) return;
|
|
220
|
+
event.preventDefault();
|
|
221
|
+
menuX.value = event.clientX;
|
|
222
|
+
menuY.value = event.clientY;
|
|
223
|
+
menuOpen.value = true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function closeMenu(): void {
|
|
227
|
+
menuOpen.value = false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function onContextNewFile(): void {
|
|
231
|
+
closeMenu();
|
|
232
|
+
if (!createPolicy.value) return;
|
|
233
|
+
// Make sure the folder is open so the inline input is visible.
|
|
234
|
+
if (!isExpanded(props.node.path)) expand(props.node.path);
|
|
235
|
+
newFileSlug.value = "";
|
|
236
|
+
createError.value = null;
|
|
237
|
+
createPending.value = true;
|
|
238
|
+
void nextTick(() => {
|
|
239
|
+
newFileInputRef.value?.focus();
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function cancelCreate(): void {
|
|
244
|
+
createPending.value = false;
|
|
245
|
+
newFileSlug.value = "";
|
|
246
|
+
createError.value = null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function onInputBlur(): void {
|
|
250
|
+
if (suppressBlur) {
|
|
251
|
+
suppressBlur = false;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
// Cancel on blur — matches Finder/VSCode's "click away to discard"
|
|
255
|
+
// behaviour. Submit still happens on Enter explicitly.
|
|
256
|
+
cancelCreate();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const composingFlag = ref(false);
|
|
260
|
+
function onCompositionStart(): void {
|
|
261
|
+
composingFlag.value = true;
|
|
262
|
+
}
|
|
263
|
+
function onCompositionEnd(): void {
|
|
264
|
+
// Defer clearing so a keydown Enter that fires immediately after
|
|
265
|
+
// compositionend (Chromium IME-commit behaviour) still sees the
|
|
266
|
+
// flag true and is suppressed.
|
|
267
|
+
setTimeout(() => {
|
|
268
|
+
composingFlag.value = false;
|
|
269
|
+
}, 0);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function onInputKeydown(event: KeyboardEvent): void {
|
|
273
|
+
// Enter / Escape are wired here (not via Vue's `.enter` / `.esc`
|
|
274
|
+
// modifiers) because those modifiers were not firing for the user
|
|
275
|
+
// in #1598. Reading `event.key` directly matches the spec.
|
|
276
|
+
//
|
|
277
|
+
// IME guard: the Enter that commits a Japanese / Chinese / Korean
|
|
278
|
+
// composition fires keydown with either `isComposing === true`
|
|
279
|
+
// (Firefox) or `keyCode === 229` (Chrome / Safari). Without the
|
|
280
|
+
// 229 fallback the user gets an empty-filename error the moment
|
|
281
|
+
// they confirm an IME candidate. `composingFlag` covers the
|
|
282
|
+
// "compositionend just fired but the trailing keyup hasn't" gap
|
|
283
|
+
// some browsers expose.
|
|
284
|
+
if (event.key === "Enter") {
|
|
285
|
+
if (composingFlag.value || event.isComposing || event.keyCode === 229) return;
|
|
286
|
+
event.preventDefault();
|
|
287
|
+
onNewFileSubmit();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (event.key === "Escape") {
|
|
291
|
+
event.preventDefault();
|
|
292
|
+
cancelCreate();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function refocusInput(): void {
|
|
297
|
+
// Arm `suppressBlur` for the SINGLE blur that the focus() call
|
|
298
|
+
// may produce before settling, then clear it so the next real
|
|
299
|
+
// click-away triggers cancel-on-blur as expected. Without the
|
|
300
|
+
// post-focus clear the flag stays armed after a validation /
|
|
301
|
+
// save failure and silently swallows the user's next blur
|
|
302
|
+
// (CodeRabbit review on #1608).
|
|
303
|
+
suppressBlur = true;
|
|
304
|
+
void nextTick(() => {
|
|
305
|
+
newFileInputRef.value?.focus();
|
|
306
|
+
setTimeout(() => {
|
|
307
|
+
suppressBlur = false;
|
|
308
|
+
}, 0);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function onNewFileSubmit(): void {
|
|
313
|
+
if (!createPolicy.value) return;
|
|
314
|
+
const result = normaliseNewFileSlug(newFileSlug.value, createPolicy.value);
|
|
315
|
+
if (!result.ok) {
|
|
316
|
+
createError.value = result.reason === "empty" ? t("fileTree.newFileError.empty") : t("fileTree.newFileError.unsafe");
|
|
317
|
+
refocusInput();
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
// Keep the input open until the parent's PUT resolves so a save
|
|
321
|
+
// failure leaves the user where they were typing. The parent
|
|
322
|
+
// hands back ok / error via the `resolve` callback.
|
|
323
|
+
suppressBlur = true;
|
|
324
|
+
emit("createFile", {
|
|
325
|
+
folder: props.node.path,
|
|
326
|
+
filename: result.filename,
|
|
327
|
+
resolve: (ok, error) => {
|
|
328
|
+
if (ok) {
|
|
329
|
+
cancelCreate();
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
createError.value = error ?? t("fileTree.newFileError.saveFailed");
|
|
333
|
+
refocusInput();
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Global click closes the menu — Teleport puts it on body, so a
|
|
339
|
+
// click outside the menu but inside the tree pane will still trigger
|
|
340
|
+
// this. The Teleport's @click.stop above keeps clicks inside the
|
|
341
|
+
// menu from being treated as outside.
|
|
342
|
+
function onWindowClick(): void {
|
|
343
|
+
if (menuOpen.value) closeMenu();
|
|
344
|
+
}
|
|
345
|
+
function onWindowKeydown(event: KeyboardEvent): void {
|
|
346
|
+
if (event.key === "Escape" && menuOpen.value) closeMenu();
|
|
347
|
+
}
|
|
348
|
+
watch(menuOpen, (open) => {
|
|
349
|
+
if (open) {
|
|
350
|
+
window.addEventListener("click", onWindowClick);
|
|
351
|
+
window.addEventListener("keydown", onWindowKeydown);
|
|
352
|
+
} else {
|
|
353
|
+
window.removeEventListener("click", onWindowClick);
|
|
354
|
+
window.removeEventListener("keydown", onWindowKeydown);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
onBeforeUnmount(() => {
|
|
358
|
+
window.removeEventListener("click", onWindowClick);
|
|
359
|
+
window.removeEventListener("keydown", onWindowKeydown);
|
|
360
|
+
});
|
|
145
361
|
</script>
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
:sort-mode="sortMode"
|
|
41
41
|
@select="emit('select', $event)"
|
|
42
42
|
@load-children="emit('loadChildren', $event)"
|
|
43
|
+
@create-file="emit('createFile', $event)"
|
|
43
44
|
/>
|
|
44
45
|
<template v-if="refRoots.length > 0">
|
|
45
46
|
<div class="mt-2 pt-2 border-t border-gray-200 px-1 mb-1 flex items-center gap-1">
|
|
@@ -83,6 +84,7 @@ const emit = defineEmits<{
|
|
|
83
84
|
select: [path: string];
|
|
84
85
|
loadChildren: [path: string];
|
|
85
86
|
"update:sortMode": [mode: FileSortMode];
|
|
87
|
+
createFile: [args: { folder: string; filename: string; resolve: (ok: boolean, error?: string) => void }];
|
|
86
88
|
}>();
|
|
87
89
|
|
|
88
90
|
// Shared empty set for reference roots (they don't highlight recents).
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="h-full flex bg-white">
|
|
2
|
+
<div class="h-full flex bg-white" data-testid="files-view-root">
|
|
3
3
|
<FileTreePane
|
|
4
|
+
ref="treePaneRef"
|
|
4
5
|
:root-node="rootNode"
|
|
5
6
|
:ref-roots="refRoots"
|
|
6
7
|
:children-by-path="childrenByPath"
|
|
@@ -11,6 +12,7 @@
|
|
|
11
12
|
@select="selectFile"
|
|
12
13
|
@load-children="loadDirChildren"
|
|
13
14
|
@update:sort-mode="setSortMode"
|
|
15
|
+
@create-file="handleCreateFile"
|
|
14
16
|
/>
|
|
15
17
|
<!-- Content pane -->
|
|
16
18
|
<div class="flex-1 flex flex-col min-w-0 overflow-hidden">
|
|
@@ -28,8 +30,6 @@
|
|
|
28
30
|
:content="content"
|
|
29
31
|
:content-error="contentError"
|
|
30
32
|
:content-loading="contentLoading"
|
|
31
|
-
:scheduler-result="schedulerResult"
|
|
32
|
-
:todo-explorer-result="todoExplorerResult"
|
|
33
33
|
:is-markdown="isMarkdown"
|
|
34
34
|
:is-html="isHtml"
|
|
35
35
|
:is-json="isJson"
|
|
@@ -74,10 +74,8 @@ import { useMarkdownMode } from "../composables/useMarkdownMode";
|
|
|
74
74
|
import { useFileSortMode } from "../composables/useFileSortMode";
|
|
75
75
|
import { useContentDisplay } from "../composables/useContentDisplay";
|
|
76
76
|
import { useMarkdownLinkHandler } from "../composables/useMarkdownLinkHandler";
|
|
77
|
-
import { apiPut } from "../utils/api";
|
|
77
|
+
import { apiPost, apiPut } from "../utils/api";
|
|
78
78
|
import { API_ROUTES } from "../config/apiRoutes";
|
|
79
|
-
import { toSchedulerResult } from "../utils/filesPreview/schedulerPreview";
|
|
80
|
-
import { toTodoExplorerResult } from "../utils/filesPreview/todoPreview";
|
|
81
79
|
|
|
82
80
|
const RECENT_THRESHOLD_MS = 60 * 1000;
|
|
83
81
|
|
|
@@ -95,7 +93,7 @@ const emit = defineEmits<{
|
|
|
95
93
|
loadSession: [sessionId: string];
|
|
96
94
|
}>();
|
|
97
95
|
|
|
98
|
-
const { rootNode, refRoots, childrenByPath, treeError, loadDirChildren, ensureAncestorsLoaded, reloadRoot, loadRefRoots } = useFileTree();
|
|
96
|
+
const { rootNode, refRoots, childrenByPath, treeError, loadDirChildren, ensureAncestorsLoaded, reloadDirChildren, reloadRoot, loadRefRoots } = useFileTree();
|
|
99
97
|
|
|
100
98
|
const { selectedPath, content, contentLoading, contentError, loadContent, selectFile, deselectFile, abortContent } = useFileSelection();
|
|
101
99
|
|
|
@@ -149,9 +147,47 @@ watch(content, () => {
|
|
|
149
147
|
rawSaveError.value = null;
|
|
150
148
|
});
|
|
151
149
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
150
|
+
// #1598 — folder-row context menu → "New file" inline flow. The
|
|
151
|
+
// FileTree component handles input + slug validation; here we
|
|
152
|
+
// only do the conflict check + PUT + refresh. The callback shape
|
|
153
|
+
// lets the inline input close itself on success and stay open
|
|
154
|
+
// (with the error label) on failure.
|
|
155
|
+
async function handleCreateFile(args: { folder: string; filename: string; resolve: (ok: boolean, error?: string) => void }): Promise<void> {
|
|
156
|
+
const { folder, filename, resolve } = args;
|
|
157
|
+
const targetPath = folder ? `${folder}/${filename}` : filename;
|
|
158
|
+
// Client-side conflict pre-check via the local cache — cheap and
|
|
159
|
+
// matches what the user sees. The server's create endpoint also
|
|
160
|
+
// refuses on conflict (#1598), so a tab racing with another that
|
|
161
|
+
// already won would still get a 409 below — this just turns the
|
|
162
|
+
// common case into a localised inline error without a round-trip.
|
|
163
|
+
const cached = childrenByPath.value.get(folder);
|
|
164
|
+
if (Array.isArray(cached) && cached.some((child) => child.name === filename)) {
|
|
165
|
+
resolve(false, t("fileTree.newFileError.exists", { filename }));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const result = await apiPost<{ path: string; size: number; modifiedMs: number }>(API_ROUTES.files.create, {
|
|
169
|
+
path: targetPath,
|
|
170
|
+
content: "",
|
|
171
|
+
});
|
|
172
|
+
if (!result.ok) {
|
|
173
|
+
// Map HTTP status to a localised message so the inline error
|
|
174
|
+
// matches the rest of the menu's language. 409 = a race lost
|
|
175
|
+
// to another tab/agent that just created the same file.
|
|
176
|
+
if (result.status === 409) {
|
|
177
|
+
resolve(false, t("fileTree.newFileError.exists", { filename }));
|
|
178
|
+
await reloadDirChildren(folder);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
resolve(false, t("fileTree.newFileError.saveFailed"));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
resolve(true);
|
|
185
|
+
await reloadDirChildren(folder);
|
|
186
|
+
// Reveal the new file in the right-hand content pane so the user
|
|
187
|
+
// can start editing immediately. selectFile() also drives the URL
|
|
188
|
+
// bar + ancestor expansion via the existing watcher.
|
|
189
|
+
selectFile(result.data.path);
|
|
190
|
+
}
|
|
155
191
|
|
|
156
192
|
const recentPaths = computed(() => {
|
|
157
193
|
const set = new Set<string>();
|
|
@@ -197,6 +233,50 @@ watch(
|
|
|
197
233
|
},
|
|
198
234
|
);
|
|
199
235
|
|
|
236
|
+
// Keep the tree expanded down to the active selection regardless of
|
|
237
|
+
// how the selection changed (URL bar, back/forward, selectFile from a
|
|
238
|
+
// markdown link). selectFile() updates selectedPath synchronously
|
|
239
|
+
// before pushing the route, so a guard on the route watcher would
|
|
240
|
+
// miss in-app file→file navigation — we watch the source of truth
|
|
241
|
+
// directly. `immediate: true` covers the deep-link mount case, so
|
|
242
|
+
// onMounted doesn't need its own ensureAncestorsLoaded call.
|
|
243
|
+
// Idempotent: loadDirChildren and expand() both short-circuit when
|
|
244
|
+
// the cache/expand-state already has the ancestor.
|
|
245
|
+
watch(
|
|
246
|
+
selectedPath,
|
|
247
|
+
(newPath) => {
|
|
248
|
+
if (newPath) ensureAncestorsLoaded(newPath);
|
|
249
|
+
},
|
|
250
|
+
{ immediate: true },
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// Reveal the selected file row in the tree pane. The tree grows
|
|
254
|
+
// incrementally on deep-link mount: ensureAncestorsLoaded fetches the
|
|
255
|
+
// direct ancestors, but sibling dirs whose `expanded` state was
|
|
256
|
+
// restored from localStorage lazy-load their children later via each
|
|
257
|
+
// FileTree's own watcher. Each of those loads pushes the selected
|
|
258
|
+
// row further down, so scrollIntoView must re-run whenever the tree
|
|
259
|
+
// grows, not just once on selection change. A pending-rAF guard
|
|
260
|
+
// coalesces a burst of childrenByPath updates into a single scroll.
|
|
261
|
+
// Scope the query to the FileTreePane's root via a template ref so
|
|
262
|
+
// it survives data-testid / DOM-structure changes elsewhere in
|
|
263
|
+
// FilesView.
|
|
264
|
+
const treePaneRef = ref<InstanceType<typeof FileTreePane> | null>(null);
|
|
265
|
+
let pendingRevealRaf = 0;
|
|
266
|
+
function revealSelectedInTree(): void {
|
|
267
|
+
if (!selectedPath.value) return;
|
|
268
|
+
if (pendingRevealRaf !== 0) return;
|
|
269
|
+
pendingRevealRaf = requestAnimationFrame(() => {
|
|
270
|
+
pendingRevealRaf = 0;
|
|
271
|
+
const paneRoot = treePaneRef.value?.$el as HTMLElement | undefined;
|
|
272
|
+
const button = paneRoot?.querySelector<HTMLElement>('button[data-selected="true"]');
|
|
273
|
+
button?.scrollIntoView({ block: "nearest" });
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
watch(selectedPath, revealSelectedInTree);
|
|
278
|
+
watch(childrenByPath, revealSelectedInTree);
|
|
279
|
+
|
|
200
280
|
watch(
|
|
201
281
|
() => props.refreshToken,
|
|
202
282
|
() => {
|
|
@@ -209,16 +289,14 @@ onMounted(async () => {
|
|
|
209
289
|
await loadDirChildren("");
|
|
210
290
|
await loadRefRoots();
|
|
211
291
|
|
|
212
|
-
// Deep-link
|
|
213
|
-
// by
|
|
214
|
-
//
|
|
215
|
-
if (selectedPath.value)
|
|
216
|
-
await ensureAncestorsLoaded(selectedPath.value);
|
|
217
|
-
loadContent(selectedPath.value);
|
|
218
|
-
}
|
|
292
|
+
// Deep-link content load. The ancestor expansion + scroll reveal are
|
|
293
|
+
// handled by the selectedPath watchers above (the ensureAncestorsLoaded
|
|
294
|
+
// watcher runs with immediate: true).
|
|
295
|
+
if (selectedPath.value) loadContent(selectedPath.value);
|
|
219
296
|
});
|
|
220
297
|
|
|
221
298
|
onUnmounted(() => {
|
|
222
299
|
abortContent();
|
|
300
|
+
if (pendingRevealRaf !== 0) cancelAnimationFrame(pendingRevealRaf);
|
|
223
301
|
});
|
|
224
302
|
</script>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<button
|
|
3
|
+
type="button"
|
|
4
|
+
:class="[
|
|
5
|
+
'h-8 w-8 flex items-center justify-center rounded transition-colors',
|
|
6
|
+
pinned ? 'text-amber-500 hover:bg-amber-50' : 'text-slate-400 hover:bg-slate-100 hover:text-slate-600',
|
|
7
|
+
]"
|
|
8
|
+
:title="pinned ? t('shortcuts.unpin') : t('shortcuts.pin')"
|
|
9
|
+
:aria-label="pinned ? t('shortcuts.unpin') : t('shortcuts.pin')"
|
|
10
|
+
:aria-pressed="pinned"
|
|
11
|
+
:data-testid="`pin-toggle-${kind}-${slug}`"
|
|
12
|
+
@click.stop="toggle"
|
|
13
|
+
@keydown.enter.stop
|
|
14
|
+
@keydown.space.stop
|
|
15
|
+
>
|
|
16
|
+
<span class="material-icons text-lg">{{ pinned ? "star" : "star_border" }}</span>
|
|
17
|
+
</button>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup lang="ts">
|
|
21
|
+
import { computed } from "vue";
|
|
22
|
+
import { useI18n } from "vue-i18n";
|
|
23
|
+
import { useShortcuts } from "../composables/useShortcuts";
|
|
24
|
+
import type { ShortcutKind } from "../types/shortcuts";
|
|
25
|
+
|
|
26
|
+
// Shared ★ toggle used by the collections / feeds index cards and the
|
|
27
|
+
// individual view header. Talks to the `useShortcuts` singleton itself,
|
|
28
|
+
// so a parent only supplies the target's identity + cached label/icon.
|
|
29
|
+
// Click + keyboard activation are stopped so toggling the star never
|
|
30
|
+
// also opens the underlying card.
|
|
31
|
+
|
|
32
|
+
const props = defineProps<{
|
|
33
|
+
kind: ShortcutKind;
|
|
34
|
+
slug: string;
|
|
35
|
+
/** Cached at pin time so the launcher renders without re-fetching. */
|
|
36
|
+
title: string;
|
|
37
|
+
icon: string;
|
|
38
|
+
}>();
|
|
39
|
+
|
|
40
|
+
const { t } = useI18n();
|
|
41
|
+
const { isPinned, pin, unpin } = useShortcuts();
|
|
42
|
+
|
|
43
|
+
const pinned = computed(() => isPinned(props.kind, props.slug));
|
|
44
|
+
|
|
45
|
+
function toggle(): void {
|
|
46
|
+
if (pinned.value) {
|
|
47
|
+
void unpin(props.kind, props.slug);
|
|
48
|
+
} else {
|
|
49
|
+
void pin({ kind: props.kind, slug: props.slug, title: props.title, icon: props.icon });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
</script>
|