mulmoclaude 0.1.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/README.md +44 -0
- package/bin/mulmoclaude.js +202 -0
- package/bin/prepare-dist.js +93 -0
- package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +1 -0
- package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +5 -0
- package/client/assets/index-D8rhwXLq.js +4906 -0
- package/client/assets/index-KNLBjwuh.css +1 -0
- package/client/assets/index.es-D4YyL_Dg-BfRHLTZV.js +5 -0
- package/client/assets/material-icons-Dr0goTwe.woff +0 -0
- package/client/assets/material-icons-kAwBdRge.woff2 +0 -0
- package/client/assets/material-icons-outlined-BpWbwl2n.woff +0 -0
- package/client/assets/material-icons-outlined-DZhiGvEA.woff2 +0 -0
- package/client/assets/material-icons-round-BDlwx-sv.woff +0 -0
- package/client/assets/material-icons-round-DrirKXBx.woff2 +0 -0
- package/client/assets/material-icons-sharp-CH1KkVu7.woff +0 -0
- package/client/assets/material-icons-sharp-gidztirS.woff2 +0 -0
- package/client/assets/material-icons-two-tone-B7wz7mED.woff +0 -0
- package/client/assets/material-icons-two-tone-DuNIpaEj.woff2 +0 -0
- package/client/assets/mulmo_bw-ERmkSv0a.png +0 -0
- package/client/assets/purify.es-Fx1Nqyry-PeS5RUhs.js +2 -0
- package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +1 -0
- package/client/index.html +28 -0
- package/package.json +66 -0
- package/server/agent/attachmentConverter.ts +270 -0
- package/server/agent/config.ts +414 -0
- package/server/agent/index.ts +260 -0
- package/server/agent/mcp-server.ts +412 -0
- package/server/agent/mcp-tools/index.ts +63 -0
- package/server/agent/mcp-tools/x.ts +188 -0
- package/server/agent/plugin-names.ts +75 -0
- package/server/agent/prompt.ts +349 -0
- package/server/agent/resumeFailover.ts +129 -0
- package/server/agent/sandboxMounts.ts +329 -0
- package/server/agent/stream.ts +194 -0
- package/server/api/auth/bearerAuth.ts +61 -0
- package/server/api/auth/token.ts +98 -0
- package/server/api/csrfGuard.ts +85 -0
- package/server/api/routes/agent.ts +478 -0
- package/server/api/routes/chart.ts +98 -0
- package/server/api/routes/chat-index.ts +46 -0
- package/server/api/routes/config.ts +258 -0
- package/server/api/routes/dispatchResponse.ts +79 -0
- package/server/api/routes/files.ts +812 -0
- package/server/api/routes/html.ts +101 -0
- package/server/api/routes/image.ts +169 -0
- package/server/api/routes/mulmo-script.ts +712 -0
- package/server/api/routes/mulmoScriptValidate.ts +101 -0
- package/server/api/routes/notifications.ts +69 -0
- package/server/api/routes/pdf.ts +163 -0
- package/server/api/routes/plugins.ts +276 -0
- package/server/api/routes/presentHtml.ts +48 -0
- package/server/api/routes/roles.ts +125 -0
- package/server/api/routes/scheduler.ts +153 -0
- package/server/api/routes/schedulerHandlers.ts +151 -0
- package/server/api/routes/schedulerTasks.ts +163 -0
- package/server/api/routes/sessions.ts +294 -0
- package/server/api/routes/sessionsCursor.ts +59 -0
- package/server/api/routes/skills.ts +195 -0
- package/server/api/routes/sources.ts +540 -0
- package/server/api/routes/todos.ts +263 -0
- package/server/api/routes/todosColumnsHandlers.ts +347 -0
- package/server/api/routes/todosHandlers.ts +274 -0
- package/server/api/routes/todosItemsHandlers.ts +386 -0
- package/server/api/routes/wiki/pageIndex.ts +53 -0
- package/server/api/routes/wiki.ts +363 -0
- package/server/api/sandboxStatus.ts +64 -0
- package/server/events/notifications.ts +160 -0
- package/server/events/pub-sub/index.ts +45 -0
- package/server/events/relay-client.ts +288 -0
- package/server/events/scheduler-adapter.ts +302 -0
- package/server/events/session-store/index.ts +492 -0
- package/server/events/task-manager/index.ts +181 -0
- package/server/index.ts +572 -0
- package/server/system/config.ts +243 -0
- package/server/system/credentials.ts +220 -0
- package/server/system/docker.ts +97 -0
- package/server/system/env.ts +109 -0
- package/server/system/logger/config.ts +112 -0
- package/server/system/logger/formatters.ts +40 -0
- package/server/system/logger/index.ts +53 -0
- package/server/system/logger/rotation.ts +37 -0
- package/server/system/logger/sinks.ts +101 -0
- package/server/system/logger/types.ts +29 -0
- package/server/utils/date.ts +57 -0
- package/server/utils/errors.ts +7 -0
- package/server/utils/fetch.ts +27 -0
- package/server/utils/files/atomic.ts +125 -0
- package/server/utils/files/html-io.ts +20 -0
- package/server/utils/files/image-store.ts +66 -0
- package/server/utils/files/index.ts +45 -0
- package/server/utils/files/journal-io.ts +213 -0
- package/server/utils/files/json.ts +69 -0
- package/server/utils/files/markdown-store.ts +33 -0
- package/server/utils/files/naming.ts +50 -0
- package/server/utils/files/reference-dirs-io.ts +45 -0
- package/server/utils/files/roles-io.ts +45 -0
- package/server/utils/files/safe.ts +106 -0
- package/server/utils/files/scheduler-io.ts +20 -0
- package/server/utils/files/scheduler-overrides-io.ts +64 -0
- package/server/utils/files/session-io.ts +136 -0
- package/server/utils/files/spreadsheet-store.ts +63 -0
- package/server/utils/files/todos-io.ts +29 -0
- package/server/utils/files/user-tasks-io.ts +25 -0
- package/server/utils/files/workspace-io.ts +221 -0
- package/server/utils/gemini.ts +59 -0
- package/server/utils/gitignore.ts +69 -0
- package/server/utils/http.ts +15 -0
- package/server/utils/httpError.ts +61 -0
- package/server/utils/id.ts +16 -0
- package/server/utils/json.ts +83 -0
- package/server/utils/logBackgroundError.ts +22 -0
- package/server/utils/markdown.ts +82 -0
- package/server/utils/request.ts +29 -0
- package/server/utils/slug.ts +50 -0
- package/server/utils/spawn.ts +62 -0
- package/server/utils/time.ts +34 -0
- package/server/utils/types.ts +47 -0
- package/server/workspace/chat-index/index.ts +153 -0
- package/server/workspace/chat-index/indexer.ts +209 -0
- package/server/workspace/chat-index/paths.ts +34 -0
- package/server/workspace/chat-index/summarizer.ts +247 -0
- package/server/workspace/chat-index/types.ts +38 -0
- package/server/workspace/custom-dirs.ts +220 -0
- package/server/workspace/helps/business.md +104 -0
- package/server/workspace/helps/github.md +23 -0
- package/server/workspace/helps/index.md +60 -0
- package/server/workspace/helps/mulmoscript.md +249 -0
- package/server/workspace/helps/sandbox.md +90 -0
- package/server/workspace/helps/spreadsheet.md +43 -0
- package/server/workspace/helps/telegram.md +135 -0
- package/server/workspace/helps/wiki.md +131 -0
- package/server/workspace/journal/archivist.ts +386 -0
- package/server/workspace/journal/dailyPass.ts +743 -0
- package/server/workspace/journal/diff.ts +71 -0
- package/server/workspace/journal/index.ts +185 -0
- package/server/workspace/journal/indexFile.ts +136 -0
- package/server/workspace/journal/linkRewrite.ts +4 -0
- package/server/workspace/journal/memoryExtractor.ts +130 -0
- package/server/workspace/journal/optimizationPass.ts +160 -0
- package/server/workspace/journal/paths.ts +76 -0
- package/server/workspace/journal/state.ts +125 -0
- package/server/workspace/paths.ts +158 -0
- package/server/workspace/reference-dirs.ts +252 -0
- package/server/workspace/roles.ts +37 -0
- package/server/workspace/skills/discovery.ts +125 -0
- package/server/workspace/skills/index.ts +10 -0
- package/server/workspace/skills/parser.ts +144 -0
- package/server/workspace/skills/paths.ts +41 -0
- package/server/workspace/skills/scheduler.ts +149 -0
- package/server/workspace/skills/types.ts +30 -0
- package/server/workspace/skills/user-tasks.ts +257 -0
- package/server/workspace/skills/writer.ts +189 -0
- package/server/workspace/sources/arxivDiscovery.ts +182 -0
- package/server/workspace/sources/classifier.ts +268 -0
- package/server/workspace/sources/fetchers/arxiv.ts +170 -0
- package/server/workspace/sources/fetchers/github.ts +106 -0
- package/server/workspace/sources/fetchers/githubIssues.ts +208 -0
- package/server/workspace/sources/fetchers/githubReleases.ts +186 -0
- package/server/workspace/sources/fetchers/index.ts +71 -0
- package/server/workspace/sources/fetchers/registerAll.ts +15 -0
- package/server/workspace/sources/fetchers/rss.ts +141 -0
- package/server/workspace/sources/fetchers/rssParser.ts +295 -0
- package/server/workspace/sources/httpFetcher.ts +230 -0
- package/server/workspace/sources/interests.ts +120 -0
- package/server/workspace/sources/paths.ts +110 -0
- package/server/workspace/sources/pipeline/dedup.ts +60 -0
- package/server/workspace/sources/pipeline/fetch.ts +136 -0
- package/server/workspace/sources/pipeline/index.ts +249 -0
- package/server/workspace/sources/pipeline/notify.ts +72 -0
- package/server/workspace/sources/pipeline/plan.ts +66 -0
- package/server/workspace/sources/pipeline/summarize.ts +189 -0
- package/server/workspace/sources/pipeline/write.ts +185 -0
- package/server/workspace/sources/rateLimiter.ts +148 -0
- package/server/workspace/sources/registry.ts +326 -0
- package/server/workspace/sources/robots.ts +271 -0
- package/server/workspace/sources/sourceState.ts +135 -0
- package/server/workspace/sources/taxonomy.ts +74 -0
- package/server/workspace/sources/types.ts +144 -0
- package/server/workspace/sources/urls.ts +112 -0
- package/server/workspace/tool-trace/classify.ts +114 -0
- package/server/workspace/tool-trace/index.ts +250 -0
- package/server/workspace/tool-trace/writeSearch.ts +98 -0
- package/server/workspace/wiki-backlinks/index.ts +107 -0
- package/server/workspace/wiki-backlinks/sessionBacklinks.ts +144 -0
- package/server/workspace/workspace.ts +66 -0
- package/src/App.vue +720 -0
- package/src/assets/mulmo_bw.png +0 -0
- package/src/components/CanvasViewToggle.vue +27 -0
- package/src/components/ChatAttachmentPreview.vue +45 -0
- package/src/components/ChatImagePreview.vue +17 -0
- package/src/components/ChatInput.vue +208 -0
- package/src/components/FileContentHeader.vue +49 -0
- package/src/components/FileContentRenderer.vue +162 -0
- package/src/components/FileTree.vue +115 -0
- package/src/components/FileTreePane.vue +85 -0
- package/src/components/FilesView.vue +206 -0
- package/src/components/LockStatusPopup.vue +111 -0
- package/src/components/NotificationBell.vue +131 -0
- package/src/components/NotificationToast.vue +72 -0
- package/src/components/PluginLauncher.vue +138 -0
- package/src/components/RightSidebar.vue +113 -0
- package/src/components/RoleSelector.vue +64 -0
- package/src/components/SessionHistoryPanel.vue +176 -0
- package/src/components/SessionTabBar.vue +81 -0
- package/src/components/SettingsMcpTab.vue +350 -0
- package/src/components/SettingsModal.vue +275 -0
- package/src/components/SettingsReferenceDirsTab.vue +173 -0
- package/src/components/SettingsWorkspaceDirsTab.vue +174 -0
- package/src/components/SidebarHeader.vue +69 -0
- package/src/components/StackView.vue +360 -0
- package/src/components/SuggestionsPanel.vue +65 -0
- package/src/components/TodoExplorer.vue +358 -0
- package/src/components/ToolResultsPanel.vue +77 -0
- package/src/components/todo/TodoAddDialog.vue +131 -0
- package/src/components/todo/TodoEditDialog.vue +47 -0
- package/src/components/todo/TodoEditPanel.vue +113 -0
- package/src/components/todo/TodoKanbanView.vue +249 -0
- package/src/components/todo/TodoListView.vue +79 -0
- package/src/components/todo/TodoTableView.vue +177 -0
- package/src/composables/useActiveSession.ts +40 -0
- package/src/composables/useAppApi.ts +45 -0
- package/src/composables/useCanvasViewMode.ts +121 -0
- package/src/composables/useChatScroll.ts +47 -0
- package/src/composables/useClickOutside.ts +26 -0
- package/src/composables/useClipboardCopy.ts +44 -0
- package/src/composables/useContentDisplay.ts +52 -0
- package/src/composables/useDebugBeat.ts +23 -0
- package/src/composables/useDynamicFavicon.ts +115 -0
- package/src/composables/useEventListeners.ts +42 -0
- package/src/composables/useExpandedDirs.ts +64 -0
- package/src/composables/useFaviconState.ts +30 -0
- package/src/composables/useFileSelection.ts +115 -0
- package/src/composables/useFileSortMode.ts +24 -0
- package/src/composables/useFileTree.ts +85 -0
- package/src/composables/useFreshPluginData.ts +89 -0
- package/src/composables/useHealth.ts +38 -0
- package/src/composables/useImeAwareEnter.ts +57 -0
- package/src/composables/useKeyNavigation.ts +60 -0
- package/src/composables/useMarkdownLinkHandler.ts +46 -0
- package/src/composables/useMarkdownMode.ts +17 -0
- package/src/composables/useMcpTools.ts +71 -0
- package/src/composables/useMergedSessions.ts +27 -0
- package/src/composables/useNotifications.ts +90 -0
- package/src/composables/usePdfDownload.ts +60 -0
- package/src/composables/usePendingCalls.ts +77 -0
- package/src/composables/usePubSub.ts +85 -0
- package/src/composables/useRightSidebar.ts +23 -0
- package/src/composables/useRoles.ts +34 -0
- package/src/composables/useSandboxStatus.ts +67 -0
- package/src/composables/useSelectedResult.ts +49 -0
- package/src/composables/useSessionDerived.ts +51 -0
- package/src/composables/useSessionHistory.ts +81 -0
- package/src/composables/useSessionSync.ts +57 -0
- package/src/composables/useViewLayout.ts +55 -0
- package/src/config/apiRoutes.ts +173 -0
- package/src/config/pubsubChannels.ts +45 -0
- package/src/config/roles.ts +335 -0
- package/src/config/schedulerActions.ts +25 -0
- package/src/config/toolNames.ts +71 -0
- package/src/config/workspacePaths.ts +24 -0
- package/src/index.css +107 -0
- package/src/main.ts +25 -0
- package/src/plugins/canvas/Preview.vue +13 -0
- package/src/plugins/canvas/View.vue +333 -0
- package/src/plugins/canvas/definition.ts +38 -0
- package/src/plugins/canvas/index.ts +36 -0
- package/src/plugins/chart/Preview.vue +49 -0
- package/src/plugins/chart/View.vue +143 -0
- package/src/plugins/chart/definition.ts +58 -0
- package/src/plugins/chart/index.ts +52 -0
- package/src/plugins/editImage/Preview.vue +13 -0
- package/src/plugins/editImage/View.vue +13 -0
- package/src/plugins/editImage/definition.ts +27 -0
- package/src/plugins/editImage/index.ts +36 -0
- package/src/plugins/generateImage/Preview.vue +13 -0
- package/src/plugins/generateImage/View.vue +33 -0
- package/src/plugins/generateImage/definition.ts +32 -0
- package/src/plugins/generateImage/index.ts +56 -0
- package/src/plugins/manageRoles/Preview.vue +49 -0
- package/src/plugins/manageRoles/View.vue +525 -0
- package/src/plugins/manageRoles/definition.ts +43 -0
- package/src/plugins/manageRoles/index.ts +47 -0
- package/src/plugins/manageSkills/Preview.vue +21 -0
- package/src/plugins/manageSkills/View.vue +321 -0
- package/src/plugins/manageSkills/definition.ts +49 -0
- package/src/plugins/manageSkills/index.ts +49 -0
- package/src/plugins/manageSource/Preview.vue +33 -0
- package/src/plugins/manageSource/View.vue +697 -0
- package/src/plugins/manageSource/definition.ts +63 -0
- package/src/plugins/manageSource/index.ts +66 -0
- package/src/plugins/markdown/Preview.vue +77 -0
- package/src/plugins/markdown/View.vue +476 -0
- package/src/plugins/markdown/definition.ts +50 -0
- package/src/plugins/markdown/index.ts +36 -0
- package/src/plugins/presentHtml/Preview.vue +25 -0
- package/src/plugins/presentHtml/View.vue +52 -0
- package/src/plugins/presentHtml/definition.ts +27 -0
- package/src/plugins/presentHtml/helpers.ts +72 -0
- package/src/plugins/presentHtml/index.ts +41 -0
- package/src/plugins/presentMulmoScript/Preview.vue +23 -0
- package/src/plugins/presentMulmoScript/View.vue +1166 -0
- package/src/plugins/presentMulmoScript/definition.ts +95 -0
- package/src/plugins/presentMulmoScript/helpers.ts +162 -0
- package/src/plugins/presentMulmoScript/index.ts +40 -0
- package/src/plugins/scheduler/Preview.vue +67 -0
- package/src/plugins/scheduler/TasksTab.vue +205 -0
- package/src/plugins/scheduler/View.vue +565 -0
- package/src/plugins/scheduler/definition.ts +57 -0
- package/src/plugins/scheduler/index.ts +45 -0
- package/src/plugins/scheduler/viewModes.ts +26 -0
- package/src/plugins/spreadsheet/Preview.vue +29 -0
- package/src/plugins/spreadsheet/View.vue +997 -0
- package/src/plugins/spreadsheet/cellHighlights.ts +79 -0
- package/src/plugins/spreadsheet/definition.ts +121 -0
- package/src/plugins/spreadsheet/engine/calculator.ts +459 -0
- package/src/plugins/spreadsheet/engine/cellBuilder.ts +81 -0
- package/src/plugins/spreadsheet/engine/date-parser.ts +220 -0
- package/src/plugins/spreadsheet/engine/date-utils.ts +56 -0
- package/src/plugins/spreadsheet/engine/engine.ts +176 -0
- package/src/plugins/spreadsheet/engine/evaluator.ts +390 -0
- package/src/plugins/spreadsheet/engine/formatter.ts +172 -0
- package/src/plugins/spreadsheet/engine/formulaRefs.ts +101 -0
- package/src/plugins/spreadsheet/engine/functions/date.ts +299 -0
- package/src/plugins/spreadsheet/engine/functions/financial.ts +387 -0
- package/src/plugins/spreadsheet/engine/functions/index.ts +16 -0
- package/src/plugins/spreadsheet/engine/functions/logical.ts +262 -0
- package/src/plugins/spreadsheet/engine/functions/lookup.ts +400 -0
- package/src/plugins/spreadsheet/engine/functions/mathematical.ts +297 -0
- package/src/plugins/spreadsheet/engine/functions/statistical.ts +338 -0
- package/src/plugins/spreadsheet/engine/functions/text.ts +389 -0
- package/src/plugins/spreadsheet/engine/index.ts +27 -0
- package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +111 -0
- package/src/plugins/spreadsheet/engine/parser.ts +143 -0
- package/src/plugins/spreadsheet/engine/registry.ts +150 -0
- package/src/plugins/spreadsheet/engine/responseDecoder.ts +67 -0
- package/src/plugins/spreadsheet/engine/types.ts +64 -0
- package/src/plugins/spreadsheet/index.ts +36 -0
- package/src/plugins/textResponse/Preview.vue +94 -0
- package/src/plugins/textResponse/View.vue +503 -0
- package/src/plugins/textResponse/definition.ts +34 -0
- package/src/plugins/textResponse/index.ts +27 -0
- package/src/plugins/textResponse/plugin.ts +29 -0
- package/src/plugins/textResponse/samples.ts +97 -0
- package/src/plugins/textResponse/types.ts +11 -0
- package/src/plugins/todo/Preview.vue +63 -0
- package/src/plugins/todo/View.vue +364 -0
- package/src/plugins/todo/composables/useTodos.ts +177 -0
- package/src/plugins/todo/definition.ts +45 -0
- package/src/plugins/todo/index.ts +61 -0
- package/src/plugins/todo/labels.ts +163 -0
- package/src/plugins/todo/priority.ts +98 -0
- package/src/plugins/todo/viewModes.ts +19 -0
- package/src/plugins/ui-image/ImagePreview.vue +23 -0
- package/src/plugins/ui-image/ImageView.vue +34 -0
- package/src/plugins/ui-image/index.ts +3 -0
- package/src/plugins/ui-image/types.ts +4 -0
- package/src/plugins/wiki/Preview.vue +65 -0
- package/src/plugins/wiki/View.vue +342 -0
- package/src/plugins/wiki/definition.ts +25 -0
- package/src/plugins/wiki/helpers.ts +59 -0
- package/src/plugins/wiki/index.ts +52 -0
- package/src/router/guards.ts +61 -0
- package/src/router/index.ts +50 -0
- package/src/tools/index.ts +52 -0
- package/src/tools/types.ts +27 -0
- package/src/types/events.ts +16 -0
- package/src/types/fileTree.ts +13 -0
- package/src/types/notification.ts +67 -0
- package/src/types/session.ts +116 -0
- package/src/types/sse.ts +90 -0
- package/src/types/toolCallHistory.ts +13 -0
- package/src/utils/agent/eventDispatch.ts +74 -0
- package/src/utils/agent/request.ts +55 -0
- package/src/utils/agent/toolCalls.ts +62 -0
- package/src/utils/api.ts +218 -0
- package/src/utils/canvas/viewMode.ts +46 -0
- package/src/utils/dom/authTokenMeta.ts +20 -0
- package/src/utils/dom/clickOutside.ts +11 -0
- package/src/utils/dom/externalLink.ts +57 -0
- package/src/utils/dom/scrollable.ts +24 -0
- package/src/utils/errors.ts +11 -0
- package/src/utils/files/expandedDirs.ts +25 -0
- package/src/utils/files/filename.ts +12 -0
- package/src/utils/files/sortChildren.ts +20 -0
- package/src/utils/filesPreview/schedulerPreview.ts +38 -0
- package/src/utils/filesPreview/todoPreview.ts +40 -0
- package/src/utils/format/date.ts +85 -0
- package/src/utils/format/frontmatter.ts +80 -0
- package/src/utils/format/jsonSyntax.ts +109 -0
- package/src/utils/html/previewCsp.ts +65 -0
- package/src/utils/image/resolve.ts +8 -0
- package/src/utils/image/rewriteMarkdownImageRefs.ts +182 -0
- package/src/utils/markdown/extractFirstH1.ts +39 -0
- package/src/utils/notification/dispatch.ts +22 -0
- package/src/utils/path/relativeLink.ts +130 -0
- package/src/utils/role/icon.ts +20 -0
- package/src/utils/role/merge.ts +10 -0
- package/src/utils/role/plugins.ts +12 -0
- package/src/utils/session/mergeSessions.ts +103 -0
- package/src/utils/session/seedRoleDefault.ts +35 -0
- package/src/utils/session/sessionEntries.ts +121 -0
- package/src/utils/session/sessionFactory.ts +22 -0
- package/src/utils/session/sessionHelpers.ts +99 -0
- package/src/utils/tools/dedup.ts +17 -0
- package/src/utils/tools/mcp.ts +33 -0
- package/src/utils/tools/pendingCalls.ts +16 -0
- package/src/utils/tools/result.ts +40 -0
- package/src/utils/types.ts +44 -0
|
@@ -0,0 +1,997 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="spreadsheet-container">
|
|
3
|
+
<div v-if="loading" class="min-h-full p-8 flex items-center justify-center">
|
|
4
|
+
<div class="text-gray-500">Loading spreadsheet...</div>
|
|
5
|
+
</div>
|
|
6
|
+
<div v-else-if="errorMessage" class="min-h-full p-8 flex items-center justify-center">
|
|
7
|
+
<div class="error">{{ errorMessage }}</div>
|
|
8
|
+
</div>
|
|
9
|
+
<div v-else-if="!resolvedSheets || resolvedSheets.length === 0" class="min-h-full p-8 flex items-center justify-center">
|
|
10
|
+
<div class="text-gray-500">No spreadsheet data available</div>
|
|
11
|
+
</div>
|
|
12
|
+
<template v-else>
|
|
13
|
+
<div class="spreadsheet-content-wrapper">
|
|
14
|
+
<div class="p-4">
|
|
15
|
+
<div class="header">
|
|
16
|
+
<h1 class="title">
|
|
17
|
+
{{ selectedResult.title || "Spreadsheet" }}
|
|
18
|
+
</h1>
|
|
19
|
+
<div class="button-group">
|
|
20
|
+
<button class="download-btn excel-btn" @click="downloadExcel">
|
|
21
|
+
<span class="material-icons">download</span>
|
|
22
|
+
Excel
|
|
23
|
+
</button>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<!-- Sheet tabs (if multiple sheets) -->
|
|
28
|
+
<div v-if="resolvedSheets.length > 1" class="sheet-tabs">
|
|
29
|
+
<button
|
|
30
|
+
v-for="(sheet, index) in resolvedSheets"
|
|
31
|
+
:key="index"
|
|
32
|
+
:class="['sheet-tab', { active: activeSheetIndex === index }]"
|
|
33
|
+
@click="activeSheetIndex = index"
|
|
34
|
+
>
|
|
35
|
+
{{ sheet.name }}
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<!-- Spreadsheet table -->
|
|
40
|
+
<div ref="tableContainer" class="table-container" @click="handleTableClick" v-html="renderedHtml"></div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<!-- Collapsible Editor -->
|
|
45
|
+
<details v-if="!miniEditorOpen" ref="editorDetails" class="spreadsheet-source">
|
|
46
|
+
<summary>Edit Spreadsheet Data</summary>
|
|
47
|
+
<textarea ref="editorTextarea" v-model="editableData" class="spreadsheet-editor" spellcheck="false" @input="handleDataEdit"></textarea>
|
|
48
|
+
<button class="apply-btn" :disabled="!hasChanges" @click="applyChanges">Apply Changes</button>
|
|
49
|
+
</details>
|
|
50
|
+
|
|
51
|
+
<!-- Mini Editor at Bottom -->
|
|
52
|
+
<div v-if="miniEditorOpen" class="mini-editor-panel">
|
|
53
|
+
<div class="mini-editor-content">
|
|
54
|
+
<span v-if="miniEditorCell" class="cell-ref"> {{ indexToCol(miniEditorCell.col) }}{{ miniEditorCell.row + 1 }} </span>
|
|
55
|
+
|
|
56
|
+
<!-- Type Selector -->
|
|
57
|
+
<div class="radio-group">
|
|
58
|
+
<label class="radio-option">
|
|
59
|
+
<input v-model="miniEditorType" type="radio" value="string" />
|
|
60
|
+
String
|
|
61
|
+
</label>
|
|
62
|
+
<label class="radio-option">
|
|
63
|
+
<input v-model="miniEditorType" type="radio" value="object" />
|
|
64
|
+
Formula
|
|
65
|
+
</label>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<!-- String input -->
|
|
69
|
+
<input
|
|
70
|
+
v-if="miniEditorType === 'string'"
|
|
71
|
+
v-model="miniEditorValue"
|
|
72
|
+
type="text"
|
|
73
|
+
class="form-input"
|
|
74
|
+
placeholder="Value"
|
|
75
|
+
@keyup.enter="saveMiniEditor"
|
|
76
|
+
/>
|
|
77
|
+
|
|
78
|
+
<!-- Formula inputs -->
|
|
79
|
+
<template v-if="miniEditorType === 'object'">
|
|
80
|
+
<input
|
|
81
|
+
v-model="miniEditorFormula"
|
|
82
|
+
type="text"
|
|
83
|
+
class="form-input"
|
|
84
|
+
placeholder="Value or Formula (e.g., 100 or SUM(B2:B11))"
|
|
85
|
+
@keyup.enter="saveMiniEditor"
|
|
86
|
+
/>
|
|
87
|
+
<input v-model="miniEditorFormat" type="text" class="form-input" placeholder="Format (e.g., $#,##0.00)" @keyup.enter="saveMiniEditor" />
|
|
88
|
+
</template>
|
|
89
|
+
|
|
90
|
+
<button class="save-btn" @click="saveMiniEditor">Update</button>
|
|
91
|
+
<button class="cancel-btn" @click="closeMiniEditor">✕</button>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</template>
|
|
95
|
+
</div>
|
|
96
|
+
</template>
|
|
97
|
+
|
|
98
|
+
<script setup lang="ts">
|
|
99
|
+
import { computed, ref, watch, onMounted, onUnmounted } from "vue";
|
|
100
|
+
import * as XLSX from "xlsx";
|
|
101
|
+
import type { ToolResult } from "gui-chat-protocol";
|
|
102
|
+
import type { SpreadsheetToolData, SpreadsheetSheet } from "./definition";
|
|
103
|
+
import {
|
|
104
|
+
SpreadsheetEngine,
|
|
105
|
+
indexToColumn,
|
|
106
|
+
extractCellReferences,
|
|
107
|
+
buildCellFromInput,
|
|
108
|
+
decodeSpreadsheetResponse,
|
|
109
|
+
findCellJsonPosition,
|
|
110
|
+
type SpreadsheetCell,
|
|
111
|
+
type CellValue,
|
|
112
|
+
} from "./engine";
|
|
113
|
+
import { applyCellHighlights, clearCellHighlights } from "./cellHighlights";
|
|
114
|
+
|
|
115
|
+
// Import all spreadsheet functions to populate the function registry
|
|
116
|
+
import "./engine/functions";
|
|
117
|
+
import { apiGet, apiPut } from "../../utils/api";
|
|
118
|
+
import { API_ROUTES } from "../../config/apiRoutes";
|
|
119
|
+
import type { FilesContentResponseLike } from "./engine/responseDecoder";
|
|
120
|
+
import { isObj, isRecord } from "../../utils/types";
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Normalize malformed data structures
|
|
124
|
+
* Some models generate flat arrays instead of 2D arrays - fix them
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
// Cells can be raw LLM output of arbitrary shape; tightening here would cascade through the engine API.
|
|
128
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
129
|
+
function normalizeSheetData(data: any): any[][] {
|
|
130
|
+
// Handle null/undefined
|
|
131
|
+
if (!data) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// If not an array
|
|
136
|
+
if (!Array.isArray(data)) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Empty array
|
|
141
|
+
if (data.length === 0) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// If data is already a 2D array, return as-is
|
|
146
|
+
if (Array.isArray(data[0])) {
|
|
147
|
+
return data;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// If data is a flat array of cell objects, convert to 2D by pairing cells
|
|
151
|
+
// Pattern: [cell1, cell2, cell3, cell4] -> [[cell1, cell2], [cell3, cell4]]
|
|
152
|
+
if (isObj(data[0])) {
|
|
153
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
154
|
+
const rows: any[][] = [];
|
|
155
|
+
for (let i = 0; i < data.length; i += 2) {
|
|
156
|
+
const row = [data[i]];
|
|
157
|
+
if (i + 1 < data.length) {
|
|
158
|
+
row.push(data[i + 1]);
|
|
159
|
+
}
|
|
160
|
+
rows.push(row);
|
|
161
|
+
}
|
|
162
|
+
return rows;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Unknown structure - return empty
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const props = defineProps<{
|
|
170
|
+
selectedResult: ToolResult<SpreadsheetToolData>;
|
|
171
|
+
}>();
|
|
172
|
+
|
|
173
|
+
const emit = defineEmits<{
|
|
174
|
+
updateResult: [result: ToolResult];
|
|
175
|
+
}>();
|
|
176
|
+
|
|
177
|
+
// Create spreadsheet engine instance
|
|
178
|
+
const engine = new SpreadsheetEngine();
|
|
179
|
+
|
|
180
|
+
const loading = ref(false);
|
|
181
|
+
const errorMessage = ref("");
|
|
182
|
+
const resolvedSheets = ref<SpreadsheetSheet[]>([]);
|
|
183
|
+
|
|
184
|
+
function isFilePath(value: unknown): value is string {
|
|
185
|
+
return typeof value === "string" && (value.startsWith("artifacts/spreadsheets/") || value.startsWith("spreadsheets/")) && value.endsWith(".json");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function fetchSheets(): Promise<void> {
|
|
189
|
+
const raw = props.selectedResult.data?.sheets;
|
|
190
|
+
// Clear any stale error from a previous result BEFORE the early
|
|
191
|
+
// returns, otherwise switching from a failed file-backed load to
|
|
192
|
+
// a new inline-data result leaves the error panel on screen.
|
|
193
|
+
errorMessage.value = "";
|
|
194
|
+
if (!raw) {
|
|
195
|
+
resolvedSheets.value = [];
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (!isFilePath(raw)) {
|
|
199
|
+
// Legacy inline data
|
|
200
|
+
resolvedSheets.value = raw as SpreadsheetSheet[];
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
loading.value = true;
|
|
204
|
+
const response = await apiGet<FilesContentResponseLike>(API_ROUTES.files.content, { path: raw });
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
errorMessage.value = `Failed to load spreadsheet: ${response.error}`;
|
|
207
|
+
resolvedSheets.value = [];
|
|
208
|
+
loading.value = false;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// The /files/content endpoint returns { kind, content?, message? }.
|
|
212
|
+
// Delegate the shape/validation decision to decodeSpreadsheetResponse
|
|
213
|
+
// so the async wrapper stays simple.
|
|
214
|
+
const result = decodeSpreadsheetResponse(response.data);
|
|
215
|
+
if (result.kind === "error") {
|
|
216
|
+
errorMessage.value = result.message;
|
|
217
|
+
resolvedSheets.value = [];
|
|
218
|
+
} else {
|
|
219
|
+
resolvedSheets.value = result.sheets as SpreadsheetSheet[];
|
|
220
|
+
}
|
|
221
|
+
loading.value = false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Fetch on mount and sync editableData
|
|
225
|
+
fetchSheets().then(() => {
|
|
226
|
+
editableData.value = JSON.stringify(resolvedSheets.value || [], null, 2);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
/** Persist edited sheets to disk when file-backed, and emit updateResult. */
|
|
230
|
+
async function persistSheets(sheets: SpreadsheetSheet[]): Promise<void> {
|
|
231
|
+
const raw = props.selectedResult.data?.sheets;
|
|
232
|
+
if (isFilePath(raw)) {
|
|
233
|
+
const filename = raw.replace(/^(artifacts\/spreadsheets|spreadsheets)\//, "");
|
|
234
|
+
const result = await apiPut<unknown>(API_ROUTES.plugins.updateSpreadsheet.replace(":filename", filename), {
|
|
235
|
+
sheets,
|
|
236
|
+
});
|
|
237
|
+
if (!result.ok) {
|
|
238
|
+
errorMessage.value = `Failed to save spreadsheet: ${result.error}`;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
resolvedSheets.value = sheets;
|
|
244
|
+
|
|
245
|
+
const updatedResult: ToolResult<SpreadsheetToolData> = {
|
|
246
|
+
...props.selectedResult,
|
|
247
|
+
data: {
|
|
248
|
+
...props.selectedResult.data,
|
|
249
|
+
sheets: isFilePath(raw) ? raw : sheets,
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
emit("updateResult", updatedResult);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const activeSheetIndex = ref(0);
|
|
256
|
+
const editableData = ref(JSON.stringify(resolvedSheets.value || [], null, 2));
|
|
257
|
+
const editorTextarea = ref<HTMLTextAreaElement | null>(null);
|
|
258
|
+
const editorDetails = ref<HTMLDetailsElement | null>(null);
|
|
259
|
+
const tableContainer = ref<HTMLDivElement | null>(null);
|
|
260
|
+
|
|
261
|
+
// Mini editor state
|
|
262
|
+
const miniEditorOpen = ref(false);
|
|
263
|
+
const miniEditorCell = ref<{ row: number; col: number } | null>(null);
|
|
264
|
+
|
|
265
|
+
const miniEditorValue = ref<unknown>(null);
|
|
266
|
+
const miniEditorType = ref<"number" | "string" | "object">("string");
|
|
267
|
+
const miniEditorFormula = ref("");
|
|
268
|
+
const miniEditorFormat = ref("");
|
|
269
|
+
|
|
270
|
+
// Referenced cells state (for formula highlighting)
|
|
271
|
+
const referencedCells = ref<Array<{ row: number; col: number }>>([]);
|
|
272
|
+
|
|
273
|
+
// Check if spreadsheet data has been modified
|
|
274
|
+
const hasChanges = computed(() => {
|
|
275
|
+
try {
|
|
276
|
+
const currentData = JSON.stringify(resolvedSheets.value || [], null, 2);
|
|
277
|
+
return editableData.value !== currentData;
|
|
278
|
+
} catch {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Short alias used in the template column header.
|
|
284
|
+
const indexToCol = indexToColumn;
|
|
285
|
+
|
|
286
|
+
// Calculate formulas in the data using the spreadsheet engine
|
|
287
|
+
const calculateFormulas = (data: SpreadsheetCell[][], sheetName?: string): CellValue[][] => {
|
|
288
|
+
// If we have a sheet name, we need to find all sheets for cross-sheet references
|
|
289
|
+
const allSheets = resolvedSheets.value;
|
|
290
|
+
|
|
291
|
+
// Create a SheetData object for the engine
|
|
292
|
+
const sheet = {
|
|
293
|
+
name: sheetName || "Sheet1",
|
|
294
|
+
data,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// Calculate using the engine
|
|
298
|
+
const result = engine.calculate(sheet, allSheets);
|
|
299
|
+
|
|
300
|
+
// Return the calculated data
|
|
301
|
+
|
|
302
|
+
return result.data;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// Render the active sheet as HTML table
|
|
306
|
+
const renderedHtml = computed(() => {
|
|
307
|
+
if (!resolvedSheets.value || resolvedSheets.value.length === 0) {
|
|
308
|
+
return "";
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const sheet = resolvedSheets.value[activeSheetIndex.value];
|
|
312
|
+
if (!sheet || !sheet.data) {
|
|
313
|
+
return "";
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
// Calculate formulas first with sheet name for cross-sheet references
|
|
318
|
+
const calculatedData = calculateFormulas(sheet.data, sheet.name);
|
|
319
|
+
|
|
320
|
+
// Convert data array to worksheet
|
|
321
|
+
const worksheet = XLSX.utils.aoa_to_sheet(calculatedData);
|
|
322
|
+
|
|
323
|
+
// Generate HTML table
|
|
324
|
+
const html = XLSX.utils.sheet_to_html(worksheet, {
|
|
325
|
+
id: "spreadsheet-table",
|
|
326
|
+
editable: false,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
return html;
|
|
330
|
+
} catch (error) {
|
|
331
|
+
console.error("Failed to render spreadsheet:", error);
|
|
332
|
+
return `<div class="error">Failed to render spreadsheet: ${error instanceof Error ? error.message : "Unknown error"}</div>`;
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Download as Excel file
|
|
337
|
+
const downloadExcel = () => {
|
|
338
|
+
if (!resolvedSheets.value || resolvedSheets.value.length === 0) return;
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
const workbook = XLSX.utils.book_new();
|
|
342
|
+
|
|
343
|
+
// Add all sheets to workbook
|
|
344
|
+
resolvedSheets.value.forEach((sheet) => {
|
|
345
|
+
const worksheet = XLSX.utils.aoa_to_sheet(sheet.data);
|
|
346
|
+
XLSX.utils.book_append_sheet(workbook, worksheet, sheet.name);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Generate filename
|
|
350
|
+
const filename = props.selectedResult.title ? `${props.selectedResult.title.replace(/[^a-z0-9]/gi, "_").toLowerCase()}.xlsx` : "spreadsheet.xlsx";
|
|
351
|
+
|
|
352
|
+
// Write file
|
|
353
|
+
XLSX.writeFile(workbook, filename);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
console.error("Failed to download Excel:", error);
|
|
356
|
+
alert(`Failed to download Excel file: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
function handleDataEdit() {
|
|
361
|
+
// Just update the local state, don't apply yet
|
|
362
|
+
// User needs to click "Apply Changes" button
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// `extractCellReferences` now lives in `./engine/formulaRefs.ts`
|
|
366
|
+
// (imported at the top of this file). Extracted to bring this
|
|
367
|
+
// file's cognitive complexity back under the sonarjs threshold
|
|
368
|
+
// and to make the formula-reference scanner unit-testable.
|
|
369
|
+
// See `test/plugins/spreadsheet/engine/test_formulaRefs.ts`.
|
|
370
|
+
|
|
371
|
+
function openMiniEditor(rowIndex: number, colIndex: number) {
|
|
372
|
+
try {
|
|
373
|
+
const sheets = JSON.parse(editableData.value);
|
|
374
|
+
const currentSheet = sheets[activeSheetIndex.value];
|
|
375
|
+
|
|
376
|
+
if (!currentSheet || !currentSheet.data) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Normalize the data in case it's malformed
|
|
381
|
+
const normalizedData = normalizeSheetData(currentSheet.data);
|
|
382
|
+
|
|
383
|
+
if (!normalizedData[rowIndex] || normalizedData[rowIndex][colIndex] === undefined) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const cellValue = normalizedData[rowIndex][colIndex];
|
|
388
|
+
|
|
389
|
+
// Determine cell type and extract values (new format: {v, f})
|
|
390
|
+
if (isRecord(cellValue) && "v" in cellValue) {
|
|
391
|
+
const value = cellValue.v;
|
|
392
|
+
const format = typeof cellValue.f === "string" ? cellValue.f : "";
|
|
393
|
+
|
|
394
|
+
// Check if it's a formula (value starts with "=")
|
|
395
|
+
if (typeof value === "string" && value.startsWith("=")) {
|
|
396
|
+
miniEditorType.value = "object";
|
|
397
|
+
miniEditorValue.value = "";
|
|
398
|
+
miniEditorFormula.value = value.substring(1); // Remove "=" prefix
|
|
399
|
+
miniEditorFormat.value = format;
|
|
400
|
+
// Extract and store referenced cells for highlighting
|
|
401
|
+
referencedCells.value = extractCellReferences(value);
|
|
402
|
+
} else if (typeof value === "number") {
|
|
403
|
+
miniEditorType.value = "object";
|
|
404
|
+
miniEditorValue.value = "";
|
|
405
|
+
miniEditorFormula.value = String(value);
|
|
406
|
+
miniEditorFormat.value = format;
|
|
407
|
+
referencedCells.value = [];
|
|
408
|
+
} else {
|
|
409
|
+
miniEditorType.value = "string";
|
|
410
|
+
miniEditorValue.value = String(value);
|
|
411
|
+
miniEditorFormula.value = "";
|
|
412
|
+
miniEditorFormat.value = "";
|
|
413
|
+
referencedCells.value = [];
|
|
414
|
+
}
|
|
415
|
+
} else {
|
|
416
|
+
// Legacy format or plain value
|
|
417
|
+
miniEditorType.value = "string";
|
|
418
|
+
miniEditorValue.value = String(cellValue ?? "");
|
|
419
|
+
miniEditorFormula.value = "";
|
|
420
|
+
miniEditorFormat.value = "";
|
|
421
|
+
referencedCells.value = [];
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
miniEditorCell.value = { row: rowIndex, col: colIndex };
|
|
425
|
+
miniEditorOpen.value = true;
|
|
426
|
+
} catch (error) {
|
|
427
|
+
console.error("Failed to open mini editor:", error);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function closeMiniEditor() {
|
|
432
|
+
miniEditorOpen.value = false;
|
|
433
|
+
miniEditorCell.value = null;
|
|
434
|
+
miniEditorValue.value = null;
|
|
435
|
+
miniEditorFormula.value = "";
|
|
436
|
+
miniEditorFormat.value = "";
|
|
437
|
+
referencedCells.value = [];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function saveMiniEditor() {
|
|
441
|
+
if (!miniEditorCell.value) return;
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
const sheets = JSON.parse(editableData.value);
|
|
445
|
+
const currentSheet = sheets[activeSheetIndex.value];
|
|
446
|
+
|
|
447
|
+
if (!currentSheet || !currentSheet.data) return;
|
|
448
|
+
|
|
449
|
+
const { row, col } = miniEditorCell.value;
|
|
450
|
+
|
|
451
|
+
// Normalize the data in case it's malformed
|
|
452
|
+
const normalizedData = normalizeSheetData(currentSheet.data);
|
|
453
|
+
|
|
454
|
+
// Ensure the row exists
|
|
455
|
+
while (normalizedData.length <= row) {
|
|
456
|
+
normalizedData.push([]);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Ensure the row is an array
|
|
460
|
+
if (!Array.isArray(normalizedData[row])) {
|
|
461
|
+
normalizedData[row] = [];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Build the new cell value (delegates formula/number detection
|
|
465
|
+
// and format attachment to the pure helper).
|
|
466
|
+
const newCellValue: SpreadsheetCell = buildCellFromInput({
|
|
467
|
+
type: miniEditorType.value,
|
|
468
|
+
value: miniEditorValue.value,
|
|
469
|
+
formula: miniEditorFormula.value,
|
|
470
|
+
format: miniEditorFormat.value,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Update the cell in normalized data
|
|
474
|
+
normalizedData[row][col] = newCellValue;
|
|
475
|
+
|
|
476
|
+
// Update the sheet with normalized data
|
|
477
|
+
currentSheet.data = normalizedData;
|
|
478
|
+
|
|
479
|
+
// Update editableData
|
|
480
|
+
editableData.value = JSON.stringify(sheets, null, 2);
|
|
481
|
+
|
|
482
|
+
// Persist to disk (if file-backed) and emit update
|
|
483
|
+
persistSheets(sheets);
|
|
484
|
+
|
|
485
|
+
// Update referenced cells if the saved cell contains a formula
|
|
486
|
+
if (typeof newCellValue.v === "string" && newCellValue.v.startsWith("=")) {
|
|
487
|
+
referencedCells.value = extractCellReferences(newCellValue.v);
|
|
488
|
+
} else {
|
|
489
|
+
referencedCells.value = [];
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Don't close the mini editor - keep it open so user can see the updated references
|
|
493
|
+
// closeMiniEditor();
|
|
494
|
+
} catch (error) {
|
|
495
|
+
alert(`Failed to save cell: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function handleTableClick(event: MouseEvent) {
|
|
500
|
+
const target = event.target as HTMLElement;
|
|
501
|
+
|
|
502
|
+
// Check if clicked element is a table cell
|
|
503
|
+
if (target.tagName !== "TD") return;
|
|
504
|
+
|
|
505
|
+
// Get the row and column indices
|
|
506
|
+
const cell = target as HTMLTableCellElement;
|
|
507
|
+
const row = cell.parentElement as HTMLTableRowElement;
|
|
508
|
+
|
|
509
|
+
const colIndex = cell.cellIndex;
|
|
510
|
+
const rowIndex = row.rowIndex;
|
|
511
|
+
|
|
512
|
+
// Check if the main editor details is open
|
|
513
|
+
const isEditorOpen = editorDetails.value?.open ?? false;
|
|
514
|
+
|
|
515
|
+
// If editor is closed, open mini editor
|
|
516
|
+
if (!isEditorOpen) {
|
|
517
|
+
openMiniEditor(rowIndex, colIndex);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// If editor is open, try to find and select this cell in the editor.
|
|
522
|
+
if (!editorTextarea.value) return;
|
|
523
|
+
try {
|
|
524
|
+
const sheets = JSON.parse(editableData.value);
|
|
525
|
+
const currentSheet = sheets[activeSheetIndex.value];
|
|
526
|
+
if (!currentSheet || !currentSheet.data) return;
|
|
527
|
+
const normalizedData = normalizeSheetData(currentSheet.data);
|
|
528
|
+
if (!normalizedData[rowIndex] || normalizedData[rowIndex][colIndex] === undefined) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const cellStr = JSON.stringify(normalizedData[rowIndex][colIndex]);
|
|
532
|
+
const cellStart = findCellJsonPosition(editableData.value, currentSheet.name, rowIndex, colIndex);
|
|
533
|
+
if (cellStart < 0) return;
|
|
534
|
+
editorTextarea.value.focus();
|
|
535
|
+
editorTextarea.value.setSelectionRange(cellStart, cellStart + cellStr.length);
|
|
536
|
+
// Scroll the textarea to make the selection visible.
|
|
537
|
+
const textBeforeSelection = editableData.value.substring(0, cellStart);
|
|
538
|
+
const lineNumber = textBeforeSelection.split("\n").length;
|
|
539
|
+
const lineHeight = 22;
|
|
540
|
+
const textarea = editorTextarea.value;
|
|
541
|
+
textarea.scrollTop = Math.max(0, lineNumber * lineHeight - textarea.clientHeight / 2);
|
|
542
|
+
} catch (error) {
|
|
543
|
+
console.error("Failed to select cell in editor:", error);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async function applyChanges() {
|
|
548
|
+
try {
|
|
549
|
+
// Parse the edited JSON
|
|
550
|
+
const parsedSheets = JSON.parse(editableData.value);
|
|
551
|
+
|
|
552
|
+
// Validate it's an array
|
|
553
|
+
if (!Array.isArray(parsedSheets)) {
|
|
554
|
+
throw new Error("Data must be an array of sheets");
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Persist to disk (if file-backed) and emit update
|
|
558
|
+
await persistSheets(parsedSheets);
|
|
559
|
+
|
|
560
|
+
// Reset to first sheet after update
|
|
561
|
+
activeSheetIndex.value = 0;
|
|
562
|
+
} catch (error) {
|
|
563
|
+
alert(`Invalid JSON format: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Watch for external changes to selectedResult
|
|
568
|
+
watch(
|
|
569
|
+
() => props.selectedResult.data?.sheets,
|
|
570
|
+
() => {
|
|
571
|
+
fetchSheets().then(() => {
|
|
572
|
+
editableData.value = JSON.stringify(resolvedSheets.value || [], null, 2);
|
|
573
|
+
// Reset to first sheet when result changes
|
|
574
|
+
activeSheetIndex.value = 0;
|
|
575
|
+
});
|
|
576
|
+
},
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
// Reset active sheet if it's out of bounds
|
|
580
|
+
watch(
|
|
581
|
+
() => resolvedSheets.value?.length,
|
|
582
|
+
(length) => {
|
|
583
|
+
if (length && activeSheetIndex.value >= length) {
|
|
584
|
+
activeSheetIndex.value = 0;
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
// Highlight selected cell and referenced cells when mini editor is
|
|
590
|
+
// open. The per-step DOM work lives in cellHighlights.ts so this
|
|
591
|
+
// callback stays trivial and the complexity lands on the helpers,
|
|
592
|
+
// each of which is linear.
|
|
593
|
+
watch(
|
|
594
|
+
[miniEditorOpen, miniEditorCell, referencedCells, renderedHtml],
|
|
595
|
+
() => {
|
|
596
|
+
clearCellHighlights(tableContainer.value);
|
|
597
|
+
if (!miniEditorOpen.value) return;
|
|
598
|
+
applyCellHighlights(tableContainer.value, miniEditorCell.value, referencedCells.value);
|
|
599
|
+
},
|
|
600
|
+
{ flush: "post" },
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
// Keyboard navigation handler
|
|
604
|
+
function handleKeyboardNavigation(event: KeyboardEvent) {
|
|
605
|
+
// Only handle arrow keys when mini editor is open and not focused on input
|
|
606
|
+
if (!miniEditorOpen.value || !miniEditorCell.value) return;
|
|
607
|
+
|
|
608
|
+
// Don't interfere if user is typing in an input field
|
|
609
|
+
const target = event.target as HTMLElement;
|
|
610
|
+
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const { row, col } = miniEditorCell.value;
|
|
615
|
+
let newRow = row;
|
|
616
|
+
let newCol = col;
|
|
617
|
+
|
|
618
|
+
// Determine new position based on arrow key
|
|
619
|
+
switch (event.key) {
|
|
620
|
+
case "ArrowUp":
|
|
621
|
+
newRow = Math.max(0, row - 1);
|
|
622
|
+
break;
|
|
623
|
+
case "ArrowDown":
|
|
624
|
+
newRow = row + 1;
|
|
625
|
+
break;
|
|
626
|
+
case "ArrowLeft":
|
|
627
|
+
newCol = Math.max(0, col - 1);
|
|
628
|
+
break;
|
|
629
|
+
case "ArrowRight":
|
|
630
|
+
newCol = col + 1;
|
|
631
|
+
break;
|
|
632
|
+
default:
|
|
633
|
+
return; // Not an arrow key, ignore
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Get current sheet data to validate bounds
|
|
637
|
+
try {
|
|
638
|
+
const sheets = JSON.parse(editableData.value);
|
|
639
|
+
const currentSheet = sheets[activeSheetIndex.value];
|
|
640
|
+
|
|
641
|
+
if (!currentSheet || !currentSheet.data) return;
|
|
642
|
+
|
|
643
|
+
// Validate new position is within bounds
|
|
644
|
+
if (newRow < 0 || newRow >= currentSheet.data.length || newCol < 0 || !currentSheet.data[newRow] || newCol >= currentSheet.data[newRow].length) {
|
|
645
|
+
return; // Out of bounds, ignore
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Prevent default scrolling behavior
|
|
649
|
+
event.preventDefault();
|
|
650
|
+
|
|
651
|
+
// Move to new cell
|
|
652
|
+
openMiniEditor(newRow, newCol);
|
|
653
|
+
} catch (error) {
|
|
654
|
+
console.error("Failed to navigate cells:", error);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Add keyboard event listener on mount
|
|
659
|
+
onMounted(() => {
|
|
660
|
+
document.addEventListener("keydown", handleKeyboardNavigation);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
// Remove keyboard event listener on unmount
|
|
664
|
+
onUnmounted(() => {
|
|
665
|
+
document.removeEventListener("keydown", handleKeyboardNavigation);
|
|
666
|
+
});
|
|
667
|
+
</script>
|
|
668
|
+
|
|
669
|
+
<style scoped>
|
|
670
|
+
.spreadsheet-container {
|
|
671
|
+
width: 100%;
|
|
672
|
+
height: 100%;
|
|
673
|
+
display: flex;
|
|
674
|
+
flex-direction: column;
|
|
675
|
+
background: white;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.spreadsheet-content-wrapper {
|
|
679
|
+
flex: 1;
|
|
680
|
+
overflow-y: auto;
|
|
681
|
+
min-height: 0;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.header {
|
|
685
|
+
display: flex;
|
|
686
|
+
align-items: center;
|
|
687
|
+
justify-content: space-between;
|
|
688
|
+
margin-bottom: 1em;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
.title {
|
|
692
|
+
font-size: 2em;
|
|
693
|
+
margin: 0;
|
|
694
|
+
font-weight: bold;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.button-group {
|
|
698
|
+
display: flex;
|
|
699
|
+
gap: 0.5em;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.download-btn {
|
|
703
|
+
padding: 0.5em 1em;
|
|
704
|
+
color: white;
|
|
705
|
+
border: none;
|
|
706
|
+
border-radius: 4px;
|
|
707
|
+
cursor: pointer;
|
|
708
|
+
font-size: 0.9em;
|
|
709
|
+
display: flex;
|
|
710
|
+
align-items: center;
|
|
711
|
+
gap: 0.5em;
|
|
712
|
+
transition: background-color 0.2s;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
.excel-btn {
|
|
716
|
+
background-color: #217346;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.excel-btn:hover {
|
|
720
|
+
background-color: #1e6a3f;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.excel-btn:active {
|
|
724
|
+
background-color: #1a5c36;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
.download-btn .material-icons {
|
|
728
|
+
font-size: 1.2em;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/* Sheet tabs */
|
|
732
|
+
.sheet-tabs {
|
|
733
|
+
display: flex;
|
|
734
|
+
gap: 0.25em;
|
|
735
|
+
margin-bottom: 1em;
|
|
736
|
+
border-bottom: 2px solid #e0e0e0;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
.sheet-tab {
|
|
740
|
+
padding: 0.5em 1em;
|
|
741
|
+
background: #f5f5f5;
|
|
742
|
+
border: none;
|
|
743
|
+
border-top-left-radius: 4px;
|
|
744
|
+
border-top-right-radius: 4px;
|
|
745
|
+
cursor: pointer;
|
|
746
|
+
font-size: 0.9em;
|
|
747
|
+
color: #666;
|
|
748
|
+
transition: background-color 0.2s;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.sheet-tab:hover {
|
|
752
|
+
background: #e8e8e8;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
.sheet-tab.active {
|
|
756
|
+
background: white;
|
|
757
|
+
color: #333;
|
|
758
|
+
font-weight: 500;
|
|
759
|
+
border-bottom: 2px solid white;
|
|
760
|
+
margin-bottom: -2px;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/* Table container */
|
|
764
|
+
.table-container {
|
|
765
|
+
overflow-x: auto;
|
|
766
|
+
background: white;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/* Style the generated table */
|
|
770
|
+
.table-container :deep(table) {
|
|
771
|
+
border-collapse: collapse;
|
|
772
|
+
width: 100%;
|
|
773
|
+
font-family: "Segoe UI", Arial, sans-serif;
|
|
774
|
+
font-size: 0.9em;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
.table-container :deep(td),
|
|
778
|
+
.table-container :deep(th) {
|
|
779
|
+
border: 1px solid #d0d0d0;
|
|
780
|
+
padding: 0.5em 0.75em;
|
|
781
|
+
text-align: left;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
.table-container :deep(th) {
|
|
785
|
+
background-color: #f5f5f5;
|
|
786
|
+
font-weight: 600;
|
|
787
|
+
color: #333;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
.table-container :deep(tr:nth-child(even)) {
|
|
791
|
+
background-color: #fafafa;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
.table-container :deep(tr:hover) {
|
|
795
|
+
background-color: #f0f0f0;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
.table-container :deep(.cell-editing) {
|
|
799
|
+
background-color: #e8f5e9 !important;
|
|
800
|
+
outline: 2px solid #217346 !important;
|
|
801
|
+
outline-offset: -2px;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
.table-container :deep(.cell-referenced) {
|
|
805
|
+
background-color: #fff3e0 !important;
|
|
806
|
+
outline: 2px solid #ff9800 !important;
|
|
807
|
+
outline-offset: -2px;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/* Error message */
|
|
811
|
+
.error {
|
|
812
|
+
padding: 1em;
|
|
813
|
+
background: #ffebee;
|
|
814
|
+
color: #c62828;
|
|
815
|
+
border-radius: 4px;
|
|
816
|
+
margin: 1em 0;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/* Editor section */
|
|
820
|
+
.spreadsheet-source {
|
|
821
|
+
padding: 0.5rem;
|
|
822
|
+
background: #f5f5f5;
|
|
823
|
+
border-top: 1px solid #e0e0e0;
|
|
824
|
+
font-family: monospace;
|
|
825
|
+
font-size: 0.85rem;
|
|
826
|
+
flex-shrink: 0;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
.spreadsheet-source summary {
|
|
830
|
+
cursor: pointer;
|
|
831
|
+
user-select: none;
|
|
832
|
+
padding: 0.5rem;
|
|
833
|
+
background: #e8e8e8;
|
|
834
|
+
border-radius: 4px;
|
|
835
|
+
font-weight: 500;
|
|
836
|
+
color: #333;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
.spreadsheet-source[open] summary {
|
|
840
|
+
margin-bottom: 0.5rem;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
.spreadsheet-source summary:hover {
|
|
844
|
+
background: #d8d8d8;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
.spreadsheet-editor {
|
|
848
|
+
width: 100%;
|
|
849
|
+
height: 40vh;
|
|
850
|
+
padding: 1rem;
|
|
851
|
+
background: #ffffff;
|
|
852
|
+
border: 1px solid #ccc;
|
|
853
|
+
border-radius: 4px;
|
|
854
|
+
color: #333;
|
|
855
|
+
font-family: "Courier New", monospace;
|
|
856
|
+
font-size: 0.9rem;
|
|
857
|
+
resize: vertical;
|
|
858
|
+
margin-bottom: 0.5rem;
|
|
859
|
+
line-height: 1.5;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
.spreadsheet-editor:focus {
|
|
863
|
+
outline: none;
|
|
864
|
+
border-color: #217346;
|
|
865
|
+
box-shadow: 0 0 0 2px rgba(33, 115, 70, 0.1);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
.apply-btn {
|
|
869
|
+
padding: 0.5rem 1rem;
|
|
870
|
+
background: #217346;
|
|
871
|
+
color: white;
|
|
872
|
+
border: none;
|
|
873
|
+
border-radius: 4px;
|
|
874
|
+
cursor: pointer;
|
|
875
|
+
font-size: 0.9rem;
|
|
876
|
+
transition: background 0.2s;
|
|
877
|
+
font-weight: 500;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
.apply-btn:hover {
|
|
881
|
+
background: #1e6a3f;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
.apply-btn:active {
|
|
885
|
+
background: #1a5c36;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
.apply-btn:disabled {
|
|
889
|
+
background: #cccccc;
|
|
890
|
+
color: #666666;
|
|
891
|
+
cursor: not-allowed;
|
|
892
|
+
opacity: 0.6;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
.apply-btn:disabled:hover {
|
|
896
|
+
background: #cccccc;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/* Mini Editor Panel */
|
|
900
|
+
.mini-editor-panel {
|
|
901
|
+
background: #f8f8f8;
|
|
902
|
+
border-top: 1px solid #d0d0d0;
|
|
903
|
+
flex-shrink: 0;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
.mini-editor-content {
|
|
907
|
+
display: flex;
|
|
908
|
+
align-items: center;
|
|
909
|
+
gap: 0.5rem;
|
|
910
|
+
padding: 0.5rem 1rem;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
.cell-ref {
|
|
914
|
+
font-family: monospace;
|
|
915
|
+
font-weight: 600;
|
|
916
|
+
color: #217346;
|
|
917
|
+
font-size: 0.85rem;
|
|
918
|
+
min-width: 2rem;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
.radio-group {
|
|
922
|
+
display: flex;
|
|
923
|
+
gap: 0.75rem;
|
|
924
|
+
border-right: 1px solid #d0d0d0;
|
|
925
|
+
padding-right: 0.75rem;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
.radio-option {
|
|
929
|
+
display: flex;
|
|
930
|
+
align-items: center;
|
|
931
|
+
gap: 0.25rem;
|
|
932
|
+
cursor: pointer;
|
|
933
|
+
font-size: 0.85rem;
|
|
934
|
+
color: #555;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
.radio-option input[type="radio"] {
|
|
938
|
+
cursor: pointer;
|
|
939
|
+
width: 0.9rem;
|
|
940
|
+
height: 0.9rem;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
.form-input {
|
|
944
|
+
flex: 1;
|
|
945
|
+
padding: 0.4rem 0.6rem;
|
|
946
|
+
border: 1px solid #ccc;
|
|
947
|
+
border-radius: 3px;
|
|
948
|
+
font-size: 0.85rem;
|
|
949
|
+
font-family: inherit;
|
|
950
|
+
transition: border-color 0.2s;
|
|
951
|
+
min-width: 120px;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
.form-input:focus {
|
|
955
|
+
outline: none;
|
|
956
|
+
border-color: #217346;
|
|
957
|
+
box-shadow: 0 0 0 2px rgba(33, 115, 70, 0.1);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
.form-input::placeholder {
|
|
961
|
+
color: #999;
|
|
962
|
+
font-size: 0.8rem;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
.save-btn,
|
|
966
|
+
.cancel-btn {
|
|
967
|
+
padding: 0.4rem 0.8rem;
|
|
968
|
+
border: none;
|
|
969
|
+
border-radius: 3px;
|
|
970
|
+
cursor: pointer;
|
|
971
|
+
font-size: 0.85rem;
|
|
972
|
+
font-weight: 500;
|
|
973
|
+
transition: background 0.2s;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
.save-btn {
|
|
977
|
+
background: #217346;
|
|
978
|
+
color: white;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
.save-btn:hover {
|
|
982
|
+
background: #1e6a3f;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
.cancel-btn {
|
|
986
|
+
background: transparent;
|
|
987
|
+
color: #666;
|
|
988
|
+
padding: 0.4rem;
|
|
989
|
+
font-size: 1.2rem;
|
|
990
|
+
line-height: 1;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
.cancel-btn:hover {
|
|
994
|
+
color: #333;
|
|
995
|
+
background: #e0e0e0;
|
|
996
|
+
}
|
|
997
|
+
</style>
|