From 60330e191c752cd585543c99d5be3db1f5586c2b Mon Sep 17 00:00:00 2001 From: eondrcode <2106626532@qq.com> Date: Fri, 29 May 2026 00:00:00 +0800 Subject: [PATCH] chore: 1.0.4 --- package.json | 2 +- src/github-install.ts | 13 +- src/lang/locale/en.ts | 30 + src/lang/locale/zh_cn.ts | 30 + src/modal/group-modal.ts | 157 +++-- src/modal/manager-modal.ts | 470 +++++++++++-- src/modal/tags-modal.ts | 144 ++-- src/modal/update-modal.ts | 139 +++- styles.css | 1335 ++++++++++++++++++++++++++++++++++-- 9 files changed, 2108 insertions(+), 212 deletions(-) diff --git a/package.json b/package.json index 738a872..3cb952a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-sample-plugin", - "version": "1.0.3", + "version": "1.0.4", "description": "This is a sample plugin for Obsidian (https://obsidian.md)", "main": "main.js", "scripts": { diff --git a/src/github-install.ts b/src/github-install.ts index ae5ba4c..40ff1a3 100644 --- a/src/github-install.ts +++ b/src/github-install.ts @@ -62,7 +62,7 @@ export interface ReleaseVersion { } const API_BASE = "https://api.github.com"; -const RELEASES_PER_PAGE = 50; +const RELEASES_PER_PAGE = 100; const RELEASE_VERSION_CACHE_TTL_MS = 5 * 60 * 1000; /** @@ -292,9 +292,14 @@ export const fetchReleaseVersions = async (manager: Manager, repoInput: string): return cloneReleaseVersions(cached.versions); } - const url = `${API_BASE}/repos/${repo}/releases?per_page=${RELEASES_PER_PAGE}`; - const releases = await fetchJson(url, token); - if (!Array.isArray(releases)) return []; + const releases: ReleaseResponse[] = []; + for (let page = 1; ; page++) { + const url = `${API_BASE}/repos/${repo}/releases?per_page=${RELEASES_PER_PAGE}&page=${page}`; + const pageReleases = await fetchJson(url, token); + if (!Array.isArray(pageReleases) || pageReleases.length === 0) break; + releases.push(...pageReleases); + if (pageReleases.length < RELEASES_PER_PAGE) break; + } const versions = releases .map((release) => ({ diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 8dae54e..0b609cb 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -6,6 +6,8 @@ export default { 通用_操作_文本: 'Operation', 通用_搜索_文本: 'Search', 通用_名称_文本: 'Name', + 通用_ID_文本: 'ID', + 通用_颜色_文本: 'Color', 通用_无分组_文本: 'ALL', 通用_无标签_文本: 'ALL', 通用_无延迟_文本: 'No Delay', @@ -90,7 +92,13 @@ export default { 管理器_选择版本_获取失败提示: 'Failed to fetch version list. Please try again later.', 管理器_选择版本_无版本提示: 'No version list available. Will try the detected or latest version.', 管理器_选择版本_下载按钮: 'Download update', + 管理器_选择版本_切换按钮: 'Switch to selected version', 管理器_选择版本_成功提示: 'Downloaded and updated plugin', + 管理器_选择版本_说明: 'Choose the GitHub Release version to install.', + 管理器_选择版本_列表标题: 'Available versions', + 管理器_选择版本_当前版本: 'Installed: {version}', + 管理器_选择版本_版本数量: '{count} versions', + 管理器_选择版本_选择版本: 'Select version {version}', 筛选_全部_描述: 'All', 筛选_仅启用_描述: 'Enabled only', @@ -469,6 +477,27 @@ export default { 管理器_应用更改中_提示: 'Applying changes. Please avoid repeated actions.', 管理器_暂无匹配插件: 'No matching plugins', 管理器_安装_操作_安装中: 'Installing...', + 批量编辑_入口: 'Bulk edit', + 批量编辑_标题: 'Bulk edit', + 批量编辑_已选择数量: 'Selected {count}/{total}', + 批量编辑_全选当前列表: 'Select current list', + 批量编辑_清空选择: 'Clear selection', + 批量编辑_设置分组: 'Set group', + 批量编辑_清除分组: 'Clear group', + 批量编辑_添加标签: 'Add tag', + 批量编辑_移除标签: 'Remove tag', + 批量编辑_清除全部标签: 'Clear all tags', + 批量编辑_设置延迟: 'Set delay', + 批量编辑_选择插件: 'Select {name}', + 批量编辑_清除全部标签确认: 'Clear all tags from {count} plugins? Built-in BPM tags will be kept.', + 批量编辑_启用确认: 'Enable {count} plugins?', + 批量编辑_禁用确认: 'Disable {count} plugins?', + 批量编辑_无可操作插件: 'No actionable plugins', + 批量编辑_已更新分组: 'Updated group for {count} plugins', + 批量编辑_已添加标签: 'Added tag to {count} plugins', + 批量编辑_已移除标签: 'Updated tags for {count} plugins', + 批量编辑_已更新延迟: 'Updated delay for {count} plugins', + 批量编辑_已更新状态: 'Updated status for {count} plugins', 更新_未获取到远端版本: 'Remote version not found', 更新_无来源无法检测: 'No source available, cannot check', 开发_刷新插件_提示: 'Refresh plugin', @@ -570,6 +599,7 @@ export default { 分组编辑_分组名称_标签: 'Group name', 分组编辑_创建分组: 'Create group', 分组编辑_创建全局分组: 'Create a new global group.', + 分组编辑_打开切换: 'Change group for {name}', 标签编辑_标题: 'Edit Tags', 标签编辑_说明: 'Assign tags to the current plugin, or maintain tag names and colors directly.', 标签编辑_分配标题: 'Tag assignment', diff --git a/src/lang/locale/zh_cn.ts b/src/lang/locale/zh_cn.ts index 8f1ee05..17dce0f 100644 --- a/src/lang/locale/zh_cn.ts +++ b/src/lang/locale/zh_cn.ts @@ -6,6 +6,8 @@ export default { 通用_操作_文本: '操作', 通用_搜索_文本: '搜索', 通用_名称_文本: '名称', + 通用_ID_文本: 'ID', + 通用_颜色_文本: '颜色', 通用_无分组_文本: '全部', 通用_无标签_文本: '全部', 通用_无延迟_文本: '无', @@ -149,7 +151,13 @@ export default { 管理器_选择版本_获取失败提示: '获取版本列表失败,请稍后再试', 管理器_选择版本_无版本提示: '未获取到版本列表,将尝试使用检测到的版本或最新版本。', 管理器_选择版本_下载按钮: '下载更新', + 管理器_选择版本_切换按钮: '切换到所选版本', 管理器_选择版本_成功提示: '已下载并更新插件', + 管理器_选择版本_说明: '选择要安装的 GitHub Release 版本。', + 管理器_选择版本_列表标题: '可用版本', + 管理器_选择版本_当前版本: '当前安装:{version}', + 管理器_选择版本_版本数量: '{count} 个版本', + 管理器_选择版本_选择版本: '选择版本 {version}', 通知_ID已复制: 'ID已复制', @@ -471,6 +479,27 @@ export default { 管理器_应用更改中_提示: '正在应用更改,请勿频繁操作。', 管理器_暂无匹配插件: '暂无匹配插件', 管理器_安装_操作_安装中: '安装中...', + 批量编辑_入口: '批量编辑', + 批量编辑_标题: '批量编辑', + 批量编辑_已选择数量: '已选择 {count}/{total}', + 批量编辑_全选当前列表: '选择当前列表', + 批量编辑_清空选择: '清空选择', + 批量编辑_设置分组: '设置分组', + 批量编辑_清除分组: '清除分组', + 批量编辑_添加标签: '添加标签', + 批量编辑_移除标签: '移除标签', + 批量编辑_清除全部标签: '清除全部标签', + 批量编辑_设置延迟: '设置延迟', + 批量编辑_选择插件: '选择 {name}', + 批量编辑_清除全部标签确认: '确定清除 {count} 个插件的全部标签吗?BPM 内置标签会保留。', + 批量编辑_启用确认: '确定启用 {count} 个插件吗?', + 批量编辑_禁用确认: '确定禁用 {count} 个插件吗?', + 批量编辑_无可操作插件: '没有可操作的插件', + 批量编辑_已更新分组: '已更新 {count} 个插件的分组', + 批量编辑_已添加标签: '已为 {count} 个插件添加标签', + 批量编辑_已移除标签: '已更新 {count} 个插件的标签', + 批量编辑_已更新延迟: '已更新 {count} 个插件的延迟', + 批量编辑_已更新状态: '已更新 {count} 个插件的状态', 更新_未获取到远端版本: '未获取到远端版本', 更新_无来源无法检测: '无来源,无法检测', 开发_刷新插件_提示: '刷新插件', @@ -572,6 +601,7 @@ export default { 分组编辑_分组名称_标签: '分组名称', 分组编辑_创建分组: '创建分组', 分组编辑_创建全局分组: '创建新的全局分组。', + 分组编辑_打开切换: '切换 {name} 的分组', 标签编辑_标题: '编辑标签', 标签编辑_说明: '为当前插件分配标签,或直接维护标签名称与颜色。', 标签编辑_分配标题: '标签分配', diff --git a/src/modal/group-modal.ts b/src/modal/group-modal.ts index 02502d7..179a1c1 100644 --- a/src/modal/group-modal.ts +++ b/src/modal/group-modal.ts @@ -83,8 +83,15 @@ export class GroupModal extends Modal { summaryMain.createDiv({ cls: 'manager-tag-editor__summary-title', text: t('分组编辑_选择标题') }); summaryMain.createDiv({ cls: 'manager-tag-editor__summary-desc', text: t('分组编辑_选择说明', { id: this.managerPlugin.id, selected: selectedCount, total: this.settings.GROUPS.length }) }); const summaryStats = summary.createDiv('manager-tag-editor__summary-stats'); - summaryStats.createSpan({ cls: 'manager-tag-editor__summary-stat', text: t('设置_分类_统计', { label: t('通用_全部_文本'), count: this.settings.GROUPS.length }) }); - summaryStats.createSpan({ cls: 'manager-tag-editor__summary-stat', text: t('设置_分类_统计', { label: t('通用_已选择_文本'), count: selectedCount }) }); + const createSummaryStat = (label: string, value: number, icon: string) => { + const stat = summaryStats.createSpan({ cls: 'manager-tag-editor__summary-stat' }); + const statIcon = stat.createSpan({ cls: 'manager-tag-editor__summary-stat-icon' }); + setIcon(statIcon, icon); + stat.createSpan({ cls: 'manager-tag-editor__summary-stat-label', text: label }); + stat.createSpan({ cls: 'manager-tag-editor__summary-stat-value', text: `${value}` }); + }; + createSummaryStat(t('通用_全部_文本'), this.settings.GROUPS.length, 'folders'); + createSummaryStat(t('通用_已选择_文本'), selectedCount, 'check'); const list = page.createDiv('manager-tag-editor__list'); for (const group of this.settings.GROUPS) { @@ -103,7 +110,8 @@ export class GroupModal extends Modal { const previewLine = itemEl.nameEl.createDiv('manager-tag-editor__preview-line'); const tag = this.manager.createTag(group.name || group.id, group.color, this.settings.GROUP_STYLE); tag.addClass('manager-tag-editor__chip'); - previewLine.appendChild(tag); + const chipSlot = previewLine.createSpan({ cls: 'manager-tag-editor__chip-slot' }); + chipSlot.appendChild(tag); previewLine.createSpan({ cls: 'manager-tag-editor__id', text: group.id }); if (selected) previewLine.createSpan({ cls: 'manager-tag-editor__badge is-assigned', text: t('通用_已选择_文本') }); @@ -181,54 +189,103 @@ export class GroupModal extends Modal { } } if (this.add) { - let id = ''; - let name = ''; let color = this.pickDistinctColor(this.settings.GROUPS.map(g => g.color)); - const foodBar = new Setting(page).setClass('manager-bar__title'); - foodBar.settingEl.addClass('manager-tag-editor__add-panel'); - foodBar.nameEl.empty(); - foodBar.descEl.empty(); - const addTitle = foodBar.nameEl.createDiv('manager-tag-editor__add-title'); - const addIcon = addTitle.createSpan({ cls: 'manager-tag-editor__add-title-icon' }); - setIcon(addIcon, 'plus'); - addTitle.createSpan({ text: t('设置_分组设置_新增分组') }); - foodBar.descEl.setText(t('设置_分类_ID说明')); - foodBar.addColorPicker(cb => cb - .setValue(color) - .onChange((value) => { - color = value; - }) - ) - foodBar.addText(cb => cb - .setPlaceholder('ID') - .onChange((value) => { id = value; }) - .inputEl.addClass('manager-editor__item-input') - ) - foodBar.addText(cb => cb - .setPlaceholder(this.manager.translator.t('通用_名称_文本')) - .onChange((value) => { name = value; }) - .inputEl.addClass('manager-editor__item-input') - ) - foodBar.addExtraButton((cb) => { - cb.setIcon('plus'); - this.prepareIconButton(cb, t('分组编辑_创建分组'), 'manager-tag-editor__save-button'); - cb.onClick(() => { - const nextId = id.trim(); - const nextName = name.trim() || nextId; - const containsId = this.manager.settings.GROUPS.some(tag => tag.id === nextId); - if (!containsId && nextId !== '') { - if (color === '') color = this.pickDistinctColor(this.settings.GROUPS.map(g => g.color)); - this.manager.settings.GROUPS.push({ id: nextId, name: nextName, color }); - this.manager.saveSettings(); - this.add = false; - this.reloadShowData(); - Commands(this.app, this.manager); - new Notice(this.manager.translator.t('设置_分组设置_通知_一')); - } else { - new Notice(this.manager.translator.t('设置_分组设置_通知_二')); - } - }); - }) + const addPanel = page.createDiv('manager-tag-editor__create-panel'); + const preview = addPanel.createDiv('manager-tag-editor__create-preview'); + const previewMain = preview.createDiv('manager-tag-editor__create-preview-main'); + const previewChip = this.manager.createTag(t('通用_名称_文本'), color, this.settings.GROUP_STYLE); + previewChip.addClass('manager-tag-editor__chip'); + previewChip.addClass('manager-tag-editor__create-chip'); + const previewChipSlot = previewMain.createSpan({ cls: 'manager-tag-editor__chip-slot manager-tag-editor__chip-slot--preview' }); + previewChipSlot.appendChild(previewChip); + const previewText = previewMain.createDiv('manager-tag-editor__create-preview-text'); + previewText.createDiv({ cls: 'manager-tag-editor__create-title', text: t('设置_分组设置_新增分组') }); + const previewId = previewText.createDiv({ cls: 'manager-tag-editor__create-id', text: 'ID' }); + preview.createDiv({ cls: 'manager-tag-editor__create-desc', text: t('设置_分类_新增描述') }); + + const form = addPanel.createDiv('manager-tag-editor__create-form'); + const createField = (label: string, className = '') => { + const field = form.createDiv(`manager-tag-editor__create-field ${className}`); + field.createDiv({ cls: 'manager-tag-editor__create-label', text: label }); + return field; + }; + const colorField = createField(t('通用_颜色_文本'), 'manager-tag-editor__create-field--color'); + const colorInput = colorField.createEl('input'); + colorInput.type = 'color'; + colorInput.value = color; + colorInput.addClass('manager-tag-editor__create-color'); + colorInput.setAttribute('aria-label', t('通用_颜色_文本')); + + const idField = createField(t('通用_ID_文本')); + const idInput = idField.createEl('input'); + idInput.type = 'text'; + idInput.placeholder = 'group-id'; + idInput.spellcheck = false; + idInput.addClass('manager-editor__item-input'); + idInput.addClass('manager-tag-editor__create-input'); + idInput.setAttribute('aria-label', t('通用_ID_文本')); + + const nameField = createField(t('通用_名称_文本')); + const nameInput = nameField.createEl('input'); + nameInput.type = 'text'; + nameInput.placeholder = t('通用_名称_文本'); + nameInput.addClass('manager-editor__item-input'); + nameInput.addClass('manager-tag-editor__create-input'); + nameInput.setAttribute('aria-label', t('通用_名称_文本')); + + const updatePreview = () => { + const nextId = idInput.value.trim(); + const nextName = nameInput.value.trim(); + previewChip.textContent = nextName || nextId || t('通用_名称_文本'); + previewChip.setAttribute('style', this.manager.generateTagStyle(color, this.settings.GROUP_STYLE)); + previewId.setText(nextId || 'ID'); + }; + const submit = () => { + const nextId = idInput.value.trim(); + const nextName = nameInput.value.trim() || nextId; + const containsId = this.manager.settings.GROUPS.some(tag => tag.id === nextId); + if (!containsId && nextId !== '') { + this.manager.settings.GROUPS.push({ id: nextId, name: nextName, color }); + this.manager.saveSettings(); + this.add = false; + this.reloadShowData(); + Commands(this.app, this.manager); + new Notice(this.manager.translator.t('设置_分组设置_通知_一')); + } else { + new Notice(this.manager.translator.t('设置_分组设置_通知_二')); + } + }; + const onEnter = (event: KeyboardEvent) => { + if (event.key !== 'Enter') return; + event.preventDefault(); + submit(); + }; + colorInput.addEventListener('input', () => { + color = colorInput.value; + updatePreview(); + }); + idInput.addEventListener('input', () => updatePreview()); + nameInput.addEventListener('input', () => updatePreview()); + idInput.addEventListener('keydown', onEnter); + nameInput.addEventListener('keydown', onEnter); + + const actions = addPanel.createDiv('manager-tag-editor__create-actions'); + const cancelButton = actions.createEl('button', { cls: 'manager-tag-editor__create-action' }); + cancelButton.type = 'button'; + cancelButton.setAttribute('aria-label', t('通用_取消_文本')); + setIcon(cancelButton.createSpan({ cls: 'manager-tag-editor__create-action-icon' }), 'x'); + cancelButton.createSpan({ text: t('通用_取消_文本') }); + cancelButton.addEventListener('click', () => { + this.add = false; + this.reloadShowData(); + }); + const submitButton = actions.createEl('button', { cls: 'manager-tag-editor__create-action manager-tag-editor__create-action--primary' }); + submitButton.type = 'button'; + submitButton.setAttribute('aria-label', t('分组编辑_创建分组')); + setIcon(submitButton.createSpan({ cls: 'manager-tag-editor__create-action-icon' }), 'plus'); + submitButton.createSpan({ text: t('分组编辑_创建分组') }); + submitButton.addEventListener('click', submit); + window.setTimeout(() => idInput.focus(), 0); } else { // [底部行] 新增 const foodBar = new Setting(page).setClass('manager-bar__title').setName(this.manager.translator.t('通用_新增_文本')); diff --git a/src/modal/manager-modal.ts b/src/modal/manager-modal.ts index c8b738e..9c8102d 100644 --- a/src/modal/manager-modal.ts +++ b/src/modal/manager-modal.ts @@ -138,6 +138,8 @@ export class ManagerModal extends Modal { groupDropdown?: DropdownComponent; tagDropdown?: DropdownComponent; delayDropdown?: DropdownComponent; + private bulkEditMode = false; + private bulkSelectedPluginIds = new Set(); actionCollapsed = false; filterCollapsed = false; private reloadingManifests = false; @@ -496,12 +498,50 @@ export class ManagerModal extends Modal { return ((button as any).extraSettingsEl || (button as any).buttonEl) as HTMLElement | undefined; } + private async openPluginVersionList(pluginId: string, updateInfo?: PluginUpdateViewStatus | null) { + const t = (k: any, vars?: Record) => this.manager.translator.t(k, vars); + const progress = this.showInlineProgress(t("通知_获取版本中文案"), pluginId); + progress.update(0, 1, pluginId); + try { + let status = updateInfo ?? (this.manager.updateStatus?.[pluginId] as PluginUpdateViewStatus | undefined); + let repo = status?.repo || this.manager.settings.REPO_MAP?.[pluginId] || null; + if (!repo) { + try { + repo = await this.manager.repoResolver.resolveRepo(pluginId); + } catch { + repo = null; + } + } + if (!repo) { + new Notice(t("下载更新_缺少仓库提示")); + return; + } + + const versions = await fetchReleaseVersions(this.manager, repo); + if (!status) status = {}; + status.repo = repo; + status.versions = versions; + const stableVersion = versions.find((release) => !release.prerelease)?.version; + status.remoteVersion = stableVersion || versions[0]?.version || status.remoteVersion || null; + this.manager.updateStatus[pluginId] = { + ...(this.manager.updateStatus?.[pluginId] ?? {}), + ...status, + } as any; + + progress.update(1, 1, pluginId); + this.refreshSinglePluginUpdateUi(pluginId); + this.updateStats(); + new UpdateModal(this.app, this.manager, pluginId, versions, status.remoteVersion ?? null, repo).open(); + } catch (e) { + console.error("[BPM] fetch remote versions failed", e); + new Notice(t("管理器_选择版本_获取失败提示"), 4000); + } finally { + progress.hide(); + } + } + private openPluginUpdateModal(pluginId: string, updateInfo: PluginUpdateViewStatus) { - if (!updateInfo.remoteVersion) return; - const versions = updateInfo.versions && updateInfo.versions.length > 0 - ? updateInfo.versions - : [{ version: updateInfo.remoteVersion, prerelease: false }]; - new UpdateModal(this.app, this.manager, pluginId, versions, updateInfo.remoteVersion, updateInfo.repo || undefined).open(); + void this.openPluginVersionList(pluginId, updateInfo); } private addPluginDownloadButton(controlEl: HTMLElement, pluginId: string, updateInfo: PluginUpdateViewStatus, prepend = false) { @@ -723,6 +763,265 @@ export class ManagerModal extends Modal { await this.reloadShowData(); } + private getSelectedManagerPlugins(): ManagerPlugin[] { + return this.settings.Plugins.filter((plugin) => this.bulkSelectedPluginIds.has(plugin.id)); + } + + private getSelectableDisplayedPluginIds(): string[] { + return this.displayPlugins + .map((plugin) => plugin.id) + .filter((id) => id !== this.manager.manifest.id); + } + + private getBulkSelectedCount(): number { + return this.getSelectedManagerPlugins().length; + } + + private cleanupBulkSelection() { + const validIds = new Set(this.settings.Plugins.map((plugin) => plugin.id)); + for (const id of Array.from(this.bulkSelectedPluginIds)) { + if (!validIds.has(id) || id === this.manager.manifest.id) this.bulkSelectedPluginIds.delete(id); + } + } + + private setBulkEditMode(value: boolean) { + if (value && this.activePage !== "plugins") return; + this.bulkEditMode = value; + if (value) this.editorMode = false; + if (!value) this.bulkSelectedPluginIds.clear(); + this.applyEditingStyle(); + this.renderContent(); + if (Platform.isMobileApp) this.showHeadMobile(); + } + + private toggleBulkPluginSelection(pluginId: string, selected: boolean) { + if (pluginId === this.manager.manifest.id) return; + if (selected) this.bulkSelectedPluginIds.add(pluginId); + else this.bulkSelectedPluginIds.delete(pluginId); + this.reloadShowData(); + } + + private selectDisplayedPlugins() { + this.bulkSelectedPluginIds = new Set(this.getSelectableDisplayedPluginIds()); + this.reloadShowData(); + } + + private clearBulkSelection() { + this.bulkSelectedPluginIds.clear(); + this.reloadShowData(); + } + + private createBulkActionButton(container: HTMLElement, icon: string, label: string, onClick: (event: MouseEvent) => void, disabled = false) { + const button = container.createEl("button", { cls: "manager-bulk-bar__button" }); + button.type = "button"; + button.disabled = disabled; + button.setAttribute("aria-label", label); + button.setAttribute("title", label); + const iconEl = button.createSpan({ cls: "manager-bulk-bar__button-icon" }); + iconEl.setAttribute("aria-hidden", "true"); + setIcon(iconEl, icon); + button.createSpan({ cls: "manager-bulk-bar__button-label", text: label }); + button.addEventListener("click", onClick); + this.bindLongPressTooltip(button, label); + return button; + } + + private showBulkGroupMenu(event: MouseEvent) { + const t = (k: any, vars?: Record) => this.manager.translator.t(k, vars); + const menu = new Menu(); + menu.addItem((item) => item + .setTitle(t("批量编辑_清除分组")) + .setIcon("folder-x") + .onClick(() => { void this.applyBulkGroup(""); })); + if (this.settings.GROUPS.length > 0) menu.addSeparator(); + for (const group of this.settings.GROUPS) { + menu.addItem((item) => item + .setTitle(group.name || group.id) + .setIcon("folder-tree") + .onClick(() => { void this.applyBulkGroup(group.id); })); + } + menu.showAtMouseEvent(event); + } + + private showBulkTagMenu(event: MouseEvent, mode: "add" | "remove") { + const t = (k: any, vars?: Record) => this.manager.translator.t(k, vars); + const menu = new Menu(); + const tags = this.settings.TAGS.filter((tag) => tag.id !== BPM_TAG_ID); + if (mode === "remove") { + menu.addItem((item) => item + .setTitle(t("批量编辑_清除全部标签")) + .setIcon("tags") + .onClick(() => { void this.applyBulkClearTags(); })); + if (tags.length > 0) menu.addSeparator(); + } + for (const tag of tags) { + menu.addItem((item) => item + .setTitle(tag.name || tag.id) + .setIcon(mode === "add" ? "tag" : "tag-x") + .onClick(() => { + if (mode === "add") void this.applyBulkAddTag(tag.id); + else void this.applyBulkRemoveTag(tag.id); + })); + } + menu.showAtMouseEvent(event); + } + + private showBulkDelayMenu(event: MouseEvent) { + const t = (k: any, vars?: Record) => this.manager.translator.t(k, vars); + const menu = new Menu(); + menu.addItem((item) => item + .setTitle(t("通用_无延迟_文本")) + .setIcon("timer-off") + .onClick(() => { void this.applyBulkDelay(""); })); + if (this.settings.DELAYS.length > 0) menu.addSeparator(); + for (const delay of this.settings.DELAYS) { + menu.addItem((item) => item + .setTitle(`${delay.name || delay.id} (${delay.time}s)`) + .setIcon("timer") + .onClick(() => { void this.applyBulkDelay(delay.id); })); + } + menu.showAtMouseEvent(event); + } + + private showBulkMoreMenu(event: MouseEvent) { + const t = (k: any, vars?: Record) => this.manager.translator.t(k, vars); + const selectedCount = this.getBulkSelectedCount(); + const disabled = selectedCount === 0; + const menu = new Menu(); + menu.addItem((item) => item + .setTitle(t("通用_启用_文本")) + .setIcon("power") + .setDisabled(disabled) + .onClick(() => { void this.applyBulkEnabled(true); })); + menu.addItem((item) => item + .setTitle(t("通用_禁用_文本")) + .setIcon("power-off") + .setDisabled(disabled) + .onClick(() => { void this.applyBulkEnabled(false); })); + menu.addSeparator(); + menu.addItem((item) => item + .setTitle(t("批量编辑_全选当前列表")) + .setIcon("list-checks") + .onClick(() => this.selectDisplayedPlugins())); + menu.addItem((item) => item + .setTitle(t("批量编辑_清空选择")) + .setIcon("x") + .setDisabled(this.bulkSelectedPluginIds.size === 0) + .onClick(() => this.clearBulkSelection())); + menu.showAtMouseEvent(event); + } + + private renderBulkBar(container: HTMLElement) { + if (!this.bulkEditMode) return; + this.cleanupBulkSelection(); + const t = (k: any, vars?: Record) => this.manager.translator.t(k, vars); + const selectedCount = this.getBulkSelectedCount(); + const displayedCount = this.getSelectableDisplayedPluginIds().length; + const disabled = selectedCount === 0; + const bar = container.createDiv("manager-bulk-bar"); + const summary = bar.createDiv("manager-bulk-bar__summary"); + const summaryIcon = summary.createSpan({ cls: "manager-bulk-bar__summary-icon" }); + setIcon(summaryIcon, "square-check-big"); + const text = summary.createDiv("manager-bulk-bar__summary-text"); + text.createSpan({ cls: "manager-bulk-bar__title", text: t("批量编辑_标题") }); + text.createSpan({ cls: "manager-bulk-bar__count", text: t("批量编辑_已选择数量", { count: selectedCount, total: displayedCount }) }); + + const actions = bar.createDiv("manager-bulk-bar__actions"); + this.createBulkActionButton(actions, "list-checks", t("批量编辑_全选当前列表"), () => this.selectDisplayedPlugins(), displayedCount === 0); + this.createBulkActionButton(actions, "folder-tree", t("批量编辑_设置分组"), (event) => this.showBulkGroupMenu(event), disabled); + this.createBulkActionButton(actions, "tag", t("批量编辑_添加标签"), (event) => this.showBulkTagMenu(event, "add"), disabled); + this.createBulkActionButton(actions, "tag-x", t("批量编辑_移除标签"), (event) => this.showBulkTagMenu(event, "remove"), disabled); + if (this.settings.DELAY) this.createBulkActionButton(actions, "timer", t("批量编辑_设置延迟"), (event) => this.showBulkDelayMenu(event), disabled); + this.createBulkActionButton(actions, "more-horizontal", t("管理器_更多操作_描述"), (event) => this.showBulkMoreMenu(event)); + this.createBulkActionButton(actions, "x", t("通用_完成_文本"), () => this.setBulkEditMode(false)); + } + + private async applyBulkGroup(groupId: string) { + const plugins = this.getSelectedManagerPlugins(); + plugins.forEach((plugin) => { plugin.group = groupId; }); + await this.finishBulkMetadataEdit("批量编辑_已更新分组", plugins.length); + } + + private async applyBulkAddTag(tagId: string) { + const plugins = this.getSelectedManagerPlugins(); + plugins.forEach((plugin) => { + if (!plugin.tags.includes(tagId)) plugin.tags.push(tagId); + }); + await this.finishBulkMetadataEdit("批量编辑_已添加标签", plugins.length); + } + + private async applyBulkRemoveTag(tagId: string) { + const plugins = this.getSelectedManagerPlugins(); + plugins.forEach((plugin) => { + plugin.tags = plugin.tags.filter((id) => id !== tagId); + }); + await this.finishBulkMetadataEdit("批量编辑_已移除标签", plugins.length); + } + + private async applyBulkClearTags() { + const t = (k: any, vars?: Record) => this.manager.translator.t(k, vars); + const plugins = this.getSelectedManagerPlugins(); + if (plugins.length === 0) return; + if (!window.confirm(t("批量编辑_清除全部标签确认", { count: plugins.length }))) return; + plugins.forEach((plugin) => { + plugin.tags = plugin.tags.filter((id) => id === BPM_TAG_ID); + }); + await this.finishBulkMetadataEdit("批量编辑_已移除标签", plugins.length); + } + + private async applyBulkDelay(delayId: string) { + const plugins = this.getSelectedManagerPlugins().filter((plugin) => !plugin.tags.includes(BPM_IGNORE_TAG)); + if (plugins.length === 0) { + new Notice(this.manager.translator.t("批量编辑_无可操作插件")); + return; + } + plugins.forEach((plugin) => { + plugin.delay = delayId; + }); + await this.finishBulkMetadataEdit("批量编辑_已更新延迟", plugins.length); + } + + private async finishBulkMetadataEdit(messageKey: string, count: number) { + if (count === 0) return; + await this.manager.saveSettings(); + Commands(this.app, this.manager); + await this.refreshFilterOptions(true); + new Notice(this.manager.translator.t(messageKey, { count })); + } + + private async applyBulkEnabled(targetEnabled: boolean) { + const t = (k: any, vars?: Record) => this.manager.translator.t(k, vars); + const plugins = this.getSelectedManagerPlugins() + .filter((plugin) => plugin.id !== this.manager.manifest.id && !plugin.tags.includes(BPM_IGNORE_TAG)); + if (plugins.length === 0) { + new Notice(t("批量编辑_无可操作插件")); + return; + } + if (!window.confirm(t(targetEnabled ? "批量编辑_启用确认" : "批量编辑_禁用确认", { count: plugins.length }))) return; + const progress = this.showInlineProgress(t("管理器_应用更改中_提示")); + let processed = 0; + for (const plugin of plugins) { + const isEnabled = this.isManagedPluginEnabled(plugin.id); + if (isEnabled !== targetEnabled) { + plugin.enabled = targetEnabled; + if (this.settings.DELAY) { + if (targetEnabled) await this.appPlugins.enablePlugin(plugin.id); + else await this.appPlugins.disablePlugin(plugin.id); + } else { + if (targetEnabled) await this.appPlugins.enablePluginAndSave(plugin.id); + else await this.appPlugins.disablePluginAndSave(plugin.id); + } + } + processed++; + progress.update(processed, plugins.length, plugin.id); + } + progress.hide(); + await this.manager.saveSettings(); + Commands(this.app, this.manager); + await this.reloadShowData(); + new Notice(t("批量编辑_已更新状态", { count: plugins.length })); + } + private runDisplayedPluginsToggle() { const targetEnabled = this.getBulkToggleTarget(); new DisableModal(this.app, this.manager, async () => { @@ -752,6 +1051,7 @@ export class ManagerModal extends Modal { modalContainer?: HTMLElement; private desktopActionWrapper?: HTMLElement; private desktopFilterWrapper?: HTMLElement; + private bulkEditButtonEl?: HTMLButtonElement; private pluginTabEl?: HTMLButtonElement; private installTabEl?: HTMLButtonElement; private sourcesTabEl?: HTMLButtonElement; @@ -936,19 +1236,32 @@ export class ManagerModal extends Modal { const tools = toolbar.createDiv("manager-toolbar__tools"); const actionBar = new Setting(tools).setClass("manager-bar__action").setName(""); - const markTool = (btn: ButtonComponent, scope: "plugin" | "install" | "global" | "ribbon" | "layout" | "transfer" | "resource") => { + const markTool = (btn: ButtonComponent, scope: "plugin" | "install" | "global" | "ribbon" | "layout" | "transfer" | "resource", order?: number) => { btn.buttonEl.addClass("manager-tool"); btn.buttonEl.addClass(`manager-tool--${scope}`); + if (order !== undefined) btn.buttonEl.style.setProperty("--manager-tool-order", `${order}`); }; + const bulkEditButton = new ButtonComponent(actionBar.controlEl); + markTool(bulkEditButton, "plugin", 20); + bulkEditButton.setIcon(this.bulkEditMode ? "square-check-big" : "list-plus"); + bulkEditButton.setTooltip(t("批量编辑_入口")); + bulkEditButton.buttonEl.setAttribute("aria-label", t("批量编辑_入口")); + bulkEditButton.buttonEl.classList.toggle("is-active", this.bulkEditMode); + this.bulkEditButtonEl = bulkEditButton.buttonEl; + this.bindLongPressTooltip(bulkEditButton.buttonEl, t("批量编辑_入口")); + bulkEditButton.onClick(() => { + this.setBulkEditMode(!this.bulkEditMode); + }); + // [操作行] 检查更新 const updateButton = new ButtonComponent(actionBar.controlEl); - markTool(updateButton, "plugin"); + markTool(updateButton, "plugin", 10); this.preparePluginUpdateButton(updateButton); // [操作行] 全选/全部取消当前列表 const toggleAllButton = new ButtonComponent(actionBar.controlEl); - markTool(toggleAllButton, "plugin"); + markTool(toggleAllButton, "plugin", 30); toggleAllButton.setIcon("list-checks"); toggleAllButton.setTooltip(this.manager.translator.t("管理器_全选取消_描述")); this.bindLongPressTooltip(toggleAllButton.buttonEl, this.manager.translator.t("管理器_全选取消_描述")); @@ -958,7 +1271,7 @@ export class ManagerModal extends Modal { // [操作行] 重载插件 const reloadButton = new ButtonComponent(actionBar.controlEl); - markTool(reloadButton, "plugin"); + markTool(reloadButton, "plugin", 40); reloadButton.setIcon("refresh-ccw"); reloadButton.setTooltip(this.manager.translator.t("管理器_重载插件_描述")); this.bindLongPressTooltip(reloadButton.buttonEl, this.manager.translator.t("管理器_重载插件_描述")); @@ -990,12 +1303,16 @@ export class ManagerModal extends Modal { // [操作行] 编辑模式 const editorButton = new ButtonComponent(actionBar.controlEl); - markTool(editorButton, "plugin"); + markTool(editorButton, "plugin", 50); this.editorMode ? editorButton.setIcon("pen-off") : editorButton.setIcon("pen"); editorButton.setTooltip(this.manager.translator.t("管理器_编辑模式_描述")); this.bindLongPressTooltip(editorButton.buttonEl, this.manager.translator.t("管理器_编辑模式_描述")); editorButton.onClick(async () => { this.editorMode = !this.editorMode; + if (this.editorMode && this.bulkEditMode) { + this.bulkEditMode = false; + this.bulkSelectedPluginIds.clear(); + } this.editorMode ? editorButton.setIcon("pen-off") : editorButton.setIcon("pen"); this.applyEditingStyle(); if (!this.editorMode) { @@ -1006,7 +1323,7 @@ export class ManagerModal extends Modal { }); const ribbonResetButton = new ButtonComponent(actionBar.controlEl); - markTool(ribbonResetButton, "ribbon"); + markTool(ribbonResetButton, "ribbon", 10); ribbonResetButton.setIcon("rotate-ccw"); ribbonResetButton.setTooltip(t("Ribbon_重置_提示")); ribbonResetButton.buttonEl.setAttribute("aria-label", t("Ribbon_重置_提示")); @@ -1020,7 +1337,7 @@ export class ManagerModal extends Modal { }); const addSeparatorButton = new ButtonComponent(actionBar.controlEl); - markTool(addSeparatorButton, "layout"); + markTool(addSeparatorButton, "layout", 60); addSeparatorButton.setIcon("separator-horizontal"); addSeparatorButton.setTooltip(t("管理器_布局_添加分割线")); addSeparatorButton.buttonEl.setAttribute("aria-label", t("管理器_布局_添加分割线")); @@ -1030,7 +1347,7 @@ export class ManagerModal extends Modal { }); const hiddenResetButton = new ButtonComponent(actionBar.controlEl); - markTool(hiddenResetButton, "layout"); + markTool(hiddenResetButton, "layout", 61); hiddenResetButton.setIcon("rotate-ccw"); hiddenResetButton.setTooltip(t("管理器_布局_按名称重置")); hiddenResetButton.buttonEl.setAttribute("aria-label", t("管理器_布局_按名称重置")); @@ -1041,21 +1358,21 @@ export class ManagerModal extends Modal { }); const githubButton = new ButtonComponent(actionBar.controlEl); - markTool(githubButton, "resource"); + markTool(githubButton, "resource", 120); githubButton.setIcon("github"); githubButton.setTooltip(this.manager.translator.t("管理器_GITHUB_描述")); this.bindLongPressTooltip(githubButton.buttonEl, this.manager.translator.t("管理器_GITHUB_描述")); githubButton.onClick(() => { window.open("https://github.com/zenozero-dev/obsidian-manager"); }); const tutorialButton = new ButtonComponent(actionBar.controlEl); - markTool(tutorialButton, "resource"); + markTool(tutorialButton, "resource", 110); tutorialButton.setIcon("book-open"); tutorialButton.setTooltip(this.manager.translator.t("管理器_视频教程_描述")); this.bindLongPressTooltip(tutorialButton.buttonEl, this.manager.translator.t("管理器_视频教程_描述")); tutorialButton.onClick(() => { window.open("https://www.bilibili.com/video/BV1WyrkYMEce/"); }); const supportGroupButton = new ButtonComponent(actionBar.controlEl); - markTool(supportGroupButton, "resource"); + markTool(supportGroupButton, "resource", 130); supportGroupButton.setIcon("message-circle"); supportGroupButton.setTooltip(SUPPORT_QQ_GROUP_TOOLTIP); this.bindLongPressTooltip(supportGroupButton.buttonEl, SUPPORT_QQ_GROUP_TOOLTIP); @@ -1063,7 +1380,7 @@ export class ManagerModal extends Modal { // [操作行] 插件市场 const marketButton = new ButtonComponent(actionBar.controlEl); - markTool(marketButton, "global"); + markTool(marketButton, "global", 70); marketButton.setIcon("store"); marketButton.setTooltip(this.manager.translator.t("管理器_插件市场_描述")); this.bindLongPressTooltip(marketButton.buttonEl, this.manager.translator.t("管理器_插件市场_描述")); @@ -1073,7 +1390,7 @@ export class ManagerModal extends Modal { // [操作行] 插件设置 const settingsButton = new ButtonComponent(actionBar.controlEl); - markTool(settingsButton, "global"); + markTool(settingsButton, "global", 80); settingsButton.setIcon("settings"); settingsButton.setTooltip(this.manager.translator.t("管理器_插件设置_描述")); this.bindLongPressTooltip(settingsButton.buttonEl, this.manager.translator.t("管理器_插件设置_描述")); @@ -1087,7 +1404,7 @@ export class ManagerModal extends Modal { // [测试行] 刷新插件 if (this.developerMode) { const testButton = new ButtonComponent(actionBar.controlEl); - markTool(testButton, "plugin"); + markTool(testButton, "plugin", 90); testButton.setIcon("refresh-ccw"); testButton.setTooltip(t("开发_刷新插件_提示")); testButton.onClick(async () => { @@ -1100,7 +1417,7 @@ export class ManagerModal extends Modal { // [测试行] 测试插件 if (this.developerMode) { const testButton = new ButtonComponent(actionBar.controlEl); - markTool(testButton, "plugin"); + markTool(testButton, "plugin", 91); testButton.setIcon("test-tube"); testButton.setTooltip(t("开发_测试插件_提示")); testButton.onClick(async () => { @@ -1246,6 +1563,10 @@ export class ManagerModal extends Modal { const topActions = topRow.createDiv("bpm-mobile-header__actions"); + // 检查更新按钮 + const updateBtn = new ButtonComponent(topActions); + this.preparePluginUpdateButton(updateBtn); + // 编辑模式 const editorBtn = new ButtonComponent(topActions); editorBtn.setIcon(this.editorMode ? "pen-off" : "pen"); @@ -1253,6 +1574,10 @@ export class ManagerModal extends Modal { this.bindLongPressTooltip(editorBtn.buttonEl, t("管理器_编辑模式_描述")); editorBtn.onClick(async () => { this.editorMode = !this.editorMode; + if (this.editorMode && this.bulkEditMode) { + this.bulkEditMode = false; + this.bulkSelectedPluginIds.clear(); + } this.applyEditingStyle(); if (!this.editorMode) { await this.refreshFilterOptions(true); @@ -1262,6 +1587,15 @@ export class ManagerModal extends Modal { this.showHeadMobile(); }); + const bulkBtn = new ButtonComponent(topActions); + bulkBtn.setIcon(this.bulkEditMode ? "square-check-big" : "list-plus"); + bulkBtn.setTooltip(t("批量编辑_入口")); + bulkBtn.buttonEl.toggleClass("is-active", this.bulkEditMode); + this.bindLongPressTooltip(bulkBtn.buttonEl, t("批量编辑_入口")); + bulkBtn.onClick(() => { + this.setBulkEditMode(!this.bulkEditMode); + }); + // 安装/返回 const installBtn = new ButtonComponent(topActions); installBtn.setIcon(this.installMode ? "arrow-left" : "download"); @@ -1275,10 +1609,6 @@ export class ManagerModal extends Modal { this.showHeadMobile(); }); - // 检查更新按钮 - const updateBtn = new ButtonComponent(topActions); - this.preparePluginUpdateButton(updateBtn); - // 更多操作菜单 const moreBtn = new ButtonComponent(topActions); moreBtn.setIcon("more-vertical"); @@ -1286,6 +1616,9 @@ export class ManagerModal extends Modal { this.bindLongPressTooltip(moreBtn.buttonEl, t("管理器_更多操作_描述")); moreBtn.buttonEl.addEventListener("click", (ev) => { const menu = new Menu(); + menu.addItem((item) => item.setTitle(t("批量编辑_入口")).setIcon("list-plus").onClick(() => { + this.setBulkEditMode(!this.bulkEditMode); + })); menu.addItem((item) => item.setTitle(t("管理器_全选取消_描述")).setIcon("list-checks").onClick(() => { this.runDisplayedPluginsToggle(); })); @@ -1576,6 +1909,12 @@ export class ManagerModal extends Modal { }); footer.appendChild(toggleAllBtn); + const bulkBtn = createFooterBtn(this.bulkEditMode ? "square-check-big" : "list-plus", t("批量编辑_入口"), () => { + this.setBulkEditMode(!this.bulkEditMode); + }); + bulkBtn.toggleClass("is-active", this.bulkEditMode); + footer.appendChild(bulkBtn); + // 检查更新按钮 const updateBtn = createFooterBtn("rss", t("管理器_检查更新_描述"), () => { void this.runPluginUpdateCheck(updateBtn); @@ -1630,6 +1969,7 @@ export class ManagerModal extends Modal { public async showData(renderGeneration = this.renderGeneration) { this.syncPluginOverviewLayoutClass(); + const t = (k: any, vars?: Record) => this.manager.translator.t(k, vars); // 使用 manifests 按 id 去重,防止重复渲染 const page: ManagerPage = "plugins"; if (!this.isRenderCurrent(renderGeneration, page)) return; @@ -1657,7 +1997,9 @@ export class ManagerModal extends Modal { if (this.settings.DEBUG) console.log("[BPM] render showData before loop, children:", this.contentEl.children.length); this.displayPlugins = []; + const bulkBarHost = this.bulkEditMode ? this.contentEl.createDiv("manager-bulk-bar-host") : null; const renderedIds = new Set(); + let renderedCount = 0; for (const [layoutIndex, layoutItem] of layoutItems.entries()) { if (!this.isRenderCurrent(renderGeneration, page)) return; if (layoutItem.type === "separator") { @@ -1717,6 +2059,7 @@ export class ManagerModal extends Modal { itemEl.settingEl.toggleClass("has-update", Boolean(this.manager.updateStatus?.[plugin.id]?.hasUpdate)); itemEl.settingEl.toggleClass("is-bpm-ignored", ManagerPlugin.tags.includes(BPM_IGNORE_TAG)); itemEl.settingEl.toggleClass("is-hidden-layout", hiddenPluginIds.has(plugin.id)); + itemEl.settingEl.toggleClass("is-bulk-selected", this.bulkSelectedPluginIds.has(plugin.id)); itemEl.nameEl.addClass("manager-item__name-container"); itemEl.nameEl.addClass("manager-plugin-card__header"); itemEl.descEl.addClass("manager-item__description-container"); @@ -1729,9 +2072,26 @@ export class ManagerModal extends Modal { itemEl.settingEl.addClass("manager-plugin-card--layout-editing"); this.bindPluginLayoutDragHandle(itemEl.settingEl, layoutIndex, ManagerPlugin.name || plugin.name || plugin.id); } + if (this.bulkEditMode) { + const selection = itemEl.settingEl.createDiv("manager-plugin-card__bulk-select"); + const checkbox = selection.createEl("input", { type: "checkbox" }); + checkbox.checked = this.bulkSelectedPluginIds.has(plugin.id); + checkbox.disabled = isSelf; + checkbox.setAttribute("aria-label", t("批量编辑_选择插件", { name: ManagerPlugin.name || plugin.name || plugin.id })); + checkbox.addEventListener("click", (event) => event.stopPropagation()); + checkbox.addEventListener("change", () => { + this.toggleBulkPluginSelection(plugin.id, checkbox.checked); + }); + itemEl.settingEl.addEventListener("click", (event) => { + const target = event.target; + if (target instanceof HTMLElement && target.closest(".manager-plugin-card__actions, .manager-plugin-card__bulk-select, .manager-tag, .clickable-icon, button, input, select, textarea, a")) return; + this.toggleBulkPluginSelection(plugin.id, !this.bulkSelectedPluginIds.has(plugin.id)); + }); + } // [右键操作] itemEl.settingEl.addEventListener("contextmenu", (event) => { + if (this.bulkEditMode) return; event.preventDefault(); // 阻止默认的右键菜单 const menu = new Menu(); let hasContextMenuItems = false; @@ -1916,9 +2276,10 @@ export class ManagerModal extends Modal { // [批量操作] this.displayPlugins.push(plugin); + renderedCount++; // [目录样式] - if (!this.editorMode) { + if (!this.editorMode && !this.bulkEditMode) { switch (this.settings.ITEM_STYLE) { case "alwaysExpand": itemEl.descEl.addClass("manager-display-block"); break; case "neverExpand": itemEl.descEl.addClass("manager-display-none"); break; @@ -1981,7 +2342,20 @@ export class ManagerModal extends Modal { const item = groupSettingsById.get(ManagerPlugin.group); if (item) { const tag = this.manager.createTag(item.name, item.color, this.settings.GROUP_STYLE); - if (this.editorMode) tag.onclick = () => { new GroupModal(this.app, this.manager, this, ManagerPlugin).open(); }; + tag.addClass("manager-item__group-chip"); + tag.setAttribute("role", "button"); + tag.setAttribute("tabindex", "0"); + tag.setAttribute("aria-label", this.manager.translator.t("分组编辑_打开切换", { name: ManagerPlugin.name || plugin.name || plugin.id })); + const openGroupModal = (event?: Event) => { + event?.preventDefault(); + event?.stopPropagation(); + new GroupModal(this.app, this.manager, this, ManagerPlugin).open(); + }; + tag.onclick = openGroupModal; + tag.addEventListener("keydown", (event) => { + if (event.key !== "Enter" && event.key !== " ") return; + openGroupModal(event); + }); group.appendChild(tag); } } @@ -2024,26 +2398,8 @@ export class ManagerModal extends Modal { versionWrap.appendChild(version); if (!this.editorMode) { versionWrap.addClass("manager-item__versions--clickable"); - versionWrap.addEventListener("click", async () => { - const progress = this.showInlineProgress(this.manager.translator.t("通知_获取版本中文案"), plugin.id); - progress.update(0, 1, plugin.id); - try { - const st = await this.manager.checkUpdateForPlugin(plugin.id); - progress.update(1, 1, plugin.id); - this.refreshSinglePluginUpdateUi(plugin.id); - this.updateStats(); - const versions = st?.versions && st.versions.length > 0 - ? st.versions - : st?.remoteVersion - ? [{ version: st.remoteVersion, prerelease: /-/.test(st.remoteVersion) }] - : []; - new UpdateModal(this.app, this.manager, plugin.id, versions, st?.remoteVersion ?? null, st?.repo || undefined).open(); - } catch (e) { - console.error("[BPM] fetch remote versions failed", e); - new Notice(this.manager.translator.t("管理器_选择版本_获取失败提示"), 4000); - } finally { - progress.hide(); - } + versionWrap.addEventListener("click", () => { + void this.openPluginVersionList(plugin.id, this.manager.updateStatus?.[plugin.id] as PluginUpdateViewStatus | undefined); }); } const updateInfo = this.manager.updateStatus?.[plugin.id]; @@ -2561,6 +2917,13 @@ export class ManagerModal extends Modal { console.log("[BPM] render showData after loop, cards:", cards.length, "ids:", cards.map(el => el.getAttribute("data-plugin-id")).filter(Boolean).join(",")); } } + if (bulkBarHost) this.renderBulkBar(bulkBarHost); + if (renderedCount === 0) { + const empty = this.contentEl.createDiv("bpm-empty-state manager-plugin-page__empty"); + const icon = empty.createDiv(); + setIcon(icon, "search-x"); + empty.createDiv({ cls: "bpm-empty-state__text", text: t("管理器_暂无匹配插件") }); + } // 计算页尾 this.updateStats(); } @@ -2678,6 +3041,8 @@ export class ManagerModal extends Modal { this.desktopActionWrapper?.classList.toggle("is-ribbon-page", isRibbon); this.desktopActionWrapper?.classList.toggle("is-troubleshoot-page", isTroubleshoot); this.desktopActionWrapper?.classList.toggle("is-layout-editing", showLayoutTools); + this.desktopActionWrapper?.classList.toggle("is-bulk-editing", isPlugins && this.bulkEditMode); + this.bulkEditButtonEl?.classList.toggle("is-active", this.bulkEditMode); if (this.desktopFilterWrapper) { this.desktopFilterWrapper.classList.toggle("manager-display-none", !isPlugins); this.desktopFilterWrapper.style.display = isPlugins ? "" : "none"; @@ -2694,6 +3059,11 @@ export class ManagerModal extends Modal { return; } this.activePage = page; + if (page !== "plugins" && this.bulkEditMode) { + this.bulkEditMode = false; + this.bulkSelectedPluginIds.clear(); + this.applyEditingStyle(); + } this.syncPageChrome(); this.renderContent(); } @@ -5351,15 +5721,21 @@ export class ManagerModal extends Modal { this.contentEl.empty(); if (this.manager.ribbonModal === this.ribbonPage) this.manager.ribbonModal = null; if (this.modalContainer) this.modalContainer.removeClass("manager-container--editing"); + if (this.modalContainer) this.modalContainer.removeClass("manager-container--bulk-editing"); } private applyEditingStyle() { if (!this.modalContainer) return; if (this.editorMode) { - this.modalContainer.addClass("manager-container--editing"); + this.modalContainer.addClass("manager-container--editing"); } else { this.modalContainer.removeClass("manager-container--editing"); } + if (this.bulkEditMode) { + this.modalContainer.addClass("manager-container--bulk-editing"); + } else { + this.modalContainer.removeClass("manager-container--bulk-editing"); + } if (this.desktopActionWrapper) this.syncPageChrome(); } } diff --git a/src/modal/tags-modal.ts b/src/modal/tags-modal.ts index 2f96a85..bb613c7 100644 --- a/src/modal/tags-modal.ts +++ b/src/modal/tags-modal.ts @@ -115,7 +115,8 @@ export class TagsModal extends Modal { const previewLine = itemEl.nameEl.createDiv('manager-tag-editor__preview-line'); const tagEl = this.manager.createTag(tag.name || tag.id, tag.color, this.settings.TAG_STYLE); tagEl.addClass('manager-tag-editor__chip'); - previewLine.appendChild(tagEl); + const chipSlot = previewLine.createSpan({ cls: 'manager-tag-editor__chip-slot' }); + chipSlot.appendChild(tagEl); previewLine.createSpan({ cls: 'manager-tag-editor__id', text: tag.id }); if (isPreset) previewLine.createSpan({ cls: 'manager-tag-editor__badge is-system', text: t('通用_系统_文本') }); if (assigned) previewLine.createSpan({ cls: 'manager-tag-editor__badge is-assigned', text: t('通用_已分配_文本') }); @@ -211,52 +212,103 @@ export class TagsModal extends Modal { } } if (this.add) { - let id = ''; - let name = ''; let color = this.pickDistinctColor(this.settings.TAGS.map(t => t.color)); - const foodBar = new Setting(page).setClass('manager-bar__title'); - foodBar.settingEl.addClass('manager-tag-editor__add-panel'); - foodBar.nameEl.empty(); - foodBar.descEl.empty(); - const addTitle = foodBar.nameEl.createDiv('manager-tag-editor__add-title'); - const addIcon = addTitle.createSpan({ cls: 'manager-tag-editor__add-title-icon' }); - setIcon(addIcon, 'plus'); - addTitle.createSpan({ text: t('设置_标签设置_新增标签') }); - foodBar.descEl.setText(t('设置_分类_ID说明')); - foodBar.addColorPicker(cb => cb - .setValue(color) - .onChange((value) => { color = value; }) - ) - foodBar.addText(cb => cb - .setPlaceholder('ID') - .onChange((value) => { id = value; }) - .inputEl.addClass('manager-editor__item-input') - ) - foodBar.addText(cb => cb - .setPlaceholder(this.manager.translator.t('通用_名称_文本')) - .onChange((value) => { name = value; }) - .inputEl.addClass('manager-editor__item-input') - ) - foodBar.addExtraButton((cb) => { - cb.setIcon('plus'); - this.prepareIconButton(cb, t('标签编辑_创建标签'), 'manager-tag-editor__save-button'); - cb.onClick(() => { - const nextId = id.trim(); - const nextName = name.trim() || nextId; - const containsId = this.manager.settings.TAGS.some(tag => tag.id === nextId); - if (!containsId && nextId !== '' && nextId !== BPM_TAG_ID && nextId !== BPM_IGNORE_TAG) { - if (color === '') color = this.pickDistinctColor(this.settings.TAGS.map(t => t.color)); - this.manager.settings.TAGS.push({ id: nextId, name: nextName, color }); - this.manager.saveSettings(); - this.add = false; - this.reloadShowData(); - Commands(this.app, this.manager); - new Notice(this.manager.translator.t('设置_标签设置_通知_一')); - } else { - new Notice(this.manager.translator.t('设置_标签设置_通知_二')); - } - }); - }) + const addPanel = page.createDiv('manager-tag-editor__create-panel'); + const preview = addPanel.createDiv('manager-tag-editor__create-preview'); + const previewMain = preview.createDiv('manager-tag-editor__create-preview-main'); + const previewChip = this.manager.createTag(t('通用_名称_文本'), color, this.settings.TAG_STYLE); + previewChip.addClass('manager-tag-editor__chip'); + previewChip.addClass('manager-tag-editor__create-chip'); + const previewChipSlot = previewMain.createSpan({ cls: 'manager-tag-editor__chip-slot manager-tag-editor__chip-slot--preview' }); + previewChipSlot.appendChild(previewChip); + const previewText = previewMain.createDiv('manager-tag-editor__create-preview-text'); + previewText.createDiv({ cls: 'manager-tag-editor__create-title', text: t('设置_标签设置_新增标签') }); + const previewId = previewText.createDiv({ cls: 'manager-tag-editor__create-id', text: 'ID' }); + preview.createDiv({ cls: 'manager-tag-editor__create-desc', text: t('设置_分类_新增描述') }); + + const form = addPanel.createDiv('manager-tag-editor__create-form'); + const createField = (label: string, className = '') => { + const field = form.createDiv(`manager-tag-editor__create-field ${className}`); + field.createDiv({ cls: 'manager-tag-editor__create-label', text: label }); + return field; + }; + const colorField = createField(t('通用_颜色_文本'), 'manager-tag-editor__create-field--color'); + const colorInput = colorField.createEl('input'); + colorInput.type = 'color'; + colorInput.value = color; + colorInput.addClass('manager-tag-editor__create-color'); + colorInput.setAttribute('aria-label', t('通用_颜色_文本')); + + const idField = createField(t('通用_ID_文本')); + const idInput = idField.createEl('input'); + idInput.type = 'text'; + idInput.placeholder = 'tag-id'; + idInput.spellcheck = false; + idInput.addClass('manager-editor__item-input'); + idInput.addClass('manager-tag-editor__create-input'); + idInput.setAttribute('aria-label', t('通用_ID_文本')); + + const nameField = createField(t('通用_名称_文本')); + const nameInput = nameField.createEl('input'); + nameInput.type = 'text'; + nameInput.placeholder = t('通用_名称_文本'); + nameInput.addClass('manager-editor__item-input'); + nameInput.addClass('manager-tag-editor__create-input'); + nameInput.setAttribute('aria-label', t('通用_名称_文本')); + + const updatePreview = () => { + const nextId = idInput.value.trim(); + const nextName = nameInput.value.trim(); + previewChip.textContent = nextName || nextId || t('通用_名称_文本'); + previewChip.setAttribute('style', this.manager.generateTagStyle(color, this.settings.TAG_STYLE)); + previewId.setText(nextId || 'ID'); + }; + const submit = () => { + const nextId = idInput.value.trim(); + const nextName = nameInput.value.trim() || nextId; + const containsId = this.manager.settings.TAGS.some(tag => tag.id === nextId); + if (!containsId && nextId !== '' && nextId !== BPM_TAG_ID && nextId !== BPM_IGNORE_TAG) { + this.manager.settings.TAGS.push({ id: nextId, name: nextName, color }); + this.manager.saveSettings(); + this.add = false; + this.reloadShowData(); + Commands(this.app, this.manager); + new Notice(this.manager.translator.t('设置_标签设置_通知_一')); + } else { + new Notice(this.manager.translator.t('设置_标签设置_通知_二')); + } + }; + const onEnter = (event: KeyboardEvent) => { + if (event.key !== 'Enter') return; + event.preventDefault(); + submit(); + }; + colorInput.addEventListener('input', () => { + color = colorInput.value; + updatePreview(); + }); + idInput.addEventListener('input', () => updatePreview()); + nameInput.addEventListener('input', () => updatePreview()); + idInput.addEventListener('keydown', onEnter); + nameInput.addEventListener('keydown', onEnter); + + const actions = addPanel.createDiv('manager-tag-editor__create-actions'); + const cancelButton = actions.createEl('button', { cls: 'manager-tag-editor__create-action' }); + cancelButton.type = 'button'; + cancelButton.setAttribute('aria-label', t('通用_取消_文本')); + setIcon(cancelButton.createSpan({ cls: 'manager-tag-editor__create-action-icon' }), 'x'); + cancelButton.createSpan({ text: t('通用_取消_文本') }); + cancelButton.addEventListener('click', () => { + this.add = false; + this.reloadShowData(); + }); + const submitButton = actions.createEl('button', { cls: 'manager-tag-editor__create-action manager-tag-editor__create-action--primary' }); + submitButton.type = 'button'; + submitButton.setAttribute('aria-label', t('标签编辑_创建标签')); + setIcon(submitButton.createSpan({ cls: 'manager-tag-editor__create-action-icon' }), 'plus'); + submitButton.createSpan({ text: t('标签编辑_创建标签') }); + submitButton.addEventListener('click', submit); + window.setTimeout(() => idInput.focus(), 0); } else { // [底部行] 新增 const foodBar = new Setting(page).setClass('manager-bar__title').setName(this.manager.translator.t('通用_新增_文本')); diff --git a/src/modal/update-modal.ts b/src/modal/update-modal.ts index 8f05211..766e9f2 100644 --- a/src/modal/update-modal.ts +++ b/src/modal/update-modal.ts @@ -1,4 +1,4 @@ -import { App, DropdownComponent, Modal, Notice, Setting } from "obsidian"; +import { App, ExtraButtonComponent, Modal, Notice, setIcon, Setting } from "obsidian"; import Manager from "main"; import { ReleaseVersion, fetchReleaseVersions } from "src/github-install"; @@ -8,6 +8,9 @@ export class UpdateModal extends Modal { private versions: ReleaseVersion[]; private defaultVersion?: string | null; private repo?: string; + private selectedVersion = ""; + private downloadButton?: HTMLButtonElement; + private versionListEl?: HTMLElement; constructor(app: App, manager: Manager, pluginId: string, versions: ReleaseVersion[], defaultVersion?: string | null, repo?: string) { super(app); @@ -20,15 +23,55 @@ export class UpdateModal extends Modal { async onOpen() { const { contentEl } = this; - const t = (k: any) => this.manager.translator.t(k); + const t = (k: any, vars?: Record) => this.manager.translator.t(k, vars); + const manifest = this.manager.appPlugins.manifests?.[this.pluginId]; contentEl.empty(); + this.selectedVersion = this.defaultVersion || manifest?.version || (this.versions[0]?.version ?? ""); - const title = contentEl.createEl("h3", { text: t("管理器_选择版本_标题") }); - title.style.marginBottom = "8px"; + const modalEl = contentEl.parentElement as HTMLElement | null; + modalEl?.addClass("manager-editor__container"); + modalEl?.addClass("manager-version-picker"); + modalEl?.getElementsByClassName("modal-close-button")[0]?.remove(); + this.titleEl.parentElement?.addClass("manager-container__header"); + contentEl.addClass("manager-item-container"); + contentEl.addClass("manager-version-picker__body"); + + const titleBar = new Setting(this.titleEl).setClass("manager-bar__title"); + titleBar.settingEl.addClass("manager-version-picker__titlebar"); + titleBar.nameEl.empty(); + titleBar.descEl.empty(); + const titleWrap = titleBar.nameEl.createDiv("manager-version-picker__title"); + const titleIcon = titleWrap.createSpan({ cls: "manager-version-picker__title-icon" }); + setIcon(titleIcon, "package-down"); + const titleText = titleWrap.createDiv("manager-version-picker__title-text"); + titleText.createDiv({ cls: "manager-version-picker__eyebrow", text: t("管理器_选择版本_标题") }); + titleText.createDiv({ cls: "manager-version-picker__plugin-name", text: manifest?.name || this.pluginId }); + titleBar.descEl.setText(t("管理器_选择版本_说明")); + const closeButton = new ExtraButtonComponent(titleBar.controlEl); + closeButton.setIcon("x"); + closeButton.setTooltip(t("通用_取消_文本")); + const closeEl = ((closeButton as any).extraSettingsEl || (closeButton as any).buttonEl) as HTMLElement | undefined; + closeEl?.setAttribute("aria-label", t("通用_取消_文本")); + closeButton.onClick(() => this.close()); + + const page = contentEl.createDiv("manager-version-picker__page"); + const summary = page.createDiv("manager-version-picker__summary"); + const summaryMain = summary.createDiv("manager-version-picker__summary-main"); + summaryMain.createDiv({ cls: "manager-version-picker__summary-title", text: t("管理器_选择版本_列表标题") }); + summaryMain.createDiv({ + cls: "manager-version-picker__summary-desc", + text: t("管理器_选择版本_当前版本", { version: manifest?.version || "-" }), + }); + const summaryStats = summary.createDiv("manager-version-picker__summary-stats"); + const countStat = summaryStats.createSpan({ cls: "manager-version-picker__summary-stat" }); + setIcon(countStat.createSpan({ cls: "manager-version-picker__summary-stat-icon" }), "tags"); + countStat.createSpan({ text: `${this.versions.length}` }); + + this.versionListEl = page.createDiv("manager-version-picker__list"); let versionList = [...this.versions]; if (versionList.length === 0 && this.repo) { - const loading = contentEl.createDiv(); + const loading = this.versionListEl.createDiv("manager-version-picker__empty"); loading.setText(t("管理器_选择版本_获取中")); try { versionList = await fetchReleaseVersions(this.manager, this.repo); @@ -41,30 +84,42 @@ export class UpdateModal extends Modal { this.versions = versionList; } - let selected = this.defaultVersion || (versionList[0]?.version ?? ""); - if (versionList.length > 0) { - new Setting(contentEl) - .setName(t("管理器_选择版本_版本_标题")) - .addDropdown((dd: DropdownComponent) => { - versionList.forEach(v => { - dd.addOption(v.version, `${v.version}${v.prerelease ? " (pre)" : ""}`); - }); - dd.setValue(selected); - dd.onChange((v) => { selected = v; }); - }); + const hasDefaultVersion = Boolean(this.defaultVersion && versionList.some((release) => release.version === this.defaultVersion)); + const hasLocalVersion = Boolean(manifest?.version && versionList.some((release) => release.version === manifest.version)); + this.selectedVersion = hasDefaultVersion + ? this.defaultVersion! + : hasLocalVersion + ? manifest!.version + : (versionList[0]?.version ?? ""); + summaryStats.empty(); + const releaseCount = summaryStats.createSpan({ cls: "manager-version-picker__summary-stat" }); + setIcon(releaseCount.createSpan({ cls: "manager-version-picker__summary-stat-icon" }), "tags"); + releaseCount.createSpan({ text: t("管理器_选择版本_版本数量", { count: versionList.length }) }); + const currentStat = summaryStats.createSpan({ cls: "manager-version-picker__summary-stat" }); + setIcon(currentStat.createSpan({ cls: "manager-version-picker__summary-stat-icon" }), "badge-check"); + currentStat.createSpan({ text: this.selectedVersion || t("管理器_安装_版本_默认最新") }); + + if (versionList.length > 0 && this.versionListEl) { + this.renderVersionList(versionList, manifest?.version || ""); } else { - const info = contentEl.createDiv(); + const info = this.versionListEl?.createDiv("manager-version-picker__empty") ?? page.createDiv("manager-version-picker__empty"); info.setText(t("管理器_选择版本_无版本提示")); } - new Setting(contentEl) + const footer = new Setting(page); + footer.settingEl.addClass("manager-version-picker__footer"); + footer.nameEl.empty(); + footer.descEl.empty(); + footer .addButton((btn) => { - btn.setButtonText(t("管理器_选择版本_下载按钮")); + btn.setButtonText(t("管理器_选择版本_切换按钮")); btn.setCta(); + this.downloadButton = btn.buttonEl; + btn.setDisabled(!this.selectedVersion); btn.onClick(async () => { btn.setDisabled(true); try { - const ok = await this.manager.downloadUpdate(this.pluginId, selected); + const ok = await this.manager.downloadUpdate(this.pluginId, this.selectedVersion); if (ok) { new Notice(t("管理器_选择版本_成功提示"), 3000); this.close(); @@ -79,5 +134,49 @@ export class UpdateModal extends Modal { btn.onClick(() => this.close()); }); } + + private renderVersionList(versionList: ReleaseVersion[], localVersion: string) { + if (!this.versionListEl) return; + const t = (k: any, vars?: Record) => this.manager.translator.t(k, vars); + this.versionListEl.empty(); + versionList.forEach((release) => { + const item = this.versionListEl!.createEl("button", { cls: "manager-version-picker__item" }); + item.type = "button"; + item.toggleClass("is-selected", release.version === this.selectedVersion); + item.toggleClass("is-current", Boolean(localVersion && release.version === localVersion)); + item.setAttribute("aria-pressed", `${release.version === this.selectedVersion}`); + item.setAttribute("aria-label", t("管理器_选择版本_选择版本", { version: release.version })); + + const main = item.createDiv("manager-version-picker__item-main"); + const title = main.createDiv("manager-version-picker__item-title"); + title.createSpan({ cls: "manager-version-picker__item-version", text: release.version }); + if (release.version === localVersion) { + title.createSpan({ cls: "manager-version-picker__badge", text: t("来源_当前") }); + } + title.createSpan({ + cls: `manager-version-picker__badge ${release.prerelease ? "is-prerelease" : "is-stable"}`, + text: t(release.prerelease ? "安装_发布类型_预发布" : "安装_发布类型_正式版"), + }); + + const meta = main.createDiv("manager-version-picker__item-meta"); + if (release.name && release.name !== release.version) meta.createSpan({ text: release.name }); + if (release.publishedAt) meta.createSpan({ text: this.formatDate(release.publishedAt) }); + + const check = item.createSpan({ cls: "manager-version-picker__item-check" }); + setIcon(check, release.version === this.selectedVersion ? "check" : "circle"); + + item.addEventListener("click", () => { + this.selectedVersion = release.version; + this.renderVersionList(versionList, localVersion); + this.downloadButton?.removeAttribute("disabled"); + }); + }); + } + + private formatDate(value: string): string { + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + return date.toLocaleDateString(); + } } diff --git a/styles.css b/styles.css index 2b76018..1da9175 100644 --- a/styles.css +++ b/styles.css @@ -756,9 +756,26 @@ body.theme-dark { margin-left: 4px; } -.manager-item__versions--clickable { - cursor: pointer; -} +.manager-item__versions--clickable { + cursor: pointer; +} + +.manager-item__versions--clickable .manager-item__name-version:hover { + border-color: color-mix(in srgb, var(--interactive-accent) 44%, var(--bpm-border)); + background: var(--bpm-accent-soft); + color: var(--text-accent); +} + +.manager-item__group-chip.manager-tag { + cursor: pointer; +} + +.manager-item__group-chip.manager-tag:hover, +.manager-item__group-chip.manager-tag:focus-visible { + outline: none; + box-shadow: 0 0 0 2px color-mix(in srgb, var(--interactive-accent) 35%, transparent); + filter: brightness(1.04); +} .manager-item__name-remote { display: inline-flex; @@ -2498,15 +2515,15 @@ body.theme-dark { .manager-toolbar__tools .manager-tool--ribbon, .manager-toolbar__tools .manager-tool--layout, .manager-toolbar__tools .manager-tool--transfer { - order: 10; + order: var(--manager-tool-order, 10); } .manager-toolbar__tools .manager-tool--resource { - order: 20; + order: var(--manager-tool-order, 120); } .manager-toolbar__tools .manager-tool--global { - order: 30; + order: var(--manager-tool-order, 70); } .manager-section--actions:not(.is-ribbon-page) .manager-tool--ribbon, @@ -4995,13 +5012,169 @@ body.theme-dark { filter: none; } -.manager-container .manager-plugin-card { - position: relative; - align-items: stretch; - gap: 16px; - padding: 16px; - border-left-width: 3px; -} +.manager-container .manager-plugin-card { + position: relative; + align-items: stretch; + gap: 16px; + padding: 16px; + border-left-width: 3px; +} + +.manager-container--bulk-editing { + box-shadow: 0 0 0 2px color-mix(in srgb, var(--interactive-accent) 42%, transparent), 0 12px 30px rgba(0, 0, 0, 0.18); +} + +.manager-bulk-bar-host { + position: sticky; + top: 0; + z-index: 5; + width: 100%; + margin: 0 0 8px; +} + +.manager-bulk-bar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + min-height: 42px; + padding: 7px 8px; + border: 1px solid color-mix(in srgb, var(--interactive-accent) 28%, var(--bpm-border)); + border-radius: var(--bpm-radius); + background: color-mix(in srgb, var(--interactive-accent) 7%, var(--bpm-card)); + box-shadow: var(--bpm-shadow); +} + +.manager-bulk-bar__summary { + display: inline-flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.manager-bulk-bar__summary-icon { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 28px; + width: 28px; + height: 28px; + border: 1px solid color-mix(in srgb, var(--interactive-accent) 34%, var(--bpm-border)); + border-radius: var(--bpm-radius-sm); + background: color-mix(in srgb, var(--interactive-accent) 10%, var(--bpm-card)); + color: var(--text-accent); +} + +.manager-bulk-bar__summary-icon svg { + width: 15px; + height: 15px; +} + +.manager-bulk-bar__summary-text { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.manager-bulk-bar__title { + color: var(--text-normal); + font-size: 12px; + font-weight: 700; + line-height: 1.1; +} + +.manager-bulk-bar__count { + color: var(--text-muted); + font-size: 11px; + font-weight: 600; + line-height: 1.1; +} + +.manager-bulk-bar__actions { + display: inline-flex; + align-items: center; + justify-content: flex-end; + flex-wrap: wrap; + gap: 6px; + min-width: 0; +} + +.manager-bulk-bar__button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 5px; + min-height: 30px; + padding: 0 8px; + border: 1px solid var(--bpm-border); + border-radius: var(--bpm-radius-sm); + background: var(--bpm-card); + color: var(--text-normal); + font-size: 11px; + font-weight: 650; + line-height: 1; + cursor: pointer; + transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, transform 0.12s ease; +} + +.manager-bulk-bar__button:hover { + border-color: color-mix(in srgb, var(--interactive-accent) 42%, var(--bpm-border)); + background: var(--bpm-accent-soft); + color: var(--text-accent); +} + +.manager-bulk-bar__button:active { + transform: translateY(1px); +} + +.manager-bulk-bar__button:disabled { + opacity: 0.48; + cursor: not-allowed; +} + +.manager-bulk-bar__button-icon { + display: inline-flex; + width: 13px; + height: 13px; +} + +.manager-bulk-bar__button-icon svg { + width: 13px; + height: 13px; +} + +.manager-toolbar__tools .manager-tool.is-active, +.manager-container--mobile .bpm-mobile-header__actions button.is-active, +.manager-container--mobile .bpm-mobile-footer__btn.is-active { + border-color: color-mix(in srgb, var(--interactive-accent) 46%, var(--bpm-border)); + background: color-mix(in srgb, var(--interactive-accent) 14%, var(--bpm-card)); + color: var(--text-accent); +} + +.manager-container .manager-plugin-card.is-bulk-selected { + border-color: color-mix(in srgb, var(--interactive-accent) 48%, var(--bpm-border)); + background: color-mix(in srgb, var(--interactive-accent) 6%, var(--bpm-card)); +} + +.manager-plugin-card__bulk-select { + display: inline-flex; + align-items: center; + justify-content: center; + align-self: center; + flex: 0 0 30px; + order: -2; + width: 30px; + height: 30px; + margin-right: 2px; + border: 1px solid var(--bpm-border); + border-radius: var(--bpm-radius-sm); + background: color-mix(in srgb, var(--bpm-muted) 62%, var(--bpm-card)); +} + +.manager-plugin-card__bulk-select input { + margin: 0; +} .manager-container .manager-plugin-card.is-enabled { border-left-color: color-mix(in srgb, var(--color-green) 62%, var(--bpm-border)); @@ -5811,15 +5984,29 @@ body.theme-dark { width: 100%; } - .manager-container .manager-plugin-card { - align-items: stretch; - flex-direction: column; - gap: 12px; - } - - .manager-container .manager-plugin-card__actions.setting-item-control { - align-self: stretch; - justify-content: flex-end; + .manager-container .manager-plugin-card { + align-items: stretch; + flex-direction: column; + gap: 12px; + } + + .manager-bulk-bar { + align-items: stretch; + flex-direction: column; + } + + .manager-bulk-bar__actions { + justify-content: flex-start; + width: 100%; + } + + .manager-bulk-bar__button { + min-height: 36px; + } + + .manager-container .manager-plugin-card__actions.setting-item-control { + align-self: stretch; + justify-content: flex-end; width: 100%; min-width: 0; padding-top: 10px; @@ -7761,9 +7948,132 @@ body.theme-dark { padding: 0; } -.manager-tag-editor__add-trigger { - background: color-mix(in srgb, var(--bpm-muted) 48%, var(--bpm-card)); -} +.manager-tag-editor__add-trigger { + background: color-mix(in srgb, var(--bpm-muted) 48%, var(--bpm-card)); +} + +.manager-tag-editor__create-panel { + display: flex; + flex-direction: column; + gap: 10px; + padding: 10px; + border: 1px solid color-mix(in srgb, var(--interactive-accent) 28%, var(--bpm-border)); + border-radius: var(--bpm-radius); + background: color-mix(in srgb, var(--interactive-accent) 5%, var(--bpm-card)); +} + +.manager-tag-editor__create-preview { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + min-width: 0; + padding: 8px; + border: 1px solid var(--bpm-border); + border-radius: var(--bpm-radius-sm); + background: var(--bpm-card); +} + +.manager-tag-editor__create-preview-main { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.manager-tag-editor__create-preview-text { + min-width: 0; +} + +.manager-tag-editor__create-title { + color: var(--text-normal); + font-size: 13px; + font-weight: 700; + line-height: 1.2; +} + +.manager-tag-editor__create-id { + margin-top: 2px; + overflow: hidden; + color: var(--text-muted); + font-family: var(--font-monospace); + font-size: 11px; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.manager-tag-editor__create-desc { + flex: 0 1 auto; + color: var(--text-muted); + font-size: 11px; + line-height: 1.25; + text-align: right; +} + +.manager-tag-editor__create-form { + display: grid; + grid-template-columns: minmax(56px, 72px) minmax(0, 1fr) minmax(0, 1fr); + gap: 8px; + min-width: 0; +} + +.manager-tag-editor__create-field { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; +} + +.manager-tag-editor__create-label { + color: var(--text-muted); + font-size: 10px; + font-weight: 700; + line-height: 1; +} + +.manager-tag-editor__create-color { + width: 100% !important; + min-width: 0 !important; +} + +.manager-tag-editor__create-input { + width: 100% !important; +} + +.manager-tag-editor__create-actions { + display: flex; + align-items: center; + justify-content: flex-end; + flex-wrap: wrap; + gap: 7px; +} + +.manager-tag-editor__create-action { + display: inline-flex !important; + align-items: center; + justify-content: center; + gap: 5px; + width: auto !important; + min-width: 0 !important; + padding: 0 10px !important; + font-size: 12px; + font-weight: 650; + white-space: nowrap; +} + +.manager-tag-editor__create-action-icon, +.manager-tag-editor__create-action-icon svg { + width: 13px; + height: 13px; +} + +.manager-tag-editor__create-action--primary, +.manager-tag-editor__create-action--primary:hover { + border-color: color-mix(in srgb, var(--interactive-accent) 52%, var(--bpm-border)) !important; + background: color-mix(in srgb, var(--interactive-accent) 15%, var(--bpm-card)) !important; + color: var(--text-accent) !important; +} .manager-setting-tag__container { margin-top: 0; @@ -7816,19 +8126,36 @@ body.theme-dark { flex-direction: column; } - .manager-tag-editor__summary-stats, - .manager-setting-tag__stats, - .manager-tag-editor__item-actions.setting-item-control, - .manager-setting-tag__controls.setting-item-control, - .manager-tag-editor__add-panel .setting-item-control, - .manager-tag-editor__add-trigger .setting-item-control { - width: 100%; - min-width: 0; - justify-content: flex-start; - } - - .manager-tag-editor .clickable-icon, - .manager-tag-editor button, + .manager-tag-editor__summary-stats, + .manager-setting-tag__stats, + .manager-tag-editor__item-actions.setting-item-control, + .manager-setting-tag__controls.setting-item-control, + .manager-tag-editor__add-panel .setting-item-control, + .manager-tag-editor__add-trigger .setting-item-control { + width: 100%; + min-width: 0; + justify-content: flex-start; + } + + .manager-tag-editor__create-preview { + align-items: stretch; + flex-direction: column; + } + + .manager-tag-editor__create-desc { + text-align: left; + } + + .manager-tag-editor__create-form { + grid-template-columns: 1fr; + } + + .manager-tag-editor__create-actions { + justify-content: flex-start; + } + + .manager-tag-editor .clickable-icon, + .manager-tag-editor button, .manager-setting-tag__container .clickable-icon, .manager-setting-tag__container button { width: 40px; @@ -8369,12 +8696,293 @@ body.theme-dark { .manager-source-card:hover, .manager-hidden-card:hover, .ribbon-manager-modal .draggable-item:hover, -.manager-tag-editor__item.setting-item:hover, -.manager-setting-tag__item.setting-item:hover { - border-color: color-mix(in srgb, var(--text-muted) 36%, var(--bpm-border)); - background: var(--bpm-card-hover); - box-shadow: var(--bpm-shadow-hover); -} +.manager-tag-editor__item.setting-item:hover, +.manager-setting-tag__item.setting-item:hover { + border-color: color-mix(in srgb, var(--text-muted) 36%, var(--bpm-border)); + background: var(--bpm-card-hover); + box-shadow: var(--bpm-shadow-hover); +} + +/* Version picker modal */ +.manager-version-picker, +.manager-version-picker.manager-editor__container { + width: min(520px, calc(100vw - 32px)); + max-height: min(78vh, 720px); + border: 1px solid var(--bpm-border); + border-radius: var(--bpm-ui-modal-radius); + background: var(--bpm-surface); + box-shadow: var(--bpm-ui-modal-shadow); + overflow: hidden; +} + +.manager-version-picker .manager-container__header { + margin: 0; + padding: 0; + background: var(--background-primary); +} + +.manager-version-picker__titlebar.setting-item { + min-height: 50px; + margin: 0; + padding: 8px 12px; + border: 0; + border-bottom: 1px solid var(--bpm-border); + background: var(--bpm-card); +} + +.manager-version-picker__title { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.manager-version-picker__title-icon { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 var(--bpm-icon-tile); + width: var(--bpm-icon-tile); + height: var(--bpm-icon-tile); + border: 1px solid color-mix(in srgb, var(--interactive-accent) 30%, var(--bpm-border)); + border-radius: var(--bpm-radius-sm); + background: var(--bpm-accent-soft); + color: var(--text-accent); +} + +.manager-version-picker__title-icon svg { + width: var(--bpm-icon-size); + height: var(--bpm-icon-size); +} + +.manager-version-picker__title-text { + min-width: 0; +} + +.manager-version-picker__eyebrow { + color: var(--text-muted); + font-size: 10px; + font-weight: 700; + line-height: 1.1; + text-transform: uppercase; +} + +.manager-version-picker__plugin-name { + overflow: hidden; + color: var(--text-normal); + font-size: 14px; + font-weight: 700; + line-height: 1.25; + text-overflow: ellipsis; + white-space: nowrap; +} + +.manager-version-picker__titlebar .setting-item-description { + color: var(--text-muted); + font-size: 12px; +} + +.manager-version-picker__body.manager-item-container { + gap: 0; + padding: 10px 12px 12px; +} + +.manager-version-picker__page { + display: flex; + flex-direction: column; + gap: 10px; + min-height: 0; +} + +.manager-version-picker__summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 8px 9px; + border: 1px solid var(--bpm-border); + border-radius: var(--bpm-radius); + background: color-mix(in srgb, var(--bpm-muted) 48%, var(--bpm-card)); +} + +.manager-version-picker__summary-main { + min-width: 0; +} + +.manager-version-picker__summary-title { + color: var(--text-normal); + font-size: 13px; + font-weight: 700; + line-height: 1.2; +} + +.manager-version-picker__summary-desc { + margin-top: 3px; + color: var(--text-muted); + font-size: 12px; + line-height: 1.25; +} + +.manager-version-picker__summary-stats { + display: flex; + align-items: center; + flex-wrap: wrap; + justify-content: flex-end; + gap: 5px; +} + +.manager-version-picker__summary-stat, +.manager-version-picker__badge { + display: inline-flex; + align-items: center; + gap: 4px; + min-height: var(--bpm-chip-height); + padding: var(--bpm-chip-padding); + border: 1px solid var(--bpm-border); + border-radius: var(--bpm-radius-sm); + background: var(--bpm-card); + color: var(--text-muted); + font-size: 11px; + font-weight: 650; + line-height: 1; + white-space: nowrap; +} + +.manager-version-picker__summary-stat-icon { + display: inline-flex; +} + +.manager-version-picker__summary-stat-icon svg { + width: 12px; + height: 12px; +} + +.manager-version-picker__list { + display: flex; + flex-direction: column; + gap: 6px; + max-height: min(46vh, 420px); + overflow: auto; + padding-right: 2px; +} + +.manager-version-picker__item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + width: 100%; + min-height: 52px; + padding: 9px 10px; + border: 1px solid var(--bpm-border); + border-radius: var(--bpm-radius); + background: var(--bpm-card); + color: var(--text-normal); + text-align: left; + cursor: pointer; +} + +.manager-version-picker__item:hover { + border-color: color-mix(in srgb, var(--interactive-accent) 34%, var(--bpm-border)); + background: var(--bpm-card-hover); +} + +.manager-version-picker__item.is-selected { + border-color: color-mix(in srgb, var(--interactive-accent) 60%, var(--bpm-border)); + background: color-mix(in srgb, var(--interactive-accent) 9%, var(--bpm-card)); +} + +.manager-version-picker__item-main { + display: flex; + flex-direction: column; + gap: 5px; + min-width: 0; +} + +.manager-version-picker__item-title { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 5px; + min-width: 0; +} + +.manager-version-picker__item-version { + color: var(--text-normal); + font-size: 13px; + font-weight: 750; + line-height: 1.15; +} + +.manager-version-picker__badge.is-prerelease { + border-color: color-mix(in srgb, var(--color-orange) 48%, var(--bpm-border)); + background: color-mix(in srgb, var(--color-orange) 11%, var(--bpm-card)); + color: var(--color-orange); +} + +.manager-version-picker__badge.is-stable { + border-color: color-mix(in srgb, var(--color-green) 42%, var(--bpm-border)); + background: color-mix(in srgb, var(--color-green) 10%, var(--bpm-card)); + color: var(--color-green); +} + +.manager-version-picker__item-meta { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; + overflow-wrap: anywhere; + color: var(--text-muted); + font-size: 11px; + line-height: 1.25; +} + +.manager-version-picker__item-check { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 24px; + width: 24px; + height: 24px; + color: var(--text-muted); +} + +.manager-version-picker__item.is-selected .manager-version-picker__item-check { + color: var(--text-accent); +} + +.manager-version-picker__item-check svg { + width: 15px; + height: 15px; +} + +.manager-version-picker__empty { + padding: 18px 10px; + border: 1px dashed var(--bpm-border); + border-radius: var(--bpm-radius); + background: color-mix(in srgb, var(--bpm-muted) 48%, transparent); + color: var(--text-muted); + font-size: 13px; + text-align: center; +} + +.manager-version-picker__footer.setting-item { + margin: 0; + padding: 0; + border: 0; +} + +.manager-version-picker__footer .setting-item-control { + justify-content: flex-end; + width: 100%; + min-width: 0; + gap: 8px; +} + +.manager-version-picker .clickable-icon, +.manager-version-picker button { + border-radius: var(--bpm-radius-sm); +} .manager-container .manager-plugin-card { gap: 10px; @@ -8756,3 +9364,642 @@ body.theme-dark { .manager-delete__action.manager-delete__actions { padding: 2px 0 0; } + +/* Final group/tag switch modal refresh. */ +.manager-tag-editor.manager-editor__container { + width: min(620px, calc(100vw - 32px)); + max-height: min(82vh, 760px); + border: 1px solid var(--bpm-border); + border-radius: var(--bpm-ui-modal-radius); + background: var(--bpm-surface); + box-shadow: var(--bpm-ui-modal-shadow); + overflow: hidden; +} + +.manager-tag-editor .manager-container__header { + margin: 0; + padding: 0; + border-bottom: 1px solid var(--bpm-border); + background: var(--bpm-card); +} + +.manager-tag-editor__titlebar.setting-item { + align-items: center; + min-height: 58px; + margin: 0; + padding: 10px 12px; + border: 0; + background: transparent; +} + +.manager-tag-editor__titlebar .setting-item-info { + min-width: 0; + padding: 0; +} + +.manager-tag-editor__titlebar .setting-item-control { + flex: 0 0 auto; + min-width: max-content; +} + +.manager-tag-editor__title { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.manager-tag-editor__title-icon { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 var(--bpm-icon-tile); + width: var(--bpm-icon-tile); + height: var(--bpm-icon-tile); + border: 1px solid color-mix(in srgb, var(--interactive-accent) 32%, var(--bpm-border)); + border-radius: var(--bpm-radius-sm); + background: var(--bpm-accent-soft); + color: var(--text-accent); +} + +.manager-tag-editor__title-icon svg { + width: var(--bpm-icon-size); + height: var(--bpm-icon-size); +} + +.manager-tag-editor__title-text { + min-width: 0; +} + +.manager-tag-editor__eyebrow { + display: block; + color: var(--text-muted); + font-size: 10px; + font-weight: 700; + line-height: 1.1; +} + +.manager-tag-editor__plugin-name { + margin-top: 3px; + overflow: hidden; + color: var(--text-normal); + font-size: 15px; + font-weight: 750; + line-height: 1.25; + text-overflow: ellipsis; + white-space: nowrap; +} + +.manager-tag-editor__titlebar .setting-item-description { + display: block; + margin-top: 4px; + color: var(--text-muted); + font-size: 12px; + line-height: 1.35; +} + +.manager-tag-editor__body.manager-item-container { + gap: 12px; + padding: 12px; + overflow-y: auto; + background: var(--bpm-surface); +} + +.manager-tag-editor__page, +.manager-tag-editor__list { + display: flex; + flex-direction: column; + gap: 10px; + min-width: 0; +} + +.manager-tag-editor__summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-width: 0; + padding: 12px; + border: 1px solid var(--bpm-border); + border-radius: var(--bpm-radius); + background: color-mix(in srgb, var(--bpm-muted) 52%, var(--bpm-card)); + box-shadow: var(--bpm-shadow); +} + +.manager-tag-editor__summary-main { + min-width: 0; +} + +.manager-tag-editor__summary-title { + color: var(--text-normal); + font-size: 14px; + font-weight: 750; + line-height: 1.25; +} + +.manager-tag-editor__summary-desc { + margin-top: 4px; + overflow: hidden; + color: var(--text-muted); + font-size: 12px; + line-height: 1.35; + text-overflow: ellipsis; + white-space: nowrap; +} + +.manager-tag-editor__summary-stats { + display: flex; + align-items: center; + justify-content: flex-end; + flex: 0 0 auto; + flex-wrap: wrap; + gap: 6px; +} + +.manager-tag-editor__summary-stat, +.manager-tag-editor__badge { + display: inline-flex; + align-items: center; + gap: 5px; + min-height: var(--bpm-chip-height); + padding: var(--bpm-chip-padding); + border: 1px solid var(--bpm-border); + border-radius: var(--bpm-radius-sm); + background: var(--bpm-card); + color: var(--text-muted); + font-size: 11px; + font-weight: 650; + line-height: 1; + white-space: nowrap; +} + +.manager-tag-editor__summary-stat-icon { + display: inline-flex; +} + +.manager-tag-editor__summary-stat-icon svg { + width: 13px; + height: 13px; + stroke-width: 2.2; +} + +.manager-tag-editor__summary-stat-value { + color: var(--text-normal); + font-variant-numeric: tabular-nums; + font-weight: 750; +} + +.manager-tag-editor__item.setting-item, +.manager-tag-editor__add-trigger.setting-item { + align-items: center; + gap: 12px; + min-height: 58px; + margin: 0; + padding: 12px; + border: 1px solid var(--bpm-border); + border-radius: var(--bpm-radius); + background: var(--bpm-card); + box-shadow: var(--bpm-shadow); + transition: background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease; +} + +.manager-tag-editor__item.setting-item:hover, +.manager-tag-editor__add-trigger.setting-item:hover { + border-color: color-mix(in srgb, var(--text-muted) 36%, var(--bpm-border)); + background: var(--bpm-card-hover); + box-shadow: var(--bpm-shadow-hover); +} + +.manager-tag-editor__item.is-assigned { + border-left-width: 3px; + border-left-color: color-mix(in srgb, var(--interactive-accent) 64%, var(--bpm-border)); +} + +.manager-tag-editor__item.is-editing { + border-color: color-mix(in srgb, var(--interactive-accent) 48%, var(--bpm-border)); + background: color-mix(in srgb, var(--interactive-accent) 7%, var(--bpm-card)); +} + +.manager-tag-editor__item.is-system { + background: color-mix(in srgb, var(--bpm-muted) 34%, var(--bpm-card)); +} + +.manager-tag-editor__item>.setting-item-info, +.manager-tag-editor__add-trigger .setting-item-info { + flex: 1 1 auto; + min-width: 0; + padding: 0; +} + +.manager-tag-editor__item-actions.setting-item-control, +.manager-tag-editor__add-trigger .setting-item-control { + display: flex; + align-items: center; + justify-content: flex-end; + flex: 0 0 auto; + flex-wrap: wrap; + gap: 7px; + min-width: min(250px, 100%); +} + +.manager-tag-editor__preview-line { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 7px; + min-width: 0; +} + +.manager-tag-editor__chip.manager-tag { + min-height: 22px; + height: 22px; + margin-left: 0; + font-weight: 700; +} + +.manager-tag-editor__id { + min-width: 0; + overflow: hidden; + color: var(--text-muted); + font-family: var(--font-monospace); + font-size: 11px; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.manager-tag-editor__badge.is-assigned { + border-color: color-mix(in srgb, var(--interactive-accent) 42%, var(--bpm-border)); + background: color-mix(in srgb, var(--interactive-accent) 12%, var(--bpm-card)); + color: var(--text-accent); +} + +.manager-tag-editor__badge.is-system { + border-color: color-mix(in srgb, var(--text-muted) 38%, var(--bpm-border)); + background: color-mix(in srgb, var(--bpm-muted) 76%, var(--bpm-card)); +} + +.manager-tag-editor__meta { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + margin-top: 6px; + color: var(--text-muted); + font-size: 12px; + line-height: 1.35; +} + +.manager-tag-editor input[type="color"], +.manager-tag-editor .clickable-icon, +.manager-tag-editor button { + width: var(--bpm-control-size); + height: var(--bpm-control-size); + min-width: var(--bpm-control-size); + min-height: var(--bpm-control-size); + border-radius: var(--bpm-radius-sm); +} + +.manager-tag-editor__name-input, +.manager-tag-editor .manager-editor__item-input { + min-height: var(--bpm-control-size); + height: var(--bpm-control-size); + width: min(180px, 100%); + border-radius: var(--bpm-radius-sm); +} + +.manager-tag-editor__create-panel { + display: flex; + flex-direction: column; + gap: 10px; + padding: 12px; + border: 1px solid color-mix(in srgb, var(--interactive-accent) 28%, var(--bpm-border)); + border-radius: var(--bpm-radius); + background: color-mix(in srgb, var(--interactive-accent) 5%, var(--bpm-card)); + box-shadow: var(--bpm-shadow); +} + +.manager-tag-editor__create-preview { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-width: 0; + padding: 9px; + border: 1px solid var(--bpm-border); + border-radius: var(--bpm-radius-sm); + background: var(--bpm-card); +} + +.manager-tag-editor__create-preview-main { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.manager-tag-editor__create-preview-text { + min-width: 0; +} + +.manager-tag-editor__create-title { + color: var(--text-normal); + font-size: 13px; + font-weight: 700; + line-height: 1.2; +} + +.manager-tag-editor__create-id { + margin-top: 3px; + overflow: hidden; + color: var(--text-muted); + font-family: var(--font-monospace); + font-size: 11px; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.manager-tag-editor__create-desc { + flex: 0 1 auto; + color: var(--text-muted); + font-size: 12px; + line-height: 1.25; + text-align: right; +} + +.manager-tag-editor__create-form { + display: grid; + grid-template-columns: minmax(58px, 76px) minmax(0, 1fr) minmax(0, 1fr); + gap: 8px; + min-width: 0; +} + +.manager-tag-editor__create-field { + display: flex; + flex-direction: column; + gap: 5px; + min-width: 0; +} + +.manager-tag-editor__create-label { + color: var(--text-muted); + font-size: 10px; + font-weight: 700; + line-height: 1; +} + +.manager-tag-editor__create-color, +.manager-tag-editor__create-input { + width: 100% !important; + min-width: 0 !important; +} + +.manager-tag-editor__create-actions { + display: flex; + align-items: center; + justify-content: flex-end; + flex-wrap: wrap; + gap: 7px; +} + +.manager-tag-editor__create-action { + display: inline-flex !important; + align-items: center; + justify-content: center; + gap: 5px; + width: auto !important; + min-width: 0 !important; + padding: 0 10px !important; + font-size: 12px; + font-weight: 650; + white-space: nowrap; +} + +.manager-tag-editor__create-action-icon, +.manager-tag-editor__create-action-icon svg { + width: 13px; + height: 13px; +} + +.manager-tag-editor__create-action--primary, +.manager-tag-editor__create-action--primary:hover, +.manager-tag-editor__save-button, +.manager-tag-editor__save-button:hover { + border-color: color-mix(in srgb, var(--interactive-accent) 52%, var(--bpm-border)) !important; + background: color-mix(in srgb, var(--interactive-accent) 15%, var(--bpm-card)) !important; + color: var(--text-accent) !important; +} + +@media (max-width: 640px) { + .manager-tag-editor.manager-editor__container { + width: min(94vw, 620px); + max-height: 86vh; + } + + .manager-tag-editor__summary, + .manager-tag-editor__item.setting-item, + .manager-tag-editor__add-trigger.setting-item { + align-items: stretch; + flex-direction: column; + } + + .manager-tag-editor__summary-stats, + .manager-tag-editor__item-actions.setting-item-control, + .manager-tag-editor__add-trigger .setting-item-control { + justify-content: flex-start; + width: 100%; + min-width: 0; + } + + .manager-tag-editor__summary-desc { + white-space: normal; + } + + .manager-tag-editor__create-preview { + align-items: stretch; + flex-direction: column; + } + + .manager-tag-editor__create-desc { + text-align: left; + } + + .manager-tag-editor__create-form { + grid-template-columns: 1fr; + } + + .manager-tag-editor__create-actions { + justify-content: flex-start; + } +} + +/* Final taxonomy row alignment polish. */ +.manager-tag-editor__item.setting-item { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(92px, auto); + column-gap: 14px; + align-items: center; + padding: 11px 12px 11px 14px; + border-left-width: 1px; +} + +.manager-tag-editor__item.is-assigned { + padding-left: 12px; +} + +.manager-tag-editor__item.is-editing { + grid-template-columns: minmax(0, 1fr) minmax(244px, auto); +} + +.manager-tag-editor__item>.setting-item-info { + display: flex; + flex-direction: column; + justify-content: center; + min-width: 0; +} + +.manager-tag-editor__item>.setting-item-control { + display: grid; + grid-auto-flow: column; + grid-auto-columns: var(--bpm-control-size); + justify-content: end; + align-items: center; + gap: 8px; + min-width: 0; +} + +.manager-tag-editor__item.is-editing>.setting-item-control { + grid-auto-columns: max-content; +} + +.manager-tag-editor__item-actions.setting-item-control .checkbox-container { + justify-self: end; + margin: 0; +} + +.manager-tag-editor__toggle { + display: flex; + align-items: center; + justify-content: center; + width: 42px; + min-width: 42px; +} + +.manager-tag-editor__preview-line { + display: grid; + grid-template-columns: minmax(108px, max-content) minmax(52px, 1fr) auto auto; + align-items: center; + gap: 8px; + width: 100%; +} + +.manager-tag-editor__chip-slot { + display: inline-flex; + align-items: center; + justify-content: flex-start; + min-width: 108px; + max-width: 180px; + min-height: 28px; + padding: 3px 8px; + border: 1px solid color-mix(in srgb, var(--bpm-border) 78%, transparent); + border-radius: var(--bpm-radius-sm); + background: color-mix(in srgb, var(--bpm-muted) 44%, var(--bpm-card)); +} + +.manager-tag-editor__chip-slot--preview { + max-width: 220px; +} + +.manager-tag-editor__chip.manager-tag { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.manager-tag-editor__id { + align-self: center; +} + +.manager-tag-editor__meta { + padding-left: 1px; +} + +.manager-tag-editor__add-trigger.setting-item { + display: grid; + grid-template-columns: minmax(0, 1fr) var(--bpm-control-size); + column-gap: 14px; + align-items: center; + padding: 11px 12px 11px 14px; +} + +.manager-tag-editor__add-trigger>.setting-item-info { + min-width: 0; +} + +.manager-tag-editor__add-trigger .setting-item-name { + color: var(--text-normal); + font-size: 13px; + font-weight: 700; + line-height: 1.25; +} + +.manager-tag-editor__add-trigger .setting-item-description { + margin-top: 3px; + color: var(--text-muted); + font-size: 12px; + line-height: 1.35; +} + +.manager-tag-editor__add-trigger>.setting-item-control { + display: flex; + justify-content: flex-end; + min-width: 0; +} + +.manager-tag-editor__create-panel { + padding: 14px; +} + +.manager-tag-editor__create-preview { + padding: 10px 11px; +} + +.manager-tag-editor__create-preview-main { + gap: 10px; +} + +.manager-tag-editor__create-form { + grid-template-columns: 76px minmax(0, 1fr) minmax(0, 1fr); + gap: 10px; +} + +@media (max-width: 640px) { + .manager-tag-editor__item.setting-item, + .manager-tag-editor__item.is-editing, + .manager-tag-editor__add-trigger.setting-item { + display: flex; + gap: 10px; + padding: 12px; + } + + .manager-tag-editor__item>.setting-item-control, + .manager-tag-editor__item.is-editing>.setting-item-control, + .manager-tag-editor__add-trigger>.setting-item-control { + display: flex; + justify-content: flex-start; + width: 100%; + } + + .manager-tag-editor__preview-line { + grid-template-columns: minmax(0, 1fr); + gap: 7px; + } + + .manager-tag-editor__chip-slot { + width: 100%; + min-width: 0; + max-width: none; + } +}