tcyeee_obsidian-image-cluster/styles.css
tcyeee 4ff21baf2c feat: add per-image exclude/delete actions and precise multi-image drag-out
- Show exclude/delete buttons on hover for images inside a group; deleting an
  image still referenced elsewhere in the vault now asks for confirmation
  before touching the original file.
- Track each image's own start/end offset within its line (getImageMatches)
  so dragging one image out of a line with multiple images no longer removes
  or disturbs its neighbors.
- Group the image settings panel into labeled sections (Canvas size /
  Appearance) for clarity.

Also satisfies Obsidian's plugin lint pass: bump tsconfig `lib` to ES2019
(Object.entries/Array.flat were silently type-checked only via @types/node's
hidden lib reference, not the project's own lib config), switch remaining
bare `document` event listeners to `activeDocument` for popout-window
compatibility, and replace `el.closest(...) as HTMLElement | null` with the
explicit `el.closest<HTMLElement>(...)` generic form (the `as` cast was
itself supplying the contextual type that made the assertion look
redundant to `no-unnecessary-type-assertion`).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 17:27:13 +08:00

588 lines
No EOL
19 KiB
CSS
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

.plugin-image-wrapper {
display: block;
overflow: hidden;
height: var(--plugin-image-size, 220px);
width: var(--plugin-image-size, 220px);
border-radius: var(--plugin-image-radius, 10px);
position: relative;
}
/* 拖拽排序 */
.plugin-image-sortable { cursor: grab; }
.plugin-image-dragging { opacity: 0.35; }
.plugin-image-drag-before { box-shadow: -3px 0 0 0 var(--interactive-accent, #5677c0); }
.plugin-image-drag-after { box-shadow: 3px 0 0 0 var(--interactive-accent, #5677c0); }
/* 从图片组拖出图片时,编辑器目标行的插入位置提示(上方/下方) */
.plugin-image-dragline-before { box-shadow: 0 -3px 0 0 var(--interactive-accent, #5677c0); }
.plugin-image-dragline-after { box-shadow: 0 3px 0 0 var(--interactive-accent, #5677c0); }
.plugin-image {
display: block;
object-fit: cover;
height: 100%;
width: 100%;
border-radius: var(--plugin-image-radius, 10px);
position: relative;
}
/* 图片加载失败时显示的占位错误块 */
.plugin-image-error {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 8px;
/* 使用与图片相同的尺寸变量,若未设置则退回默认值 */
width: var(--plugin-image-size, 220px);
height: var(--plugin-image-size, 220px);
padding: 12px;
border: 1px solid var(--color-red, #ff6b6b);
border-radius: 6px;
background-color: rgba(var(--color-red-rgb, 216, 0, 12), 0.1);
color: var(--color-red, #d8000c);
box-sizing: border-box;
}
.plugin-image-shadow {
box-shadow: 0 1px 2px rgba(164, 163, 163, 0.348);
}
.plugin-image-border {
border: 1px solid var(--background-modifier-border, #e1e1e1);
}
.plugin-image-wrapper.plugin-image-hidden::after,
.plugin-image.plugin-image-hidden::after {
content: "";
position: absolute;
pointer-events: none;
user-select: none;
inset: 0;
backdrop-filter: blur(50px);
background-color: rgba(0, 0, 0, 0.3);
}
/* 当图片被标记为 hidden 时,彻底禁止点击,避免触发大图预览 */
.plugin-image-wrapper.plugin-image-hidden {
pointer-events: none;
}
.plugin-image-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
opacity: 0;
transition: opacity 0.3s ease;
}
.plugin-image-nav-btn {
width: 48px;
height: 48px;
border-radius: 50%;
background-color:rgba(255, 255, 255, 0.8);
border: none;
font-size: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 10001;
box-shadow: 0 2px 12px rgba(0,0,0,0.16);
transition: background 0.15s, color 0.15s;
}
.plugin-image-nav-btn:active {
background: rgba(255,255,255,0.7);
}
.plugin-image-nav-btn:disabled {
background: rgba(180,180,180,0.5);
color: #aaaaaa;
cursor: not-allowed;
}
.plugin-image-nav-btn-prev { left:32px; }
.plugin-image-nav-btn-next { right:32px; }
.plugin-image-overlay-visible {
opacity: 1;
}
.plugin-image-large {
max-width: 90%;
max-height: 90%;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
border-radius: var(--plugin-image-radius, 10px);
transition: transform 0.2s;
cursor: grab;
/* 平移/缩放由 JS 注入 CSS 变量驱动 */
transform: translate(var(--plugin-preview-tx, 0px), var(--plugin-preview-ty, 0px)) scale(var(--plugin-preview-scale, 1));
}
/* 大图切换动画:时长须与 processor.ts 中的 ANIM_MS220保持一致 */
.plugin-image-large--switching {
transition: transform 220ms ease, opacity 220ms ease;
}
.plugin-image-large--faded {
opacity: 0;
}
/* 切换图源的瞬移阶段:禁用 transition */
.plugin-image-large--no-anim {
transition: none;
}
/* 拖拽中:禁用 transition 防止每帧 transform 更新触发动画 */
.plugin-image-large--dragging {
transition: none;
cursor: grabbing;
}
/* 长截图(高宽比 > 2预览模式以宽度为基准展示overlay 可垂直滚动 */
.plugin-image-overlay--scrollable {
align-items: flex-start;
overflow-y: auto;
padding: 5vh 0;
overflow-anchor: none; /* 禁止浏览器在缩放时自动调整滚动位置 */
}
.plugin-image-large--tall {
max-height: none;
max-width: 70%;
transform-origin: top center; /* 缩放锚点固定在顶部,防止放大时画面向中间漂移 */
}
.plugin-image-border-large {
border: 1px solid var(--background-modifier-border, #cccccc);
}
.plugin-image-more-wrapper {
position: relative;
}
.workspace .plugin-image-row-hidden {
display: none;
}
.workspace .plugin-image-more-mask {
pointer-events: all;
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.45);
color: #ffffff;
font-size: 18px;
font-weight: 600;
border-radius: inherit;
cursor: pointer;
z-index: 2;
}
.plugin-image-more-text {
padding: 4px 10px;
font-size: 20px;
/* border-radius: 999px; */
/* background: rgba(0, 0, 0, 0.55); */
/* box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35); */
}
.plugin-image-container {
display: flex;
flex-wrap: wrap;
padding: 8px 0;
gap: var(--plugin-container-gap, 10px);
margin-left: var(--plugin-container-margin-left, 0);
position: relative; /* 保证setting按钮绝对定位参照容器 */
cursor: pointer; /* 覆盖编辑模式下 CodeMirror 的文本光标 */
border-radius: 6px;
transition: background-color 0.15s;
}
/* 独立图片拖拽悬停在图片组上方时,整体高亮提示这里可以放置 */
.plugin-image-container--drag-target {
background-color: var(--background-modifier-hover);
}
/* 阅读模式Obsidian 的区块流会在代码块上方多出约一行的区块间距(约 16px
而 Live Preview 中图片块紧贴上方文字(间距仅来自 container 自身的 padding-top
以 Live Preview 为准:用负 margin 抵消阅读模式多出的上间距,使两种模式上边距一致。
.markdown-preview-view 只匹配阅读视图Live Preview 位于 .markdown-source-view故不会影响编辑模式。 */
.markdown-preview-view .block-language-imgs {
margin-top: -16px;
}
/* setting操作面板仅样式
position: fixed 绕过 CodeMirror overflow 裁剪top/right 由 JS 动态计算注入 CSS 变量 */
.plugin-image-setting-panel {
position: fixed;
top: var(--plugin-panel-top, 0);
right: var(--plugin-panel-right, 0);
min-width: 250px;
display: none;
background: var(--background-primary);
border-radius: 16px;
box-shadow: 0 6px 24px rgba(0,0,0,0.13);
padding: 20px 20px 18px 20px;
z-index: 110;
font-size: 15px;
color: var(--text-normal);
flex-direction: column;
gap: 22px;
}
.plugin-image-setting-panel.plugin-image-setting-panel--open {
display: flex;
}
.plugin-image-panel-safe-zone {
position: absolute;
top: -12px;
left: 0;
right: 0;
height: 12px;
}
/* 面板分组:标题 + 内容 */
.plugin-image-setting-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.plugin-image-setting-section-title {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text-faint, var(--text-muted));
}
/* 勾选项列表 */
.plugin-image-setting-checkbox-list {
display: flex;
flex-direction: column;
gap: 14px;
}
.plugin-image-setting-checkbox {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
font-size: 14px;
color: var(--text-normal);
}
.plugin-image-setting-panel input[type="checkbox"] {
accent-color: var(--interactive-accent);
}
.plugin-image-setting-checkbox-label {
user-select: none;
}
.plugin-image-setting-switch {
position: relative;
width: 40px;
height: 22px;
flex-shrink: 0;
}
.plugin-image-setting-switch-input {
position: absolute;
inset: 0;
opacity: 0;
margin: 0;
cursor: pointer;
}
.plugin-image-setting-switch-track {
position: absolute;
inset: 0;
border-radius: 999px;
background: var(--background-modifier-border-hover, rgba(210, 214, 222, 0.9));
box-shadow: 0 0 0 1px rgba(160, 170, 190, 0.5) inset;
transition: background 0.18s ease;
}
.plugin-image-setting-switch-track::before {
content: "";
position: absolute;
top: 2px;
left: 2px;
width: 18px;
height: 18px;
border-radius: 999px;
background: var(--toggle-thumb-color, #ffffff);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
transition: transform 0.18s ease;
}
.plugin-image-setting-switch-input:checked + .plugin-image-setting-switch-track {
background: var(--interactive-accent, #5677c0);
box-shadow: 0 0 0 1px rgba(40, 90, 180, 0.35);
}
.plugin-image-setting-switch-input:checked + .plugin-image-setting-switch-track::before {
transform: translateX(18px);
}
.plugin-image-setting-size-group {
position: relative;
display: flex;
align-items: center;
width: 100%;
height: 40px;
box-sizing: border-box;
padding: 3px;
border-radius: 12px;
background: var(--background-secondary, rgba(240, 242, 247, 0.9));
gap: 0;
}
.plugin-image-setting-size-slider {
position: absolute;
top: 3px;
bottom: 3px;
left: 3px;
width: calc((100% - 6px) / 3);
border-radius: 9px;
background: var(--interactive-accent, #5677c0);
box-shadow: 0 0 0 1px rgba(40, 90, 180, 0.35);
transition: transform 0.18s ease;
pointer-events: none;
}
.plugin-image-setting-size-group[data-size="small"] .plugin-image-setting-size-slider {
transform: translateX(0%);
}
.plugin-image-setting-size-group[data-size="medium"] .plugin-image-setting-size-slider {
transform: translateX(100%);
}
.plugin-image-setting-size-group[data-size="large"] .plugin-image-setting-size-slider {
transform: translateX(200%);
}
.plugin-image-setting-size-radio {
position: relative;
display: flex;
align-items: center;
justify-content: center;
flex: 1 1 0;
height: 100%;
margin: 0;
padding: 0;
cursor: pointer;
font-family: inherit;
font-size: 13px;
font-weight: 500;
line-height: 1;
letter-spacing: normal;
color: var(--text-muted);
z-index: 1;
box-sizing: border-box;
}
.plugin-image-setting-size-radio-input {
position: absolute;
width: 0;
height: 0;
margin: 0;
padding: 0;
opacity: 0;
pointer-events: none;
}
.plugin-image-setting-size-radio-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
min-width: 14px;
padding: 0;
margin: 0;
font-size: inherit;
line-height: 1;
text-align: center;
white-space: nowrap;
border-radius: 999px;
}
.plugin-image-setting-size-radio-input:checked + .plugin-image-setting-size-radio-text {
color: var(--text-on-accent, #ffffff81);
font-weight: 600;
}
/* ── setting 外层包装 ── */
/* 阅读模式wrapper 在 .plugin-image-container 内,绝对定位到右上角 */
.plugin-image-container .plugin-image-setting-outer {
position: absolute;
top: 8px;
right: 4px;
z-index: 10;
}
/* 编辑模式Live Previewwrapper 在 .cm-preview-code-block 内,
紧贴 edit-block-button 左侧;使用与原生按钮相同的 top 值 */
.cm-preview-code-block .plugin-image-setting-outer {
position: absolute;
top: 5px;
right: calc(var(--icon-size, 24px) + 20px);
z-index: 10;
}
/* ── setting 按钮 ── */
/* 默认隐藏opacity 方式,不与 clickable-icon 的 display 冲突) */
.plugin-image-setting-btn-container {
opacity: 0;
pointer-events: none;
transition: opacity 0.1s;
}
/* 阅读模式:鼠标悬停 container 时,由 JS 添加 --visible 类 */
.plugin-image-container:hover .plugin-image-setting-btn-container--visible {
opacity: 1;
pointer-events: auto;
}
/* 编辑模式:鼠标悬停 code block 时,由 CSS 直接控制 */
.cm-preview-code-block:hover .plugin-image-setting-btn-container {
opacity: 1;
pointer-events: auto;
}
/* ── 转图片组按钮Live Preview图片 embed 获得焦点时出现) ──
注入到图片 embed 内,与原生 edit-block-button 一起放进新的 flex 容器,
占据原本 edit-block-button 的绝对定位位置。
Live Preview 下的图片 embed 是 div.image-embed并非 .cm-embed-block
Obsidian 原生用 `div.image-embed:focus-within` 触发 edit-block-button 显示,
本按钮沿用同一触发条件,显示/隐藏完全交给 CSS 控制。 */
.plugin-image-hover-group-wrap {
position: absolute;
top: var(--size-2-2);
inset-inline-end: var(--size-2-2);
display: flex;
align-items: center;
gap: 5px;
}
/* edit-block-button 现在是 wrap 内的 flex 子项,取消其自身的绝对定位。
原生规则 `.markdown-source-view.mod-cm6 .edit-block-button` 有 3 个类选择器specificity 0,3,0
只带 `.markdown-source-view.mod-cm6` 祖先前缀去压制在这个具体的祖先组合下才生效;
为了不依赖某个具体的祖先类链(万一某个 Live Preview 宿主上下文没有这两个祖先类,
这条规则就会失效edit-block-button 又会变回 absolute 定位盖住 Group 按钮),
额外叠加一条只依赖自身类名、把类选择器重复三次人为提高 specificity 到 0,4,0 的规则,
保证在任何祖先结构下都能稳定压过原生规则,且不需要用 !important。 */
.markdown-source-view.mod-cm6 .plugin-image-hover-group-wrap .edit-block-button,
.plugin-image-hover-group-wrap.plugin-image-hover-group-wrap.plugin-image-hover-group-wrap .edit-block-button {
position: static;
}
.plugin-image-hover-group-btn {
display: flex;
align-items: center;
/* 与 edit-block-button 相同的 padding保证尺寸一致纯图标无文字 */
padding: var(--size-2-2) var(--size-2-3);
/* 全局 `button {}` 规则会设 height: var(--input-height)
盖过纯内容撑开的高度,导致比 div 形式的 edit-block-button 更高,
这里显式重置为 auto让高度完全由 padding + 图标撑开 */
height: auto;
border-radius: var(--radius-s);
cursor: var(--cursor);
opacity: 0;
font: inherit;
}
/* <button> 元素会被全局的 `button:not(.clickable-icon)` / `button:hover` /
`button:focus-visible` 命中specificity 0,1,11 个元素 + 1 个类/伪类),
必须借助外层 wrap 的类名叠加一层类选择器0,2,0才能压过它们否则原生按钮
自带的背景色、box-shadow「边框」、focus 光圈、以及 `color: var(--text-color)`
(比 var(--text-muted) 深很多)都会透出来,和纯 div 的 edit-block-button 观感不一致。
color 之前误留在上面 (0,1,0) 的规则里,压不过 (0,1,1) 的原生规则,
导致图标一直显示成 --text-color 而不是 --text-muted这里一并挪过来。 */
.plugin-image-hover-group-wrap .plugin-image-hover-group-btn {
appearance: none;
-webkit-appearance: none;
background-color: transparent;
box-shadow: none;
border: none;
outline: none;
color: var(--text-muted);
/* 确保始终盖在 edit-block-buttonfocus 时 z-index: 2之上不被其抢走点击 */
position: relative;
z-index: 3;
}
/* 与原生 edit-block-button 完全一致的显示/配色规则:
仅在图片 embed 被选中(:focus-within时才出现悬停不再单独触发显示
与右侧原生按钮的显示条件保持一致。 */
body:not(.is-mobile) div.image-embed:focus-within .plugin-image-hover-group-wrap .plugin-image-hover-group-btn {
background-color: var(--background-primary);
opacity: 1;
}
@media (hover: hover) {
body:not(.is-mobile) div.image-embed:focus-within .plugin-image-hover-group-wrap .plugin-image-hover-group-btn:hover {
background-color: var(--background-secondary);
}
}
.plugin-image-hover-group-btn-icon {
display: inline-flex;
}
/* ── 图片组内单张图片的操作按钮(排除 / 删除,悬停缩略图时出现) ──
悬停时仅图片下半部分变暗,呈现从下到上的渐变效果(而非整张图变暗),
两个按钮横向居中、纵向靠下,叠在蒙层之上;蒙层与按钮均带淡入 + 位移动画。 */
.plugin-image-item-overlay {
position: absolute;
inset: 0;
background: linear-gradient(to top, rgba(0, 0, 0, 0.55) 0%, rgba(0, 0, 0, 0.25) 35%, rgba(0, 0, 0, 0) 65%);
border-radius: inherit;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease;
z-index: 3;
}
.plugin-image-wrapper:hover .plugin-image-item-overlay {
opacity: 1;
}
.plugin-image-item-actions {
position: absolute;
left: 50%;
bottom: 8px;
transform: translate(-50%, 6px);
display: flex;
gap: 8px;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 4;
}
.plugin-image-wrapper:hover .plugin-image-item-actions {
opacity: 1;
pointer-events: auto;
transform: translate(-50%, 0);
}
.plugin-image-item-action-btn {
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: var(--radius-s);
background-color: var(--background-primary);
color: var(--text-muted);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
}
.plugin-image-item-action-btn:hover {
color: var(--text-normal);
background-color: var(--background-secondary);
}
.plugin-image-item-action-btn--danger:hover {
color: var(--text-error, #e93147);
}
.icon--error-picture {
display: inline-block;
width: 20px;
height: 20px;
background-repeat: no-repeat;
background-size: 100% 100%;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Cg fill='none'%3E%3Cpath fill='%23000' d='M44 23.9941C44 22.8896 43.1046 21.9941 42 21.9941C40.8954 21.9941 40 22.8896 40 23.9941H44ZM24 7.99414C25.1046 7.99414 26 7.09871 26 5.99414C26 4.88957 25.1046 3.99414 24 3.99414V7.99414ZM39 39.9941H9V43.9941H39V39.9941ZM8 38.9941V8.99414H4V38.9941H8ZM40 23.9941V38.9941H44V23.9941H40ZM9 7.99414H24V3.99414H9V7.99414ZM9 39.9941C8.44772 39.9941 8 39.5464 8 38.9941H4C4 41.7556 6.23857 43.9941 9 43.9941V39.9941ZM39 43.9941C41.7614 43.9941 44 41.7556 44 38.9941H40C40 39.5464 39.5523 39.9941 39 39.9941V43.9941ZM8 8.99414C8 8.44186 8.44771 7.99414 9 7.99414V3.99414C6.23858 3.99414 4 6.23272 4 8.99414H8Z'/%3E%3Cpath stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='4' d='M6 35L16.6931 25.198C17.4389 24.5143 18.5779 24.4953 19.3461 25.1538L32 36'/%3E%3Cpath stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='4' d='M28 31L32.7735 26.2265C33.4772 25.5228 34.5914 25.4436 35.3877 26.0408L42 31'/%3E%3Cpath stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='4' d='M33 7L41 15'/%3E%3Cpath stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='4' d='M41 7L33 15'/%3E%3C/g%3E%3C/svg%3E");
}