mirror of
https://github.com/devonthesofa/obsidian-note-status.git
synced 2026-07-22 05:45:04 +00:00
feat: redesign status hover popup with per-tag groups and accurate data feed
This commit is contained in:
parent
298deddf9d
commit
45e2049f7b
9 changed files with 170 additions and 56 deletions
|
|
@ -10,8 +10,12 @@ import {
|
|||
|
||||
type Props = {
|
||||
statuses: GroupedStatuses;
|
||||
onMouseEnter: (statuses: GroupedStatuses) => void;
|
||||
onMouseLeave: (statuses: GroupedStatuses) => void;
|
||||
defaultTagName: string;
|
||||
onMouseEnter: (payload: {
|
||||
statuses: GroupedStatuses;
|
||||
defaultTagName: string;
|
||||
}) => void;
|
||||
onMouseLeave: () => void;
|
||||
hideUnknownStatus?: boolean;
|
||||
unknownStatusConfig?: {
|
||||
icon: string;
|
||||
|
|
@ -25,6 +29,7 @@ type Props = {
|
|||
export const FileExplorerIcon: FC<Props> = memo(
|
||||
({
|
||||
statuses,
|
||||
defaultTagName,
|
||||
onMouseLeave,
|
||||
onMouseEnter,
|
||||
hideUnknownStatus,
|
||||
|
|
@ -60,8 +65,10 @@ export const FileExplorerIcon: FC<Props> = memo(
|
|||
color={color}
|
||||
iconColorMode={iconColorMode}
|
||||
iconFrameMode={iconFrameMode}
|
||||
onMouseEnter={() => onMouseEnter({})}
|
||||
onMouseLeave={() => onMouseLeave({})}
|
||||
onMouseEnter={() =>
|
||||
onMouseEnter({ statuses: {}, defaultTagName })
|
||||
}
|
||||
onMouseLeave={onMouseLeave}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -85,8 +92,8 @@ export const FileExplorerIcon: FC<Props> = memo(
|
|||
count={totalStatuses}
|
||||
iconFrameMode={iconFrameMode}
|
||||
iconColorMode={iconColorMode}
|
||||
onMouseEnter={() => onMouseEnter(statuses)}
|
||||
onMouseLeave={() => onMouseLeave(statuses)}
|
||||
onMouseEnter={() => onMouseEnter({ statuses, defaultTagName })}
|
||||
onMouseLeave={onMouseLeave}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,11 +4,17 @@ import { StatusDisplay } from "../atoms/StatusDisplay";
|
|||
|
||||
export interface Props {
|
||||
statuses: GroupedStatuses;
|
||||
defaultTagName: string;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const StatusFileInfoPopup: React.FC<Props> = ({ statuses }) => {
|
||||
const statusEntries = Object.entries(statuses);
|
||||
export const StatusFileInfoPopup: React.FC<Props> = ({
|
||||
statuses,
|
||||
defaultTagName,
|
||||
}) => {
|
||||
const statusEntries = Object.entries(statuses).filter(
|
||||
([, statusList]) => statusList.length > 0,
|
||||
);
|
||||
|
||||
if (statusEntries.length === 0) {
|
||||
return (
|
||||
|
|
@ -27,40 +33,39 @@ export const StatusFileInfoPopup: React.FC<Props> = ({ statuses }) => {
|
|||
</div>
|
||||
|
||||
<div className="status-popup-content">
|
||||
{statusEntries.map(([groupName, statusList]) => (
|
||||
<div key={groupName} className="status-group">
|
||||
<div className="status-group__header">
|
||||
<span className="status-group__name">
|
||||
{groupName.toLowerCase()}
|
||||
</span>
|
||||
<span className="status-group__count">
|
||||
{statusList.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="status-group__items">
|
||||
{statusList.map((status, index) => (
|
||||
<div
|
||||
key={`${groupName}-${index}`}
|
||||
className="status-item"
|
||||
>
|
||||
<StatusDisplay
|
||||
status={status}
|
||||
variant="badge"
|
||||
/>
|
||||
{status.description && (
|
||||
<div
|
||||
className="status-description"
|
||||
title={status.description}
|
||||
>
|
||||
{status.description}
|
||||
</div>
|
||||
)}
|
||||
<div className="status-tag-grid">
|
||||
{statusEntries.map(([groupName, statusList]) => {
|
||||
const isDefault = groupName === defaultTagName;
|
||||
return (
|
||||
<div key={groupName} className="status-tag-card">
|
||||
<div className="status-tag-card__header">
|
||||
<div className="status-tag-card__title">
|
||||
<span className="status-tag-card__label">
|
||||
{groupName}
|
||||
</span>
|
||||
{isDefault && (
|
||||
<span className="status-tag-card__badge">
|
||||
Default tag
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="status-tag-card__count">
|
||||
{statusList.length}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="status-tag-card__statuses">
|
||||
{statusList.map((status, index) => (
|
||||
<StatusDisplay
|
||||
key={`${groupName}-${index}`}
|
||||
status={status}
|
||||
variant="badge"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { StatusIcon } from "@/components/atoms/StatusIcon";
|
|||
|
||||
interface EditorToolbarButtonProps {
|
||||
statuses: GroupedStatuses;
|
||||
defaultTagName: string;
|
||||
onClick: () => void;
|
||||
unknownStatusConfig: {
|
||||
icon: string;
|
||||
|
|
@ -14,7 +15,7 @@ interface EditorToolbarButtonProps {
|
|||
}
|
||||
|
||||
export const EditorToolbarButton: FC<EditorToolbarButtonProps> = memo(
|
||||
({ statuses, onClick, unknownStatusConfig }) => {
|
||||
({ statuses, defaultTagName, onClick, unknownStatusConfig }) => {
|
||||
const statusEntries = Object.entries(statuses);
|
||||
const allStatuses = statusEntries.flatMap(
|
||||
([_, statusList]) => statusList,
|
||||
|
|
@ -22,7 +23,7 @@ export const EditorToolbarButton: FC<EditorToolbarButtonProps> = memo(
|
|||
const totalStatuses = allStatuses.length;
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
StatusesInfoPopup.open(statuses);
|
||||
StatusesInfoPopup.open({ statuses, defaultTagName });
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
|
|
|
|||
|
|
@ -318,6 +318,28 @@ export class NoteStatusService extends BaseNoteStatusService {
|
|||
return this.buildStatusesArray(identifiers);
|
||||
}
|
||||
|
||||
public getStatusesByAllKeys(): GroupedStatuses {
|
||||
const keys = getFrontmatterKeysForFile(this.file);
|
||||
const grouped: GroupedStatuses = {};
|
||||
let hasStatuses = false;
|
||||
|
||||
keys.forEach((key) => {
|
||||
const statuses = this.getStatusesForKey(key);
|
||||
if (!statuses.length) {
|
||||
return;
|
||||
}
|
||||
grouped[key] = statuses;
|
||||
hasStatuses = true;
|
||||
});
|
||||
|
||||
if (!hasStatuses) {
|
||||
grouped[settingsService.settings.tagPrefix] =
|
||||
this.statuses[settingsService.settings.tagPrefix] ?? [];
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
async removeStatus(
|
||||
frontmatterTagName: string,
|
||||
status: NoteStatus,
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export class FileExplorerIntegration implements IElementProcessor {
|
|||
}
|
||||
|
||||
const noteStatusService = this.getFileNoteStatusService(dataPath);
|
||||
const statuses = noteStatusService?.statuses ?? null;
|
||||
const statuses = noteStatusService?.getStatusesByAllKeys() ?? {};
|
||||
const primaryStatus = getPrimaryStatus(statuses);
|
||||
const hasStatus = Boolean(primaryStatus);
|
||||
const fallbackColor = getUnknownStatusColor();
|
||||
|
|
@ -138,7 +138,11 @@ export class FileExplorerIntegration implements IElementProcessor {
|
|||
this.applyLeftBorder(navItem, statusColor, hasStatus);
|
||||
|
||||
if (noteStatusService) {
|
||||
this.render(textEl, noteStatusService.statuses);
|
||||
this.render(
|
||||
textEl,
|
||||
statuses,
|
||||
settingsService.settings.tagPrefix,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -151,7 +155,11 @@ export class FileExplorerIntegration implements IElementProcessor {
|
|||
};
|
||||
}
|
||||
|
||||
render(element: Element, statuses: GroupedStatuses): void {
|
||||
render(
|
||||
element: Element,
|
||||
statuses: GroupedStatuses,
|
||||
defaultTagName: string,
|
||||
): void {
|
||||
// Remove existing icon
|
||||
const existingIcon = element.querySelector(`.${this.ICON_CLASS}`);
|
||||
if (existingIcon) {
|
||||
|
|
@ -174,7 +182,8 @@ export class FileExplorerIntegration implements IElementProcessor {
|
|||
root.render(
|
||||
<FileExplorerIcon
|
||||
statuses={statuses}
|
||||
onMouseEnter={(s) => this.openModalInfo(s)}
|
||||
defaultTagName={defaultTagName}
|
||||
onMouseEnter={this.openModalInfo}
|
||||
onMouseLeave={this.closeModalInfo}
|
||||
hideUnknownStatus={
|
||||
settingsService.settings.hideUnknownStatusInExplorer
|
||||
|
|
@ -200,15 +209,21 @@ export class FileExplorerIntegration implements IElementProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
private openModalInfo(statuses: GroupedStatuses) {
|
||||
private openModalInfo = ({
|
||||
statuses,
|
||||
defaultTagName,
|
||||
}: {
|
||||
statuses: GroupedStatuses;
|
||||
defaultTagName: string;
|
||||
}) => {
|
||||
if (!this.plugin) {
|
||||
return;
|
||||
}
|
||||
StatusesInfoPopup.open(statuses);
|
||||
}
|
||||
private closeModalInfo() {
|
||||
StatusesInfoPopup.open({ statuses, defaultTagName });
|
||||
};
|
||||
private closeModalInfo = () => {
|
||||
StatusesInfoPopup.close();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Cleanup integration and unsubscribe from events
|
||||
|
|
|
|||
|
|
@ -8,10 +8,17 @@ export class StatusesInfoPopup {
|
|||
private static instance: StatusesInfoPopup | null = null;
|
||||
private element: HTMLElement | null = null;
|
||||
private statuses: GroupedStatuses;
|
||||
private defaultTagName: string;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static open(statuses: GroupedStatuses) {
|
||||
static open({
|
||||
statuses,
|
||||
defaultTagName,
|
||||
}: {
|
||||
statuses: GroupedStatuses;
|
||||
defaultTagName: string;
|
||||
}) {
|
||||
// Always ensure previous instance is cleaned up before showing or when disabled
|
||||
StatusesInfoPopup.close();
|
||||
|
||||
|
|
@ -22,6 +29,7 @@ export class StatusesInfoPopup {
|
|||
}
|
||||
StatusesInfoPopup.instance = new StatusesInfoPopup();
|
||||
StatusesInfoPopup.instance.statuses = statuses;
|
||||
StatusesInfoPopup.instance.defaultTagName = defaultTagName;
|
||||
StatusesInfoPopup.instance.show();
|
||||
}
|
||||
|
||||
|
|
@ -38,7 +46,12 @@ export class StatusesInfoPopup {
|
|||
document.body.appendChild(this.element);
|
||||
|
||||
this.root = createRoot(this.element);
|
||||
this.root.render(<StatusFileInfoPopup statuses={this.statuses} />);
|
||||
this.root.render(
|
||||
<StatusFileInfoPopup
|
||||
statuses={this.statuses}
|
||||
defaultTagName={this.defaultTagName}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
private destroy() {
|
||||
|
|
|
|||
|
|
@ -338,9 +338,12 @@ export class EditorToolbarIntegration {
|
|||
}
|
||||
|
||||
private renderButtonForLeaf(leafButton: LeafButton): void {
|
||||
const statuses =
|
||||
leafButton.noteStatusService?.getStatusesByAllKeys() || {};
|
||||
leafButton.root.render(
|
||||
<EditorToolbarButton
|
||||
statuses={leafButton.noteStatusService?.statuses || {}}
|
||||
statuses={statuses}
|
||||
defaultTagName={settingsService.settings.tagPrefix}
|
||||
onClick={() => this.openStatusModal(leafButton.leaf)}
|
||||
unknownStatusConfig={this.getUnknownStatusConfig()}
|
||||
/>,
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -172,6 +172,54 @@
|
|||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
.status-tag-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-4-2);
|
||||
}
|
||||
.status-tag-card {
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-m);
|
||||
padding: var(--size-4-2);
|
||||
background: var(--background-primary-alt);
|
||||
box-shadow: var(--shadow-xs);
|
||||
}
|
||||
.status-tag-card__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--size-2-2);
|
||||
}
|
||||
.status-tag-card__title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-2-1);
|
||||
}
|
||||
.status-tag-card__label {
|
||||
font-weight: var(--font-semibold);
|
||||
text-transform: none;
|
||||
}
|
||||
.status-tag-card__badge {
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
background: var(--background-modifier-border);
|
||||
padding: 2px 6px;
|
||||
border-radius: var(--radius-s);
|
||||
width: fit-content;
|
||||
}
|
||||
.status-tag-card__count {
|
||||
font-size: var(--font-ui-small);
|
||||
color: var(--text-muted);
|
||||
background: var(--background-modifier-border);
|
||||
padding: 2px 10px;
|
||||
border-radius: var(--radius-s);
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
.status-tag-card__statuses {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--size-2-2);
|
||||
}
|
||||
|
||||
/* Status Groups in Popup */
|
||||
.status-group {
|
||||
|
|
|
|||
Loading…
Reference in a new issue