From 266a7ffacd0974846726fa8d125cb588292ab6a4 Mon Sep 17 00:00:00 2001 From: cirry <812852553@qq.com> Date: Fri, 10 Jul 2026 14:41:54 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=9B=BE=E7=89=87=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E6=B7=BB=E5=8A=A0=E9=A2=84=E8=A7=88=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .reasonix/desktop-topic-auto-title-meta.json | 6 + .reasonix/desktop-topic-created-at.json | 4 +- .reasonix/desktop-topic-title-sources.json | 4 +- .reasonix/desktop-topic-titles.json | 4 +- components.json | 26 +++ src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 1 + src-tauri/src/lib.rs | 93 ++++++++- src/components/MarkdownEditor.vue | 194 +++++++++++++++++- .../ui/dropdown-menu/DropdownMenu.vue | 19 ++ .../DropdownMenuCheckboxItem.vue | 43 ++++ .../ui/dropdown-menu/DropdownMenuContent.vue | 40 ++++ .../ui/dropdown-menu/DropdownMenuGroup.vue | 15 ++ .../ui/dropdown-menu/DropdownMenuItem.vue | 31 +++ .../ui/dropdown-menu/DropdownMenuLabel.vue | 23 +++ .../dropdown-menu/DropdownMenuRadioGroup.vue | 21 ++ .../dropdown-menu/DropdownMenuRadioItem.vue | 44 ++++ .../dropdown-menu/DropdownMenuSeparator.vue | 23 +++ .../ui/dropdown-menu/DropdownMenuShortcut.vue | 17 ++ .../ui/dropdown-menu/DropdownMenuSub.vue | 18 ++ .../dropdown-menu/DropdownMenuSubContent.vue | 27 +++ .../dropdown-menu/DropdownMenuSubTrigger.vue | 32 +++ .../ui/dropdown-menu/DropdownMenuTrigger.vue | 17 ++ src/components/ui/dropdown-menu/index.ts | 16 ++ src/lib/utils.ts | 6 + 25 files changed, 713 insertions(+), 12 deletions(-) create mode 100644 components.json create mode 100644 src/components/ui/dropdown-menu/DropdownMenu.vue create mode 100644 src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue create mode 100644 src/components/ui/dropdown-menu/DropdownMenuContent.vue create mode 100644 src/components/ui/dropdown-menu/DropdownMenuGroup.vue create mode 100644 src/components/ui/dropdown-menu/DropdownMenuItem.vue create mode 100644 src/components/ui/dropdown-menu/DropdownMenuLabel.vue create mode 100644 src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue create mode 100644 src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue create mode 100644 src/components/ui/dropdown-menu/DropdownMenuSeparator.vue create mode 100644 src/components/ui/dropdown-menu/DropdownMenuShortcut.vue create mode 100644 src/components/ui/dropdown-menu/DropdownMenuSub.vue create mode 100644 src/components/ui/dropdown-menu/DropdownMenuSubContent.vue create mode 100644 src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue create mode 100644 src/components/ui/dropdown-menu/DropdownMenuTrigger.vue create mode 100644 src/components/ui/dropdown-menu/index.ts create mode 100644 src/lib/utils.ts diff --git a/.reasonix/desktop-topic-auto-title-meta.json b/.reasonix/desktop-topic-auto-title-meta.json index 2235f82..b3ae486 100644 --- a/.reasonix/desktop-topic-auto-title-meta.json +++ b/.reasonix/desktop-topic-auto-title-meta.json @@ -46,5 +46,11 @@ "userTurns": 1, "basisHash": "53517398750be87f", "updatedAt": 1783656298406 + }, + "topic_20260710-054543_5e7c427932aecd7b": { + "stage": 3, + "userTurns": 3, + "basisHash": "87073c4b14751c7b", + "updatedAt": 1783663776394 } } \ No newline at end of file diff --git a/.reasonix/desktop-topic-created-at.json b/.reasonix/desktop-topic-created-at.json index 2ff1e02..67bff44 100644 --- a/.reasonix/desktop-topic-created-at.json +++ b/.reasonix/desktop-topic-created-at.json @@ -7,5 +7,7 @@ "topic_20260710-030936_cedad2907c3954fe": 1783652976310, "topic_20260710-032907_600adc6b00791607": 1783654147796, "topic_20260710-034033_6432ee6566f7b432": 1783654833341, - "topic_20260710-034724_3c6fb29d8d0a4119": 1783655244536 + "topic_20260710-034724_3c6fb29d8d0a4119": 1783655244536, + "topic_20260710-054543_5e7c427932aecd7b": 1783662343716, + "topic_20260710-063053_353d579222d3b70b": 1783665053391 } \ No newline at end of file diff --git a/.reasonix/desktop-topic-title-sources.json b/.reasonix/desktop-topic-title-sources.json index 5943499..76cc3d3 100644 --- a/.reasonix/desktop-topic-title-sources.json +++ b/.reasonix/desktop-topic-title-sources.json @@ -45,5 +45,7 @@ "topic_20260710-030936_cedad2907c3954fe": "auto", "topic_20260710-032907_600adc6b00791607": "auto", "topic_20260710-034033_6432ee6566f7b432": "auto", - "topic_20260710-034724_3c6fb29d8d0a4119": "auto" + "topic_20260710-034724_3c6fb29d8d0a4119": "auto", + "topic_20260710-054543_5e7c427932aecd7b": "auto", + "topic_20260710-063053_353d579222d3b70b": "auto" } \ No newline at end of file diff --git a/.reasonix/desktop-topic-titles.json b/.reasonix/desktop-topic-titles.json index 7ec75b4..26ee8d6 100644 --- a/.reasonix/desktop-topic-titles.json +++ b/.reasonix/desktop-topic-titles.json @@ -45,5 +45,7 @@ "topic_20260710-030936_cedad2907c3954fe": "鼠标放在tree上的时候,只有hov…", "topic_20260710-032907_600adc6b00791607": "目录树中显示的文件夹,markdow…", "topic_20260710-034033_6432ee6566f7b432": "我在点击目录树的时候新增文件夹和新建…", - "topic_20260710-034724_3c6fb29d8d0a4119": "目录树中鼠标悬浮弹框dropmenu…" + "topic_20260710-034724_3c6fb29d8d0a4119": "目录树中鼠标悬浮弹框dropmenu…", + "topic_20260710-054543_5e7c427932aecd7b": "现在在markdown的编辑区域中…", + "topic_20260710-063053_353d579222d3b70b": "新的会话" } \ No newline at end of file diff --git a/components.json b/components.json new file mode 100644 index 0000000..5238063 --- /dev/null +++ b/components.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://shadcn-vue.com/schema.json", + "style": "reka-nova", + "font": "geist-sans", + "typescript": true, + "tailwind": { + "config": "", + "css": "src/tailwind.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "pointer": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "composables": "@/composables" + }, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 230cc84..8d126a6 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4930,6 +4930,7 @@ dependencies = [ name = "yurou" version = "0.1.0" dependencies = [ + "base64 0.22.1", "notify", "serde", "serde_json", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 38181f5..f7fa6cc 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -21,6 +21,7 @@ tauri-build = { version = "2", features = [] } tauri = { version = "2", features = ["protocol-asset"] } tauri-plugin-dialog = "2" tauri-plugin-opener = "2" +base64 = "0.22" serde = { version = "1", features = ["derive"] } serde_json = "1" time = ">=0.3.0, <0.3.47" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1d1597e..9fe6963 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -6,6 +6,7 @@ use std::sync::Mutex; use tauri::menu::{MenuBuilder, MenuItemBuilder, SubmenuBuilder}; use tauri::{Emitter, Manager}; use tauri_plugin_dialog::DialogExt; +use base64::Engine; #[derive(Debug, Clone, serde::Serialize)] struct DirEntry { @@ -55,6 +56,59 @@ fn is_hidden_entry(entry: &fs::DirEntry) -> bool { } } +/// Returns the companion hidden resources directory path for a markdown file. +/// Example: `notes/doc.md` → `notes/.doc` +fn resources_dir_path(file_path: &str) -> Option { + let path = std::path::Path::new(file_path); + if path.extension().map_or(true, |ext| ext != "md") { + return None; + } + let stem = path.file_stem()?.to_str()?; + let parent = path.parent()?; + let dir_name = format!(".{}", stem); + Some(parent.join(dir_name).to_string_lossy().to_string()) +} + +/// On Windows, set the hidden attribute on a directory so it doesn't clutter +/// the file manager. +#[cfg(windows)] +fn hide_windows_dir(dir_path: &str) { + let _ = std::process::Command::new("attrib") + .args(["+h", dir_path]) + .output(); +} + +/// Ensure a directory exists, and hide it on Windows when the name starts with `.`. +fn ensure_resources_dir(dir_path: &str) -> std::io::Result<()> { + fs::create_dir_all(dir_path)?; + #[cfg(windows)] + { + let name = std::path::Path::new(dir_path) + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or(""); + if name.starts_with('.') { + hide_windows_dir(dir_path); + } + } + Ok(()) +} + +#[tauri::command] +fn save_media_file(path: String, data: String) -> Result<(), String> { + let bytes = base64::engine::general_purpose::STANDARD + .decode(&data) + .map_err(|e| format!("解码失败: {}", e))?; + + if let Some(parent) = std::path::Path::new(&path).parent() { + let parent_str = parent.to_string_lossy().to_string(); + ensure_resources_dir(&parent_str) + .map_err(|e| format!("无法创建目录: {}", e))?; + } + + fs::write(&path, &bytes).map_err(|e| format!("无法写入文件: {}", e)) +} + #[tauri::command] fn pick_folder(app: tauri::AppHandle) -> Option { app.dialog() @@ -200,7 +254,14 @@ fn create_file(path: String) -> Result<(), String> { if let Some(parent) = PathBuf::from(&path).parent() { fs::create_dir_all(parent).map_err(|e| format!("无法创建父目录: {}", e))?; } - fs::write(&path, "").map_err(|e| format!("无法创建文件: {}", e)) + fs::write(&path, "").map_err(|e| format!("无法创建文件: {}", e))?; + + // Create companion hidden resources directory for markdown files. + if let Some(res_dir) = resources_dir_path(&path) { + ensure_resources_dir(&res_dir).map_err(|e| format!("无法创建资源文件夹: {}", e))?; + } + + Ok(()) } #[tauri::command] @@ -209,7 +270,17 @@ fn delete_entry(path: String) -> Result<(), String> { if p.is_dir() { fs::remove_dir_all(&p).map_err(|e| format!("无法删除文件夹: {}", e)) } else if p.is_file() { - fs::remove_file(&p).map_err(|e| format!("无法删除文件: {}", e)) + fs::remove_file(&p).map_err(|e| format!("无法删除文件: {}", e))?; + + // If this was a markdown file, also remove its companion resources directory. + if let Some(res_dir) = resources_dir_path(&path) { + let res = PathBuf::from(&res_dir); + if res.is_dir() { + let _ = fs::remove_dir_all(&res); + } + } + + Ok(()) } else { Err("路径不存在".to_string()) } @@ -222,6 +293,21 @@ fn rename_entry(path: String, new_name: String) -> Result { .parent() .ok_or_else(|| "无法获取父目录".to_string())?; let new_path = parent.join(&new_name); + + // If this is a markdown file, rename its companion resources directory first. + if let Some(old_res_dir) = resources_dir_path(&path) { + let old_res = PathBuf::from(&old_res_dir); + if old_res.is_dir() { + let new_res_dir = resources_dir_path(&new_path.to_string_lossy()); + if let Some(new_res) = new_res_dir { + let new_res_path = PathBuf::from(&new_res); + if new_res_path != old_res { + let _ = fs::rename(&old_res, &new_res_path); + } + } + } + } + fs::rename(&p, &new_path).map_err(|e| format!("无法重命名: {}", e))?; Ok(new_path.to_string_lossy().to_string()) } @@ -249,7 +335,8 @@ pub fn run() { create_folder, create_file, delete_entry, - rename_entry + rename_entry, + save_media_file ]) .setup(|app| { // ---- File menu ---- diff --git a/src/components/MarkdownEditor.vue b/src/components/MarkdownEditor.vue index c5b1438..e972a6b 100644 --- a/src/components/MarkdownEditor.vue +++ b/src/components/MarkdownEditor.vue @@ -13,7 +13,7 @@ import remarkGfm from "remark-gfm"; import remarkRehype from "remark-rehype"; import rehypeStringify from "rehype-stringify"; import rehypeSanitize, { defaultSchema } from "rehype-sanitize"; -import { convertFileSrc, isTauri } from "@tauri-apps/api/core"; +import { convertFileSrc, invoke, isTauri } from "@tauri-apps/api/core"; type DocumentMode = "preview" | "edit" | "mixed"; const IMAGE_EXTENSIONS = new Set(["png", "jpg", "jpeg", "gif", "webp", "avif"]); @@ -97,7 +97,16 @@ function getCurrentFileDirectory() { return props.filePath.slice(0, separatorIndex); } +function isWindowsAbsolutePath(path: string) { + return /^[A-Za-z]:[\\/]/.test(path); +} + +function isUncPath(path: string) { + return path.startsWith("\\\\"); +} + function isExternalUrl(url: string) { + if (isWindowsAbsolutePath(url) || isUncPath(url)) return false; return /^[a-z][a-z\d+.-]*:/i.test(url) || url.startsWith("//") || url.startsWith("#"); } @@ -105,7 +114,7 @@ function normalizeLocalMediaPath(url: string) { const trimmed = url.trim(); if (!trimmed || isExternalUrl(trimmed)) return trimmed; - if (/^[A-Za-z]:[\\/]/.test(trimmed) || trimmed.startsWith("\\\\")) { + if (isWindowsAbsolutePath(trimmed) || isUncPath(trimmed)) { return trimmed; } @@ -120,8 +129,8 @@ function normalizeLocalMediaPath(url: string) { function toAssetSrc(path: string) { if (isExternalUrl(path)) return path; if (isTauri()) return convertFileSrc(path); - if (/^[A-Za-z]:[\\/]/.test(path)) return `file:///${path.replace(/\\/g, "/")}`; - if (path.startsWith("\\\\")) return `file:${path.replace(/\\/g, "/")}`; + if (isWindowsAbsolutePath(path)) return `file:///${path.replace(/\\/g, "/")}`; + if (isUncPath(path)) return `file:${path.replace(/\\/g, "/")}`; return path; } @@ -679,6 +688,176 @@ onBeforeUnmount(() => { } }); +// ── Paste image / file handling ───────────────────────────────── + +function blobToBase64(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + const result = reader.result as string; + // Strip the "data:xxx;base64," prefix + resolve(result.split(',')[1] || ''); + }; + reader.onerror = () => reject(reader.error); + reader.readAsDataURL(blob); + }); +} + +function generateMediaFilename(ext: string): string { + const now = new Date(); + const pad = (n: number) => String(n).padStart(2, '0'); + const ts = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`; + const suffix = Math.random().toString(36).slice(2, 6); + return `${ts}_${suffix}.${ext}`; +} + +function imageExtFromMime(mime: string): string { + const map: Record = { + 'image/png': 'png', + 'image/jpeg': 'jpg', + 'image/gif': 'gif', + 'image/webp': 'webp', + 'image/avif': 'avif', + 'image/bmp': 'bmp', + 'image/svg+xml': 'svg', + }; + return map[mime] || 'png'; +} + +/** + * Core paste handler: detects images/files in clipboard, saves them to the + * hidden resources folder, and inserts markdown image syntax at the cursor. + */ +async function processPaste( + e: ClipboardEvent, + value: string, + selectionStart: number, + selectionEnd: number, + applyValue: (newValue: string, cursorPos: number) => void, +) { + const items = e.clipboardData?.items; + if (!items || items.length === 0) return; + + // Collect media items (images from screenshots, or files from file-manager copy) + const mediaItems: { item: DataTransferItem }[] = []; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (item.type.startsWith('image/') || item.type.startsWith('video/') || item.kind === 'file') { + mediaItems.push({ item }); + } + } + + if (mediaItems.length === 0) return; // No media — let default text paste happen + + e.preventDefault(); + e.stopPropagation(); + + if (!props.filePath) return; // No file open, nowhere to save + + const dir = getCurrentFileDirectory(); + if (!dir) return; + + // Derive the hidden resources folder name from the current markdown file + const mdFileName = props.filePath.split(/[/\\]/).pop() || 'document'; + const stem = mdFileName.replace(/\.md$/i, ''); + const resourcesDir = `${dir}/.${stem}`; + const resourcesRelDir = `.${stem}`; + + // Process each media item and build a replacement string + const insertParts: string[] = []; + + for (const { item } of mediaItems) { + const file = item.getAsFile(); + if (!file) continue; + + let ext: string; + let blob: Blob; + + if (item.type.startsWith('image/')) { + ext = imageExtFromMime(item.type); + blob = file; + } else if (item.type.startsWith('video/')) { + ext = item.type.split('/')[1] || 'mp4'; + blob = file; + } else { + // File from file manager — derive extension from file name + const dot = file.name.lastIndexOf('.'); + ext = dot >= 0 ? file.name.slice(dot + 1).toLowerCase() : 'bin'; + blob = file; + } + + const filename = generateMediaFilename(ext); + const assetPath = `${resourcesDir}/${filename}`; + const relativePath = `${resourcesRelDir}/${filename}`; + + try { + const base64 = await blobToBase64(blob); + await invoke("save_media_file", { path: assetPath, data: base64 }); + } catch (err) { + console.error("Failed to save pasted media:", err); + continue; + } + + const alt = file.name || 'image'; + // Separate multiple pasted images with a line break + if (insertParts.length > 0) insertParts.push('\n'); + insertParts.push(`![${alt}](${relativePath})`); + } + + if (insertParts.length === 0) return; + + const insertText = insertParts.join(''); + const newValue = + value.slice(0, selectionStart) + insertText + value.slice(selectionEnd); + const cursorPos = selectionStart + insertText.length; + applyValue(newValue, cursorPos); +} + +async function onEditorPaste(e: ClipboardEvent) { + const ta = editorTextareaRef.value; + if (!ta) return; + + await processPaste( + e, + props.modelValue, + ta.selectionStart, + ta.selectionEnd, + (newValue, cursorPos) => { + applyFullEditorValue(newValue); + void nextTick(() => { + const ta = editorTextareaRef.value; + if (ta) { + ta.selectionStart = ta.selectionEnd = cursorPos; + ta.focus(); + } + }); + }, + ); +} + +async function onBlockPaste(e: ClipboardEvent) { + const ta = blockTextareaRef.value; + if (!ta) return; + + await processPaste( + e, + editingSource.value, + ta.selectionStart, + ta.selectionEnd, + (newValue, cursorPos) => { + editingSource.value = newValue; + void nextTick(() => { + const ta = blockTextareaRef.value; + if (ta) { + ta.selectionStart = ta.selectionEnd = cursorPos; + ta.focus(); + scheduleTextareaResize(ta); + } + }); + }, + ); +} + defineExpose({ flushPendingChanges, }); @@ -692,7 +871,7 @@ defineExpose({ @keydown.capture="onRootKeydown" >
@@ -718,7 +898,7 @@ defineExpose({
⚠ Markdown 解析错误
-
+
@@ -736,6 +916,7 @@ defineExpose({ @input="onBlockInput" @keydown="onBlockKeydown" @blur="finalizeBlockEdit" + @paste="onBlockPaste" />
+import type { DropdownMenuRootEmits, DropdownMenuRootProps } from "reka-ui" +import { DropdownMenuRoot, useForwardPropsEmits } from "reka-ui" + +const props = defineProps() +const emits = defineEmits() + +const forwarded = useForwardPropsEmits(props, emits) + + + diff --git a/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue b/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue new file mode 100644 index 0000000..dcfc608 --- /dev/null +++ b/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/components/ui/dropdown-menu/DropdownMenuContent.vue b/src/components/ui/dropdown-menu/DropdownMenuContent.vue new file mode 100644 index 0000000..8cba214 --- /dev/null +++ b/src/components/ui/dropdown-menu/DropdownMenuContent.vue @@ -0,0 +1,40 @@ + + + diff --git a/src/components/ui/dropdown-menu/DropdownMenuGroup.vue b/src/components/ui/dropdown-menu/DropdownMenuGroup.vue new file mode 100644 index 0000000..da634ec --- /dev/null +++ b/src/components/ui/dropdown-menu/DropdownMenuGroup.vue @@ -0,0 +1,15 @@ + + + diff --git a/src/components/ui/dropdown-menu/DropdownMenuItem.vue b/src/components/ui/dropdown-menu/DropdownMenuItem.vue new file mode 100644 index 0000000..be7fe0d --- /dev/null +++ b/src/components/ui/dropdown-menu/DropdownMenuItem.vue @@ -0,0 +1,31 @@ + + + diff --git a/src/components/ui/dropdown-menu/DropdownMenuLabel.vue b/src/components/ui/dropdown-menu/DropdownMenuLabel.vue new file mode 100644 index 0000000..af0bd7d --- /dev/null +++ b/src/components/ui/dropdown-menu/DropdownMenuLabel.vue @@ -0,0 +1,23 @@ + + + diff --git a/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue b/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue new file mode 100644 index 0000000..fe82cad --- /dev/null +++ b/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue @@ -0,0 +1,21 @@ + + + diff --git a/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue b/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue new file mode 100644 index 0000000..bc86550 --- /dev/null +++ b/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue @@ -0,0 +1,44 @@ + + + diff --git a/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue b/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue new file mode 100644 index 0000000..1b936c3 --- /dev/null +++ b/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue @@ -0,0 +1,23 @@ + + + diff --git a/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue b/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue new file mode 100644 index 0000000..50fe790 --- /dev/null +++ b/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/components/ui/dropdown-menu/DropdownMenuSub.vue b/src/components/ui/dropdown-menu/DropdownMenuSub.vue new file mode 100644 index 0000000..7472e77 --- /dev/null +++ b/src/components/ui/dropdown-menu/DropdownMenuSub.vue @@ -0,0 +1,18 @@ + + + diff --git a/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue b/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue new file mode 100644 index 0000000..02e2e8a --- /dev/null +++ b/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue @@ -0,0 +1,27 @@ + + + diff --git a/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue b/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue new file mode 100644 index 0000000..62b1983 --- /dev/null +++ b/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue @@ -0,0 +1,32 @@ + + + diff --git a/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue b/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue new file mode 100644 index 0000000..75cd747 --- /dev/null +++ b/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/components/ui/dropdown-menu/index.ts b/src/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000..955fe3a --- /dev/null +++ b/src/components/ui/dropdown-menu/index.ts @@ -0,0 +1,16 @@ +export { default as DropdownMenu } from "./DropdownMenu.vue" + +export { default as DropdownMenuCheckboxItem } from "./DropdownMenuCheckboxItem.vue" +export { default as DropdownMenuContent } from "./DropdownMenuContent.vue" +export { default as DropdownMenuGroup } from "./DropdownMenuGroup.vue" +export { default as DropdownMenuItem } from "./DropdownMenuItem.vue" +export { default as DropdownMenuLabel } from "./DropdownMenuLabel.vue" +export { default as DropdownMenuRadioGroup } from "./DropdownMenuRadioGroup.vue" +export { default as DropdownMenuRadioItem } from "./DropdownMenuRadioItem.vue" +export { default as DropdownMenuSeparator } from "./DropdownMenuSeparator.vue" +export { default as DropdownMenuShortcut } from "./DropdownMenuShortcut.vue" +export { default as DropdownMenuSub } from "./DropdownMenuSub.vue" +export { default as DropdownMenuSubContent } from "./DropdownMenuSubContent.vue" +export { default as DropdownMenuSubTrigger } from "./DropdownMenuSubTrigger.vue" +export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue" +export { DropdownMenuPortal } from "reka-ui" diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..365058c --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +}