Merge pull request #72 from devonthesofa/fix/status-picker-colors

[feature] Unify template selector icon styling with file explorer
This commit is contained in:
Aleix Soler 2025-11-18 19:12:45 +01:00 committed by GitHub
commit e4eb51108b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 1684 additions and 62 deletions

View file

@ -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}
/>
))}

View file

@ -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)

View file

@ -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)}
/>
);
},
);

View 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>
);
},
);

View file

@ -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>

View file

@ -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

File diff suppressed because one or more lines are too long

View file

@ -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);

View file

@ -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 {