mirror of
https://github.com/devonthesofa/obsidian-note-status.git
synced 2026-07-22 05:45:04 +00:00
Merge pull request #72 from devonthesofa/fix/status-picker-colors
[feature] Unify template selector icon styling with file explorer
This commit is contained in:
commit
e4eb51108b
9 changed files with 1684 additions and 62 deletions
|
|
@ -11,6 +11,8 @@ export interface Props {
|
|||
filesQuantity: number;
|
||||
availableStatuses: NoteStatus[];
|
||||
templates: StatusTemplate[];
|
||||
iconFrameMode?: "always" | "never";
|
||||
iconColorMode?: "status" | "theme";
|
||||
onRemoveStatus: (
|
||||
frontmatterTagName: string,
|
||||
status: NoteStatus,
|
||||
|
|
@ -26,6 +28,8 @@ export const ChangeStatusModal: React.FC<Props> = ({
|
|||
filesQuantity,
|
||||
availableStatuses,
|
||||
templates,
|
||||
iconFrameMode = "never",
|
||||
iconColorMode = "status",
|
||||
onRemoveStatus,
|
||||
onSelectStatus,
|
||||
}) => {
|
||||
|
|
@ -57,6 +61,8 @@ export const ChangeStatusModal: React.FC<Props> = ({
|
|||
availableStatuses={availableStatuses}
|
||||
currentStatuses={statusList}
|
||||
templates={templates}
|
||||
iconFrameMode={iconFrameMode}
|
||||
iconColorMode={iconColorMode}
|
||||
onSelectedState={handleSelectedState}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ export interface Props {
|
|||
currentStatuses: NoteStatus[];
|
||||
availableStatuses: NoteStatus[];
|
||||
templates: StatusTemplate[];
|
||||
iconFrameMode: "always" | "never";
|
||||
iconColorMode: "status" | "theme";
|
||||
onSelectedState: (
|
||||
frontmatterTagName: string,
|
||||
status: NoteStatus,
|
||||
|
|
@ -24,6 +26,8 @@ export const StatusSelectorGroup: React.FC<Props> = ({
|
|||
availableStatuses,
|
||||
templates,
|
||||
frontmatterTagName,
|
||||
iconFrameMode,
|
||||
iconColorMode,
|
||||
onSelectedState,
|
||||
}) => {
|
||||
const TEMPLATE_ALL_VIEW = "__all__";
|
||||
|
|
@ -201,6 +205,8 @@ export const StatusSelectorGroup: React.FC<Props> = ({
|
|||
availableStatuses={filteredStatuses}
|
||||
currentStatuses={currentStatuses}
|
||||
focusedIndex={focusedIndex}
|
||||
iconFrameMode={iconFrameMode}
|
||||
iconColorMode={iconColorMode}
|
||||
onToggleStatus={(status, selected) =>
|
||||
selected
|
||||
? handleSelectStatus(status)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { StatusIconPreview } from "@/components/atoms/StatusIconPreview";
|
||||
import { GroupedStatuses } from "@/types/noteStatus";
|
||||
import React, { FC, memo } from "react";
|
||||
|
||||
|
|
@ -36,38 +37,22 @@ export const FileExplorerIcon: FC<Props> = memo(
|
|||
(color && color.trim()) || "var(--text-accent)";
|
||||
|
||||
if (totalStatuses === 0) {
|
||||
// If hideUnknownStatus is enabled, don't show anything for files without status
|
||||
if (hideUnknownStatus) return null;
|
||||
|
||||
// Use config passed from integration, with fallbacks
|
||||
const icon = unknownStatusConfig?.icon || "❓";
|
||||
const color = useStatusColors
|
||||
? unknownStatusConfig?.color?.trim() || "#8b949e"
|
||||
: undefined;
|
||||
|
||||
const shouldFrameUnknown = iconFrameMode === "always";
|
||||
|
||||
const unknownStyles: React.CSSProperties = {};
|
||||
if (color) {
|
||||
unknownStyles.color = color;
|
||||
}
|
||||
if (shouldFrameUnknown) {
|
||||
const frameColor = color || "currentColor";
|
||||
unknownStyles.boxShadow = `0 0 0 1px ${frameColor}`;
|
||||
unknownStyles.borderRadius = "var(--radius-s)";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="status-wrapper">
|
||||
<div
|
||||
className="status-minimal status-minimal--no-status"
|
||||
onMouseEnter={() => onMouseEnter({})}
|
||||
onMouseLeave={() => onMouseLeave({})}
|
||||
style={unknownStyles}
|
||||
>
|
||||
<span className="status-minimal__icon">{icon}</span>
|
||||
</div>
|
||||
</div>
|
||||
<StatusIconPreview
|
||||
icon={icon}
|
||||
color={color}
|
||||
iconColorMode={iconColorMode}
|
||||
iconFrameMode={iconFrameMode}
|
||||
onMouseEnter={() => onMouseEnter({})}
|
||||
onMouseLeave={() => onMouseLeave({})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -76,42 +61,17 @@ export const FileExplorerIcon: FC<Props> = memo(
|
|||
const iconColor = useStatusColors
|
||||
? getStatusColor(primaryStatus.color)
|
||||
: undefined;
|
||||
const shouldShowFrame = iconFrameMode === "always";
|
||||
const iconStyles: React.CSSProperties = {};
|
||||
if (iconColor) {
|
||||
iconStyles.color = iconColor;
|
||||
}
|
||||
if (shouldShowFrame) {
|
||||
const frameColor = iconColor || "currentColor";
|
||||
iconStyles.boxShadow = `0 0 0 1px ${frameColor}`;
|
||||
iconStyles.borderRadius = "var(--radius-s)";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="status-wrapper">
|
||||
<div
|
||||
className="status-minimal"
|
||||
onMouseEnter={() => onMouseEnter(statuses)}
|
||||
onMouseLeave={() => onMouseLeave(statuses)}
|
||||
style={iconStyles}
|
||||
>
|
||||
<span className="status-minimal__icon">
|
||||
{primaryStatus.icon}
|
||||
</span>
|
||||
{totalStatuses > 1 && (
|
||||
<span
|
||||
className="status-minimal__count"
|
||||
style={
|
||||
iconColor
|
||||
? { backgroundColor: iconColor }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{totalStatuses}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<StatusIconPreview
|
||||
icon={primaryStatus.icon}
|
||||
color={iconColor}
|
||||
count={totalStatuses}
|
||||
iconFrameMode={iconFrameMode}
|
||||
iconColorMode={iconColorMode}
|
||||
onMouseEnter={() => onMouseEnter(statuses)}
|
||||
onMouseLeave={() => onMouseLeave(statuses)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
91
components/atoms/StatusIconPreview.tsx
Normal file
91
components/atoms/StatusIconPreview.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import React, { CSSProperties, FC, memo } from "react";
|
||||
|
||||
type IconFrameMode = "always" | "never";
|
||||
type IconColorMode = "status" | "theme";
|
||||
|
||||
export interface StatusIconPreviewProps {
|
||||
icon?: string;
|
||||
color?: string;
|
||||
count?: number;
|
||||
iconFrameMode?: IconFrameMode;
|
||||
iconColorMode?: IconColorMode;
|
||||
withWrapper?: boolean;
|
||||
compact?: boolean;
|
||||
iconClassName?: string;
|
||||
wrapperClassName?: string;
|
||||
style?: CSSProperties;
|
||||
onMouseEnter?: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
onMouseLeave?: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
}
|
||||
|
||||
export const StatusIconPreview: FC<StatusIconPreviewProps> = memo(
|
||||
({
|
||||
icon,
|
||||
color,
|
||||
count,
|
||||
iconFrameMode = "never",
|
||||
iconColorMode = "status",
|
||||
withWrapper = true,
|
||||
compact = false,
|
||||
iconClassName = "",
|
||||
wrapperClassName = "",
|
||||
style,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
}) => {
|
||||
const iconDisplay = icon?.trim().length ? icon : "📝";
|
||||
const useStatusColor = iconColorMode === "status";
|
||||
const appliedColor = useStatusColor && color ? color.trim() : undefined;
|
||||
|
||||
const iconStyles: CSSProperties = {
|
||||
...style,
|
||||
};
|
||||
|
||||
if (appliedColor) {
|
||||
iconStyles.color = appliedColor;
|
||||
}
|
||||
|
||||
if (iconFrameMode === "always") {
|
||||
const frameColor = appliedColor || "currentColor";
|
||||
iconStyles.boxShadow = `0 0 0 1px ${frameColor}`;
|
||||
iconStyles.borderRadius = "var(--radius-s)";
|
||||
}
|
||||
|
||||
const iconContent = (
|
||||
<div
|
||||
className={`status-minimal${compact ? " status-minimal--compact" : ""}${iconClassName ? ` ${iconClassName}` : ""}`}
|
||||
style={iconStyles}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<span className="status-minimal__icon">{iconDisplay}</span>
|
||||
{count && count > 1 && (
|
||||
<span
|
||||
className="status-minimal__count"
|
||||
style={
|
||||
appliedColor
|
||||
? {
|
||||
backgroundColor: appliedColor,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{count}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!withWrapper) {
|
||||
return iconContent;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`status-wrapper${wrapperClassName ? ` ${wrapperClassName}` : ""}`}
|
||||
>
|
||||
{iconContent}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -2,16 +2,26 @@ import React, { memo, useCallback } from "react";
|
|||
import { NoteStatus } from "@/types/noteStatus";
|
||||
import { SelectableListItem } from "./SelectableListItem";
|
||||
import { getStatusTooltip, isStatusSelected } from "@/utils/statusUtils";
|
||||
import { StatusIconPreview } from "./StatusIconPreview";
|
||||
|
||||
interface StatusOptionProps {
|
||||
status: NoteStatus;
|
||||
isSelected: boolean;
|
||||
isFocused: boolean;
|
||||
onSelect: () => void;
|
||||
iconFrameMode: "always" | "never";
|
||||
iconColorMode: "status" | "theme";
|
||||
}
|
||||
|
||||
export const StatusModalOption: React.FC<StatusOptionProps> = memo(
|
||||
({ status, isSelected, isFocused, onSelect }) => {
|
||||
({
|
||||
status,
|
||||
isSelected,
|
||||
isFocused,
|
||||
onSelect,
|
||||
iconFrameMode,
|
||||
iconColorMode,
|
||||
}) => {
|
||||
const displayName = status.templateId
|
||||
? `${status.name} (${status.templateId})`
|
||||
: status.name;
|
||||
|
|
@ -20,14 +30,34 @@ export const StatusModalOption: React.FC<StatusOptionProps> = memo(
|
|||
<SelectableListItem
|
||||
selected={isSelected}
|
||||
focused={isFocused}
|
||||
icon={status.icon}
|
||||
icon={
|
||||
<div className="note-status-option__icon-wrapper">
|
||||
<StatusIconPreview
|
||||
icon={status.icon}
|
||||
color={status.color}
|
||||
iconFrameMode={iconFrameMode}
|
||||
iconColorMode={iconColorMode}
|
||||
withWrapper={false}
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
onClick={onSelect}
|
||||
className="note-status-option"
|
||||
title={
|
||||
status.description ? getStatusTooltip(status) : undefined
|
||||
}
|
||||
>
|
||||
{displayName}
|
||||
<div className="note-status-option__text">
|
||||
<span className="note-status-option__name">
|
||||
{displayName}
|
||||
</span>
|
||||
{status.description && (
|
||||
<span className="note-status-option__description">
|
||||
{status.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</SelectableListItem>
|
||||
);
|
||||
},
|
||||
|
|
@ -38,6 +68,8 @@ export interface Props {
|
|||
availableStatuses: NoteStatus[];
|
||||
focusedIndex?: number;
|
||||
onToggleStatus: (status: NoteStatus, selected: boolean) => void;
|
||||
iconFrameMode?: "always" | "never";
|
||||
iconColorMode?: "status" | "theme";
|
||||
}
|
||||
|
||||
export const StatusSelector: React.FC<Props> = ({
|
||||
|
|
@ -45,6 +77,8 @@ export const StatusSelector: React.FC<Props> = ({
|
|||
availableStatuses,
|
||||
focusedIndex = -1,
|
||||
onToggleStatus,
|
||||
iconFrameMode = "never",
|
||||
iconColorMode = "status",
|
||||
}) => {
|
||||
const handleSelectStatus = useCallback(
|
||||
async (status: NoteStatus) => {
|
||||
|
|
@ -72,6 +106,8 @@ export const StatusSelector: React.FC<Props> = ({
|
|||
isSelected={isStatusSelected(status, currentStatuses)}
|
||||
isFocused={index === focusedIndex}
|
||||
onSelect={() => handleSelectStatus(status)}
|
||||
iconFrameMode={iconFrameMode}
|
||||
iconColorMode={iconColorMode}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -118,6 +118,13 @@ export class StatusModalIntegration extends Modal {
|
|||
}
|
||||
filesQuantity={filesQuantity}
|
||||
templates={settingsService.settings.templates}
|
||||
iconFrameMode={
|
||||
settingsService.settings.fileExplorerIconFrame || "never"
|
||||
}
|
||||
iconColorMode={
|
||||
settingsService.settings.fileExplorerIconColorMode ||
|
||||
"status"
|
||||
}
|
||||
onRemoveStatus={this.onRemoveStatus}
|
||||
onSelectStatus={this.onSelectStatus}
|
||||
/>,
|
||||
|
|
|
|||
1463
styles.css
1463
styles.css
File diff suppressed because one or more lines are too long
|
|
@ -34,6 +34,17 @@
|
|||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.status-minimal--compact {
|
||||
margin-left: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
gap: var(--size-2-1);
|
||||
}
|
||||
|
||||
.status-minimal--compact .status-minimal__icon {
|
||||
font-size: var(--font-ui-medium);
|
||||
}
|
||||
|
||||
/* Use data attribute for dynamic colors */
|
||||
.status-minimal[data-status-color] .status-minimal__count {
|
||||
background: var(--interactive-accent);
|
||||
|
|
|
|||
|
|
@ -336,6 +336,50 @@
|
|||
|
||||
.note-status-option {
|
||||
/* Uses SelectableListItem styles */
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.note-status-option .selectable-list-item-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.note-status-option__icon-wrapper {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--size-2-1);
|
||||
border-radius: var(--radius-m);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
background: var(--background-secondary);
|
||||
min-width: 36px;
|
||||
transition:
|
||||
border-color var(--anim-duration-fast) ease,
|
||||
background var(--anim-duration-fast) ease;
|
||||
}
|
||||
|
||||
.note-status-option:hover .note-status-option__icon-wrapper,
|
||||
.note-status-option.selectable-list-item[data-selected="true"]
|
||||
.note-status-option__icon-wrapper {
|
||||
border-color: var(--interactive-accent);
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.note-status-option__text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.note-status-option__name {
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.note-status-option__description {
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.note-status-chips {
|
||||
|
|
|
|||
Loading…
Reference in a new issue