From 8b1e5b2ee72e88bd5820a2b0a22e37085f69fce1 Mon Sep 17 00:00:00 2001 From: Aden Linday Date: Thu, 16 Apr 2026 20:22:00 +0930 Subject: [PATCH] feat: start custom messages plugin --- src/plugins/built-in/messageFolders/index.ts | 670 ++++++++++++++++++ .../built-in/messageFolders/styles.css | 491 +++++++++++++ src/plugins/index.ts | 2 + 3 files changed, 1163 insertions(+) create mode 100644 src/plugins/built-in/messageFolders/index.ts create mode 100644 src/plugins/built-in/messageFolders/styles.css diff --git a/src/plugins/built-in/messageFolders/index.ts b/src/plugins/built-in/messageFolders/index.ts new file mode 100644 index 00000000..c0b35cb9 --- /dev/null +++ b/src/plugins/built-in/messageFolders/index.ts @@ -0,0 +1,670 @@ +import type { Plugin } from "../../core/types"; +import { waitForElm } from "@/seqta/utils/waitForElm"; +import styles from "./styles.css?inline"; + +interface Folder { + id: string; + name: string; + color: string; +} + +interface MessageFoldersStorage { + folders: Folder[]; + messageAssignments: Record; +} + +const FOLDER_COLORS = [ + "#3b82f6", "#ef4444", "#22c55e", "#f59e0b", + "#8b5cf6", "#ec4899", "#14b8a6", "#f97316", +]; + +const FOLDER_ICON_SVG = ``; +const PLUS_SVG = ``; +const CHECK_SVG_DARK = ``; +const CHECK_SVG_WHITE = ``; +const CLOSE_SVG = ``; +const EDIT_SVG = ``; +const TRASH_SVG = ``; + +function generateId(): string { + return Date.now().toString(36) + Math.random().toString(36).slice(2, 7); +} + +const messageFoldersPlugin: Plugin<{}, MessageFoldersStorage> = { + id: "messageFolders", + name: "Message Folders", + description: "Organize direct messages into custom folders", + version: "1.0.0", + settings: {}, + disableToggle: true, + defaultEnabled: true, + + run: async (api) => { + const styleEl = document.createElement("style"); + styleEl.textContent = styles; + document.head.appendChild(styleEl); + + await api.storage.loaded; + + if (!api.storage.folders) api.storage.folders = []; + if (!api.storage.messageAssignments) api.storage.messageAssignments = {}; + + let activeFolderId: string | null = null; + let messageListObserver: MutationObserver | null = null; + let sidebarObserver: MutationObserver | null = null; + let actionsObserver: MutationObserver | null = null; + let openDropdown: HTMLElement | null = null; + let dropdownCloseHandler: ((e: MouseEvent) => void) | null = null; + const unregisters: Array<{ unregister: () => void }> = []; + + // ── Storage accessors ── + + const getFolders = (): Folder[] => api.storage.folders ?? []; + const getAssignments = (): Record => api.storage.messageAssignments ?? {}; + + const saveFolders = (folders: Folder[]) => { + api.storage.folders = [...folders]; + }; + + const saveAssignments = (assignments: Record) => { + api.storage.messageAssignments = { ...assignments }; + }; + + const getMessageFolderIds = (messageId: string): string[] => { + const assignments = getAssignments(); + const ids: string[] = []; + for (const [folderId, msgIds] of Object.entries(assignments)) { + if (msgIds.includes(messageId)) ids.push(folderId); + } + return ids; + }; + + const toggleMessageInFolder = (messageId: string, folderId: string) => { + const assignments = getAssignments(); + if (!assignments[folderId]) assignments[folderId] = []; + const idx = assignments[folderId].indexOf(messageId); + if (idx >= 0) { + assignments[folderId].splice(idx, 1); + } else { + assignments[folderId].push(messageId); + } + saveAssignments(assignments); + }; + + const getFolderMessageCount = (folderId: string): number => { + return (getAssignments()[folderId] ?? []).length; + }; + + // ── Confirm modal ── + + const showConfirmModal = ( + title: string, + message: string, + onConfirm: () => void, + ) => { + const overlay = document.createElement("div"); + overlay.className = "bsplus-modal-overlay"; + + const modal = document.createElement("div"); + modal.className = "bsplus-modal"; + modal.innerHTML = ` +

${title}

+

${message}

+
+ + +
+ `; + overlay.appendChild(modal); + + const remove = () => { + overlay.remove(); + document.removeEventListener("keydown", onKey); + }; + + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") remove(); + }; + + overlay.addEventListener("click", (e) => { + if (e.target === overlay) remove(); + }); + modal.querySelector(".bsplus-modal-btn-cancel")!.addEventListener("click", remove); + modal.querySelector(".bsplus-modal-btn-danger")!.addEventListener("click", () => { + onConfirm(); + remove(); + }); + + document.body.appendChild(overlay); + document.addEventListener("keydown", onKey); + }; + + // ── Sidebar folder UI ── + + const renderSidebarFolders = () => { + const sidebar = document.querySelector("[class*='Viewer__sidebar___']"); + if (!sidebar) return; + + const ol = sidebar.querySelector("ol"); + if (!ol) return; + + let section = ol.querySelector(".bsplus-folders-section"); + if (!section) { + section = document.createElement("div"); + section.className = "bsplus-folders-section"; + ol.appendChild(section); + } + + const folders = getFolders(); + const existingInput = section.querySelector(".bsplus-folder-input"); + const existingColors = section.querySelector(".bsplus-folder-colors"); + + section.innerHTML = ""; + + // Header + const header = document.createElement("div"); + header.className = "bsplus-folders-header"; + + const label = document.createElement("span"); + label.textContent = "Folders"; + header.appendChild(label); + + const addBtn = document.createElement("button"); + addBtn.className = "bsplus-folders-add-btn"; + addBtn.title = "New folder"; + addBtn.innerHTML = PLUS_SVG; + addBtn.addEventListener("click", (e) => { + e.stopPropagation(); + showNewFolderInput(section!); + }); + header.appendChild(addBtn); + section.appendChild(header); + + // "All Messages" item + const allItem = document.createElement("div"); + allItem.className = `bsplus-folder-item${activeFolderId === null ? " bsplus-folder-active" : ""}`; + allItem.innerHTML = ` + + All Messages + `; + allItem.addEventListener("click", () => { + activeFolderId = null; + applyFolderFilter(); + renderSidebarFolders(); + }); + section.appendChild(allItem); + + // Folder items + for (const folder of folders) { + const item = document.createElement("div"); + item.className = `bsplus-folder-item${activeFolderId === folder.id ? " bsplus-folder-active" : ""}`; + item.dataset.folderId = folder.id; + + const dot = document.createElement("div"); + dot.className = "bsplus-folder-dot"; + dot.style.background = folder.color; + item.appendChild(dot); + + const name = document.createElement("span"); + name.className = "bsplus-folder-name"; + name.textContent = folder.name; + item.appendChild(name); + + const actions = document.createElement("div"); + actions.className = "bsplus-folder-actions"; + + const editBtn = document.createElement("button"); + editBtn.className = "bsplus-folder-action-btn"; + editBtn.title = "Rename"; + editBtn.innerHTML = EDIT_SVG; + editBtn.addEventListener("click", (e) => { + e.stopPropagation(); + showEditFolderInput(section!, folder); + }); + actions.appendChild(editBtn); + + const deleteBtn = document.createElement("button"); + deleteBtn.className = "bsplus-folder-action-btn"; + deleteBtn.title = "Delete"; + deleteBtn.innerHTML = TRASH_SVG; + deleteBtn.addEventListener("click", (e) => { + e.stopPropagation(); + showConfirmModal( + "Delete folder", + `Remove "${folder.name}"? Messages won't be deleted.`, + () => { + const folders = getFolders().filter((f) => f.id !== folder.id); + saveFolders(folders); + const assignments = getAssignments(); + delete assignments[folder.id]; + saveAssignments(assignments); + if (activeFolderId === folder.id) activeFolderId = null; + applyFolderFilter(); + applyBadges(); + renderSidebarFolders(); + }, + ); + }); + actions.appendChild(deleteBtn); + + item.appendChild(actions); + + const count = document.createElement("span"); + count.className = "bsplus-folder-count"; + const c = getFolderMessageCount(folder.id); + count.textContent = c > 0 ? String(c) : ""; + item.appendChild(count); + + item.addEventListener("click", () => { + activeFolderId = folder.id; + applyFolderFilter(); + renderSidebarFolders(); + }); + + section.appendChild(item); + } + + // Restore input if it was open + if (existingInput || existingColors) { + // Don't restore – let user re-trigger + } + }; + + const showNewFolderInput = (container: Element, editFolder?: Folder) => { + const existing = container.querySelector(".bsplus-folder-input"); + if (existing) existing.remove(); + container.querySelector(".bsplus-folder-colors")?.remove(); + + let selectedColor = editFolder?.color ?? FOLDER_COLORS[Math.floor(Math.random() * FOLDER_COLORS.length)]; + + const row = document.createElement("div"); + row.className = "bsplus-folder-input"; + + const input = document.createElement("input"); + input.type = "text"; + input.placeholder = editFolder ? "Rename folder…" : "Folder name…"; + input.value = editFolder?.name ?? ""; + input.maxLength = 30; + + const confirmBtn = document.createElement("button"); + confirmBtn.className = "bsplus-folder-input-confirm"; + confirmBtn.innerHTML = CHECK_SVG_WHITE; + + const cancelBtn = document.createElement("button"); + cancelBtn.className = "bsplus-folder-input-cancel"; + cancelBtn.innerHTML = CLOSE_SVG; + + row.appendChild(input); + row.appendChild(confirmBtn); + row.appendChild(cancelBtn); + + // Color picker + const colorRow = document.createElement("div"); + colorRow.className = "bsplus-folder-colors"; + for (const color of FOLDER_COLORS) { + const swatch = document.createElement("button"); + swatch.className = `bsplus-folder-color-opt${color === selectedColor ? " bsplus-color-selected" : ""}`; + swatch.style.background = color; + swatch.addEventListener("click", (e) => { + e.stopPropagation(); + selectedColor = color; + colorRow.querySelectorAll(".bsplus-folder-color-opt").forEach((s) => + s.classList.toggle("bsplus-color-selected", (s as HTMLElement).style.background === color), + ); + }); + colorRow.appendChild(swatch); + } + + const confirm = () => { + const name = input.value.trim(); + if (!name) return; + + if (editFolder) { + const folders = getFolders().map((f) => + f.id === editFolder.id ? { ...f, name, color: selectedColor } : f, + ); + saveFolders(folders); + } else { + const folder: Folder = { id: generateId(), name, color: selectedColor }; + saveFolders([...getFolders(), folder]); + } + applyBadges(); + renderSidebarFolders(); + }; + + confirmBtn.addEventListener("click", (e) => { + e.stopPropagation(); + confirm(); + }); + cancelBtn.addEventListener("click", (e) => { + e.stopPropagation(); + renderSidebarFolders(); + }); + input.addEventListener("keydown", (e) => { + if (e.key === "Enter") confirm(); + if (e.key === "Escape") renderSidebarFolders(); + }); + + container.appendChild(row); + container.appendChild(colorRow); + requestAnimationFrame(() => input.focus()); + }; + + const showEditFolderInput = (container: Element, folder: Folder) => { + showNewFolderInput(container, folder); + }; + + // ── Intercept native sidebar clicks to clear folder filter ── + + const attachNativeSidebarListeners = () => { + const sidebar = document.querySelector("[class*='Viewer__sidebar___']"); + if (!sidebar) return; + + const ol = sidebar.querySelector("ol"); + if (!ol) return; + + ol.addEventListener("click", (e) => { + const target = e.target as HTMLElement; + if (target.closest(".bsplus-folders-section")) return; + + const li = target.closest("li"); + if (li && ol.contains(li)) { + if (activeFolderId !== null) { + activeFolderId = null; + applyFolderFilter(); + renderSidebarFolders(); + } + } + }); + }; + + // ── "Add to folder" button in message action bar ── + + const injectFolderButton = (actionsBar: Element) => { + if (actionsBar.querySelector(".bsplus-folder-btn")) return; + + const wrapper = document.createElement("div"); + wrapper.className = "bsplus-folder-btn"; + wrapper.style.position = "relative"; + wrapper.style.display = "inline-block"; + + const btn = document.createElement("button"); + const btnClasses = actionsBar.querySelector("button")?.className ?? ""; + btn.className = btnClasses; + btn.title = "Add to folder"; + btn.innerHTML = FOLDER_ICON_SVG; + + btn.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + closeDropdown(); + + const selectedMsg = document.querySelector("[class*='MessageList__selected___']"); + const messageId = selectedMsg?.getAttribute("data-message"); + if (!messageId) return; + + showFolderDropdown(wrapper, messageId); + }); + + wrapper.appendChild(btn); + + const moreMenu = actionsBar.querySelector("[class*='MenuButton__Menu___']"); + if (moreMenu) { + actionsBar.insertBefore(wrapper, moreMenu); + } else { + actionsBar.appendChild(wrapper); + } + }; + + const showFolderDropdown = (anchor: HTMLElement, messageId: string) => { + const dropdown = document.createElement("div"); + dropdown.className = "bsplus-folder-dropdown"; + + const folders = getFolders(); + const currentFolderIds = getMessageFolderIds(messageId); + + if (folders.length === 0) { + const empty = document.createElement("div"); + empty.className = "bsplus-folder-dropdown-empty"; + empty.textContent = "No folders yet"; + dropdown.appendChild(empty); + } else { + for (const folder of folders) { + const isChecked = currentFolderIds.includes(folder.id); + const item = document.createElement("button"); + item.className = `bsplus-folder-dropdown-item${isChecked ? " bsplus-checked" : ""}`; + + const check = document.createElement("div"); + check.className = "bsplus-folder-dropdown-check"; + check.style.borderColor = isChecked ? folder.color : ""; + check.style.background = isChecked ? folder.color : ""; + check.innerHTML = CHECK_SVG_WHITE; + + const dot = document.createElement("div"); + dot.className = "bsplus-folder-dot"; + dot.style.background = folder.color; + + const name = document.createElement("span"); + name.textContent = folder.name; + + item.appendChild(check); + item.appendChild(dot); + item.appendChild(name); + + item.addEventListener("click", (e) => { + e.stopPropagation(); + toggleMessageInFolder(messageId, folder.id); + + const nowChecked = getMessageFolderIds(messageId).includes(folder.id); + item.classList.toggle("bsplus-checked", nowChecked); + check.style.borderColor = nowChecked ? folder.color : ""; + check.style.background = nowChecked ? folder.color : ""; + + applyBadges(); + applyFolderFilter(); + renderSidebarFolders(); + }); + + dropdown.appendChild(item); + } + } + + anchor.appendChild(dropdown); + openDropdown = dropdown; + + dropdownCloseHandler = (e: MouseEvent) => { + if (!dropdown.contains(e.target as Node) && !anchor.contains(e.target as Node)) { + closeDropdown(); + } + }; + setTimeout(() => { + document.addEventListener("click", dropdownCloseHandler!, true); + }, 0); + }; + + const closeDropdown = () => { + if (openDropdown) { + openDropdown.remove(); + openDropdown = null; + } + if (dropdownCloseHandler) { + document.removeEventListener("click", dropdownCloseHandler, true); + dropdownCloseHandler = null; + } + }; + + // ── Message badges ── + + const applyBadges = () => { + const folders = getFolders(); + const assignments = getAssignments(); + const messageItems = document.querySelectorAll("[class*='MessageList__MessageList___'] ol > li[data-message]"); + + for (const li of messageItems) { + const msgId = li.getAttribute("data-message"); + if (!msgId) continue; + + let badgeContainer = li.querySelector(".bsplus-msg-badges") as HTMLElement | null; + + const folderIds = []; + for (const [fId, mIds] of Object.entries(assignments)) { + if (mIds.includes(msgId)) folderIds.push(fId); + } + + if (folderIds.length === 0) { + badgeContainer?.remove(); + continue; + } + + if (!badgeContainer) { + badgeContainer = document.createElement("div"); + badgeContainer.className = "bsplus-msg-badges"; + const subject = li.querySelector("[class*='MessageList__subject___']"); + if (subject) { + if (!subject.querySelector(".bsplus-subject-text")) { + const textWrap = document.createElement("span"); + textWrap.className = "bsplus-subject-text"; + textWrap.textContent = subject.textContent; + subject.textContent = ""; + subject.appendChild(textWrap); + } + subject.appendChild(badgeContainer); + } else { + li.appendChild(badgeContainer); + } + } + + badgeContainer.innerHTML = ""; + for (const fId of folderIds) { + const folder = folders.find((f) => f.id === fId); + if (!folder) continue; + const badge = document.createElement("span"); + badge.className = "bsplus-msg-badge"; + badge.style.background = folder.color; + badge.textContent = folder.name; + badge.title = `Filter by "${folder.name}"`; + badge.addEventListener("click", (e) => { + e.stopPropagation(); + activeFolderId = folder.id; + applyFolderFilter(); + renderSidebarFolders(); + }); + badgeContainer.appendChild(badge); + } + } + }; + + // ── Folder filtering ── + + const applyFolderFilter = () => { + const messageItems = document.querySelectorAll("[class*='MessageList__MessageList___'] ol > li[data-message]"); + const moreBtn = document.querySelector("[class*='MessageList__MessageList___'] ol > button"); + + if (activeFolderId === null) { + for (const li of messageItems) { + li.classList.remove("bsplus-folder-hidden"); + } + if (moreBtn) (moreBtn as HTMLElement).classList.remove("bsplus-folder-hidden"); + return; + } + + const folderMsgIds = getAssignments()[activeFolderId] ?? []; + + for (const li of messageItems) { + const msgId = li.getAttribute("data-message"); + if (msgId && folderMsgIds.includes(msgId)) { + li.classList.remove("bsplus-folder-hidden"); + } else { + li.classList.add("bsplus-folder-hidden"); + } + } + if (moreBtn) (moreBtn as HTMLElement).classList.add("bsplus-folder-hidden"); + }; + + // ── Observers ── + + const setupMessageListObserver = () => { + const messageList = document.querySelector("[class*='MessageList__MessageList___'] ol"); + if (!messageList || messageListObserver) return; + + messageListObserver = new MutationObserver(() => { + applyBadges(); + applyFolderFilter(); + }); + messageListObserver.observe(messageList, { childList: true, subtree: false }); + }; + + const setupActionsObserver = () => { + if (actionsObserver) return; + + const target = document.querySelector("[class*='Viewer__Viewer___']") ?? document.querySelector("div.messages"); + if (!target) return; + + actionsObserver = new MutationObserver(() => { + const actionsBar = document.querySelector("[class*='Message__actions___']"); + if (actionsBar && !actionsBar.querySelector(".bsplus-folder-btn")) { + injectFolderButton(actionsBar); + } + }); + actionsObserver.observe(target, { childList: true, subtree: true }); + }; + + // ── Main page handler ── + + const handleMessagesPage = async () => { + await waitForElm("[class*='Viewer__sidebar___'] ol", true, 50, 100); + + renderSidebarFolders(); + attachNativeSidebarListeners(); + + await waitForElm("[class*='MessageList__MessageList___'] ol", true, 50, 100); + applyBadges(); + applyFolderFilter(); + setupMessageListObserver(); + + // The actions bar only exists when a message is selected/open, + // so we observe the whole viewer for it to appear dynamically + setupActionsObserver(); + + // If a message is already selected, inject immediately + const actionsBar = document.querySelector("[class*='Message__actions___']"); + if (actionsBar) injectFolderButton(actionsBar); + + // Re-observe the sidebar for SEQTA re-renders + const sidebar = document.querySelector("[class*='Viewer__sidebar___']"); + if (sidebar && !sidebarObserver) { + sidebarObserver = new MutationObserver(() => { + const ol = sidebar.querySelector("ol"); + if (ol && !ol.querySelector(".bsplus-folders-section")) { + renderSidebarFolders(); + attachNativeSidebarListeners(); + } + }); + sidebarObserver.observe(sidebar, { childList: true, subtree: true }); + } + }; + + // ── Lifecycle ── + + const mountUnsub = api.seqta.onMount("div.messages", handleMessagesPage); + unregisters.push(mountUnsub); + + return () => { + for (const u of unregisters) u.unregister(); + messageListObserver?.disconnect(); + sidebarObserver?.disconnect(); + actionsObserver?.disconnect(); + closeDropdown(); + styleEl.remove(); + document.querySelectorAll(".bsplus-folders-section").forEach((el) => el.remove()); + document.querySelectorAll(".bsplus-folder-btn").forEach((el) => el.remove()); + document.querySelectorAll(".bsplus-msg-badges").forEach((el) => el.remove()); + document.querySelectorAll(".bsplus-folder-hidden").forEach((el) => + el.classList.remove("bsplus-folder-hidden"), + ); + document.querySelectorAll(".bsplus-modal-overlay").forEach((el) => el.remove()); + }; + }, +}; + +export default messageFoldersPlugin; diff --git a/src/plugins/built-in/messageFolders/styles.css b/src/plugins/built-in/messageFolders/styles.css new file mode 100644 index 00000000..e239a883 --- /dev/null +++ b/src/plugins/built-in/messageFolders/styles.css @@ -0,0 +1,491 @@ +/* ── Sidebar folder section ── */ +.bsplus-folders-section { + border-top: 1px solid var(--background-secondary, rgba(128, 128, 128, 0.2)); + margin-top: 4px; + padding-top: 4px; +} + +.bsplus-folders-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 12px 2px; + user-select: none; +} + +.bsplus-folders-header span { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-primary, #666); + opacity: 0.5; +} + +.bsplus-folders-add-btn { + display: flex !important; + align-items: center !important; + justify-content: center !important; + width: 20px !important; + height: 20px !important; + min-width: 0 !important; + border: none !important; + background: transparent !important; + opacity: 0.5; + cursor: pointer; + border-radius: 4px !important; + padding: 0 !important; + margin: 0 !important; + transition: all 0.2s ease; + text-align: center !important; +} + +.bsplus-folders-add-btn:hover { + opacity: 1; + background: var(--background-secondary, rgba(128, 128, 128, 0.1)) !important; +} + +/* ── Folder list items ── */ +.bsplus-folder-item { + display: flex; + align-items: center; + padding: 6px 12px; + cursor: pointer; + transition: background 0.15s ease; + position: relative; + gap: 8px; + user-select: none; +} + +.bsplus-folder-item:hover { + background: var(--theme-offset-bg-more, rgba(128, 128, 128, 0.08)); +} + +.bsplus-folder-item.bsplus-folder-active { + background: var(--theme-offset-bg-more, rgba(128, 128, 128, 0.12)); +} + +.bsplus-folder-item.bsplus-folder-active::before { + content: ""; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 3px; + background: var(--better-main, #007bff); + border-radius: 0 2px 2px 0; +} + +.bsplus-folder-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} + +.bsplus-folder-name { + font-size: 13px; + color: var(--text-primary, #333); + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.bsplus-folder-count { + font-size: 11px; + color: var(--text-primary, #999); + opacity: 0.5; + flex-shrink: 0; +} + +.bsplus-folder-actions { + display: flex; + gap: 2px; + opacity: 0; + transition: opacity 0.15s ease; +} + +.bsplus-folder-item:hover .bsplus-folder-actions { + opacity: 1; +} + +.bsplus-folder-action-btn { + display: flex !important; + align-items: center !important; + justify-content: center !important; + width: 20px !important; + height: 20px !important; + min-width: 0 !important; + border: none !important; + background: transparent !important; + opacity: 0.6; + cursor: pointer; + border-radius: 4px !important; + padding: 0 !important; + margin: 0 !important; + transition: all 0.15s ease; +} + +.bsplus-folder-action-btn:hover { + opacity: 1; + background: var(--background-secondary, rgba(128, 128, 128, 0.15)) !important; +} + +/* ── Inline folder name input ── */ +.bsplus-folder-input { + display: flex; + align-items: center; + padding: 4px 12px; + gap: 6px; +} + +.bsplus-folder-input input { + flex: 1; + min-width: 0; + padding: 4px 8px; + font-size: 13px; + border: 1px solid var(--background-secondary, #ccc); + border-radius: 6px; + background: var(--background-secondary, #f5f5f5); + color: var(--text-primary, #333); + outline: none; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.bsplus-folder-input input:focus { + border-color: var(--better-main, #007bff); + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.2); +} + +.bsplus-folder-input-confirm, +.bsplus-folder-input-cancel { + display: flex !important; + align-items: center !important; + justify-content: center !important; + width: 24px !important; + height: 24px !important; + min-width: 0 !important; + border: none !important; + border-radius: 4px !important; + cursor: pointer; + padding: 0 !important; + margin: 0 !important; + transition: all 0.15s ease; +} + +.bsplus-folder-input-confirm { + background: var(--better-main, #007bff) !important; +} + +.bsplus-folder-input-confirm:hover { + transform: scale(1.1); +} + +.bsplus-folder-input-cancel { + background: transparent !important; + opacity: 0.6; +} + +.bsplus-folder-input-cancel:hover { + opacity: 1; + background: var(--background-secondary, rgba(128, 128, 128, 0.1)) !important; +} + +/* ── Color picker row ── */ +.bsplus-folder-colors { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 6px; + padding: 4px 12px 6px; + max-width: 120px; +} + +.bsplus-folder-color-opt { + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid transparent; + cursor: pointer; + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), + border-color 0.2s ease, + box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1); + padding: 0; + background: none; + box-sizing: border-box; +} + +.bsplus-folder-color-opt:hover { + transform: scale(1.25); + box-shadow: 0 0 0 3px rgba(128, 128, 128, 0.15); +} + +.bsplus-folder-color-opt.bsplus-color-selected { + border-color: var(--text-primary, #333); + transform: scale(1.15); + box-shadow: 0 0 0 3px rgba(128, 128, 128, 0.2); +} + +.bsplus-folder-color-opt.bsplus-color-selected:hover { + transform: scale(1.25); +} + +/* ── "Add to folder" button in message actions bar ── */ +.bsplus-folder-btn { + position: relative; +} + +.bsplus-folder-btn svg { + fill: currentColor; +} + +/* ── Folder dropdown ── */ +.bsplus-folder-dropdown { + position: absolute; + top: 100%; + right: 0; + margin-top: 4px; + min-width: 180px; + background: var(--background-primary, #fff); + border: 1px solid var(--background-secondary, #e0e0e0); + border-radius: 8px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); + z-index: 1000; + overflow: hidden; + animation: bsplus-dropdown-in 0.15s ease-out; +} + +@keyframes bsplus-dropdown-in { + from { + opacity: 0; + transform: translateY(-4px) scale(0.97); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.bsplus-folder-dropdown-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + cursor: pointer; + transition: background 0.1s ease; + border: none; + background: transparent; + width: 100%; + text-align: left; + color: var(--text-primary, #333); + font-size: 13px; +} + +.bsplus-folder-dropdown-item:hover { + background: var(--theme-offset-bg-more, rgba(128, 128, 128, 0.08)); +} + +.bsplus-folder-dropdown-check { + width: 16px; + height: 16px; + border: 2px solid var(--background-secondary, #ccc); + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: all 0.15s ease; +} + +.bsplus-folder-dropdown-item.bsplus-checked .bsplus-folder-dropdown-check { + background: var(--better-main, #007bff); + border-color: var(--better-main, #007bff); +} + +.bsplus-folder-dropdown-check svg { + width: 10px; + height: 10px; + color: white; + opacity: 0; + transition: opacity 0.1s ease; +} + +.bsplus-folder-dropdown-item.bsplus-checked .bsplus-folder-dropdown-check svg { + opacity: 1; +} + +.bsplus-folder-dropdown-empty { + padding: 12px; + text-align: center; + font-size: 12px; + color: var(--text-primary, #999); + opacity: 0.5; +} + +/* ── Let primary column use available space instead of being clipped ── */ +[class*='MessageList__primary___'] { + flex: 1 1 0% !important; + min-width: 0 !important; + overflow: hidden !important; +} + +/* ── Make subject line a flex row so badges sit inline ── */ +[class*='MessageList__subject___'] { + display: flex !important; + align-items: center; + gap: 6px; + min-width: 0 !important; + overflow: hidden !important; +} + +/* ── Subject text truncates to make room for badges ── */ +.bsplus-subject-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + flex: 1 1 auto; +} + +/* ── Shrink the secondary column to its content ── */ +[class*='MessageList__secondary___'] { + flex: 0 0 auto !important; + width: auto !important; + min-width: 0 !important; + max-width: 200px !important; +} + +/* ── Constrain the flags/attachment icon column ── */ +[class*='MessageList__flags___'] { + width: 24px !important; + min-width: 0 !important; + flex-shrink: 0 !important; +} + +/* ── Message list folder badges ── */ +.bsplus-msg-badges { + display: inline-flex; + align-items: center; + gap: 3px; + flex-shrink: 0; + margin-left: auto; +} + +.bsplus-msg-badge { + display: inline-flex; + align-items: center; + gap: 3px; + padding: 1px 6px; + border-radius: 8px; + font-size: 10px; + font-weight: 500; + line-height: 1.4; + color: white; + white-space: nowrap; + cursor: pointer; + transition: opacity 0.2s ease, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +.bsplus-msg-badge:hover { + opacity: 0.85; + transform: scale(1.05); +} + +/* ── Folder filtering (hide messages not in active folder) ── */ +.bsplus-folder-hidden { + display: none !important; +} + +/* ── Delete confirmation modal ── */ +@keyframes bsplus-modal-overlay-in { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes bsplus-modal-in { + from { + opacity: 0; + transform: scale(0.95) translateY(-8px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +.bsplus-modal-overlay { + position: fixed; + inset: 0; + z-index: 2147483647; + display: flex; + justify-content: center; + align-items: center; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); + animation: bsplus-modal-overlay-in 0.2s ease-out forwards; +} + +.bsplus-modal { + padding: 1rem 1.5rem; + margin: 0 1rem; + min-width: 16rem; + max-width: 22rem; + width: 100%; + box-sizing: border-box; + background: var(--background-primary, #fff); + border-radius: 0.75rem; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); + border: 1px solid var(--background-secondary, #e0e0e0); + animation: bsplus-modal-in 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; +} + +.bsplus-modal h3 { + margin: 0 0 0.5rem; + font-size: 1rem; + font-weight: 600; + color: var(--text-primary, #333); +} + +.bsplus-modal p { + margin: 0 0 1rem; + font-size: 0.875rem; + color: var(--text-primary, #666); + opacity: 0.8; +} + +.bsplus-modal-actions { + display: flex; + gap: 0.75rem; + justify-content: flex-end; +} + +.bsplus-modal-actions button { + padding: 0.4rem 1rem; + font-size: 0.875rem; + font-weight: 500; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.bsplus-modal-btn-cancel { + background: transparent; + border: 1px solid var(--background-secondary, #ccc); + color: var(--text-primary, #333); +} + +.bsplus-modal-btn-cancel:hover { + background: var(--background-secondary, rgba(128, 128, 128, 0.1)); +} + +.bsplus-modal-btn-danger { + background: #e53e3e; + border: none; + color: white; +} + +.bsplus-modal-btn-danger:hover { + background: #c53030; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(229, 62, 62, 0.35); +} diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 0648accb..009e9e43 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -10,6 +10,7 @@ import assessmentsAveragePlugin from "./built-in/assessmentsAverage"; import profilePicturePlugin from "./built-in/profilePicture"; import assessmentsOverviewPlugin from "./built-in/assessmentsOverview"; import backgroundMusicPlugin from "./built-in/backgroundMusic"; +import messageFoldersPlugin from "./built-in/messageFolders"; //import testPlugin from './built-in/test'; // Heavy plugins (lazy-loaded only when enabled) @@ -28,6 +29,7 @@ pluginManager.registerPlugin(timetableEditPlugin); pluginManager.registerPlugin(profilePicturePlugin); pluginManager.registerPlugin(assessmentsOverviewPlugin); pluginManager.registerPlugin(backgroundMusicPlugin); +pluginManager.registerPlugin(messageFoldersPlugin); //pluginManager.registerPlugin(testPlugin); // Register heavy plugins with lazy loading