mirror of
https://github.com/devonthesofa/obsidian-note-status.git
synced 2026-07-22 05:45:04 +00:00
feat: add lucid icons support
This commit is contained in:
parent
e4eb51108b
commit
1b26ba6d80
24 changed files with 477 additions and 50 deletions
|
|
@ -65,6 +65,7 @@ export const FileExplorerIcon: FC<Props> = memo(
|
|||
return (
|
||||
<StatusIconPreview
|
||||
icon={primaryStatus.icon}
|
||||
lucideIcon={primaryStatus.lucideIcon}
|
||||
color={iconColor}
|
||||
count={totalStatuses}
|
||||
iconFrameMode={iconFrameMode}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export type StatusItem = {
|
|||
name: string;
|
||||
color: string;
|
||||
icon?: string;
|
||||
lucideIcon?: string;
|
||||
};
|
||||
|
||||
export type FilesByStatus = {
|
||||
|
|
@ -69,6 +70,7 @@ const GroupedStatusViewContent = () => {
|
|||
name: noteStatus.name,
|
||||
color: noteStatus.color || "white",
|
||||
icon: noteStatus.icon,
|
||||
lucideIcon: noteStatus.lucideIcon,
|
||||
};
|
||||
|
||||
const scopedIdentifier = noteStatus.templateId
|
||||
|
|
|
|||
|
|
@ -54,7 +54,11 @@ export const StatusGroup = ({
|
|||
<div className="grouped-status-group__status">
|
||||
<StatusDisplay
|
||||
status={
|
||||
{ ...status, icon: status.icon || "" } as NoteStatus
|
||||
{
|
||||
...status,
|
||||
icon: status.icon || "",
|
||||
lucideIcon: status.lucideIcon,
|
||||
} as NoteStatus
|
||||
}
|
||||
variant="badge"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import { NoteStatus } from "@/types/noteStatus";
|
||||
import React from "react";
|
||||
import { Input } from "@/components/atoms/Input";
|
||||
import { StatusIcon } from "@/components/atoms/StatusIcon";
|
||||
import { LucideIconPicker } from "./LucideIconPicker";
|
||||
|
||||
export type Props = {
|
||||
status: NoteStatus;
|
||||
index: number;
|
||||
onCustomStatusChange: (
|
||||
index: number,
|
||||
column: "name" | "icon" | "color" | "description",
|
||||
column: "name" | "icon" | "color" | "description" | "lucideIcon",
|
||||
value: string,
|
||||
) => void;
|
||||
onCustomStatusRemove: (index: number) => void;
|
||||
|
|
@ -28,26 +30,47 @@ export const CustomStatusItem: React.FC<Props> = ({
|
|||
canMoveDown = false,
|
||||
}) => {
|
||||
const isValid = status.name.trim().length > 0;
|
||||
const displayIcon = status.icon.trim() || "📝";
|
||||
|
||||
return (
|
||||
<div className="custom-status-item">
|
||||
{/* Simple horizontal layout with all inputs in a row */}
|
||||
<div className="custom-status-item__row">
|
||||
{/* Icon field - simple text input */}
|
||||
<div className="custom-status-item__field">
|
||||
<label className="custom-status-item__label">Icon</label>
|
||||
<div className="custom-status-item__row custom-status-item__row--icons">
|
||||
<div className="custom-status-item__field custom-status-item__field--icon">
|
||||
<label className="custom-status-item__label">
|
||||
Emoji icon
|
||||
</label>
|
||||
<Input
|
||||
variant="text"
|
||||
value={status.icon}
|
||||
onChange={(value) =>
|
||||
onCustomStatusChange(index, "icon", value || "")
|
||||
}
|
||||
placeholder="📝"
|
||||
placeholder="Example: ✅ or 🚧"
|
||||
className="custom-status-item__input custom-status-item__input--icon"
|
||||
/>
|
||||
<p className="custom-status-item__hint">
|
||||
This fallback icon is shown anywhere Lucide is not
|
||||
available.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="custom-status-item__field custom-status-item__field--lucide">
|
||||
<label className="custom-status-item__label">
|
||||
Lucide icon (optional)
|
||||
</label>
|
||||
<LucideIconPicker
|
||||
value={status.lucideIcon || ""}
|
||||
onChange={(value) =>
|
||||
onCustomStatusChange(index, "lucideIcon", value)
|
||||
}
|
||||
/>
|
||||
<p className="custom-status-item__hint">
|
||||
Matches Obsidian's toolbar icons so your status
|
||||
button blends in.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Simple horizontal layout with remaining inputs */}
|
||||
<div className="custom-status-item__row">
|
||||
{/* Name field */}
|
||||
<div className="custom-status-item__field custom-status-item__field--name">
|
||||
<label className="custom-status-item__label">
|
||||
|
|
@ -139,12 +162,13 @@ export const CustomStatusItem: React.FC<Props> = ({
|
|||
|
||||
{/* Preview row - shows how the status will look */}
|
||||
<div className="custom-status-item__preview">
|
||||
<span
|
||||
<StatusIcon
|
||||
icon={status.icon}
|
||||
lucideIcon={status.lucideIcon}
|
||||
className="custom-status-item__preview-icon"
|
||||
size={18}
|
||||
style={{ color: status.color }}
|
||||
>
|
||||
{displayIcon}
|
||||
</span>
|
||||
/>
|
||||
<span
|
||||
className="custom-status-item__preview-text"
|
||||
style={{ color: status.color }}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export const CustomStatusSettings: React.FC<Props> = ({
|
|||
currentStatuses.push({
|
||||
name: "",
|
||||
icon: "",
|
||||
lucideIcon: "",
|
||||
});
|
||||
onChange("customStatuses", currentStatuses);
|
||||
};
|
||||
|
|
@ -84,7 +85,7 @@ export const CustomStatusSettings: React.FC<Props> = ({
|
|||
|
||||
<SettingItem
|
||||
name="Custom statuses"
|
||||
description="Create custom statuses with icons, names, and colors. All statuses require a name."
|
||||
description="Create custom statuses with emoji icons and optional Lucide icon names plus colors. All statuses require a name."
|
||||
vertical
|
||||
>
|
||||
<div className="custom-status-list">
|
||||
|
|
|
|||
133
components/SettingsUI/LucideIconPicker.tsx
Normal file
133
components/SettingsUI/LucideIconPicker.tsx
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { IconName, getIconIds } from "obsidian";
|
||||
import { Input } from "@/components/atoms/Input";
|
||||
import { StatusIcon } from "@/components/atoms/StatusIcon";
|
||||
|
||||
export interface LucideIconPickerProps {
|
||||
value?: string;
|
||||
onChange: (value: string) => void;
|
||||
maxResults?: number;
|
||||
}
|
||||
|
||||
const DEFAULT_MAX_RESULTS = 36;
|
||||
|
||||
export const LucideIconPicker: React.FC<LucideIconPickerProps> = ({
|
||||
value = "",
|
||||
onChange,
|
||||
maxResults = DEFAULT_MAX_RESULTS,
|
||||
}) => {
|
||||
const [query, setQuery] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
setQuery(value || "");
|
||||
}, [value]);
|
||||
|
||||
const iconIds = useMemo(() => {
|
||||
try {
|
||||
return getIconIds()
|
||||
.slice()
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
} catch (error) {
|
||||
console.error("Failed to load Lucide icon list", error);
|
||||
return [] as IconName[];
|
||||
}
|
||||
}, []);
|
||||
|
||||
const filteredIcons = useMemo(() => {
|
||||
if (!iconIds.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
const source = normalizedQuery.length
|
||||
? iconIds.filter((name) =>
|
||||
name.toLowerCase().includes(normalizedQuery),
|
||||
)
|
||||
: iconIds;
|
||||
|
||||
return source.slice(0, maxResults);
|
||||
}, [iconIds, query, maxResults]);
|
||||
|
||||
const handleSelect = (iconName: IconName) => {
|
||||
onChange(iconName);
|
||||
setQuery(iconName);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
onChange("");
|
||||
setQuery("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="lucide-icon-picker">
|
||||
<div className="lucide-icon-picker__control">
|
||||
<Input
|
||||
variant="text"
|
||||
value={query}
|
||||
onChange={setQuery}
|
||||
placeholder="Search by name (e.g. check-circle)"
|
||||
className="lucide-icon-picker__input"
|
||||
/>
|
||||
{value && (
|
||||
<button
|
||||
type="button"
|
||||
className="lucide-icon-picker__clear"
|
||||
onClick={handleClear}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="lucide-icon-picker__selected">
|
||||
<span className="lucide-icon-picker__selected-preview">
|
||||
<StatusIcon
|
||||
icon={value}
|
||||
lucideIcon={value}
|
||||
size={18}
|
||||
className="lucide-icon-picker__selected-icon"
|
||||
/>
|
||||
</span>
|
||||
<span className="lucide-icon-picker__selected-label">
|
||||
{value || "No Lucide icon selected"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{!iconIds.length && (
|
||||
<div className="lucide-icon-picker__empty">
|
||||
Lucide icons are not available yet. Try reloading the
|
||||
plugin.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{iconIds.length > 0 && (
|
||||
<div className="lucide-icon-picker__grid">
|
||||
{filteredIcons.map((iconName) => {
|
||||
const isSelected = iconName === value;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={iconName}
|
||||
className={`lucide-icon-picker__option ${isSelected ? "lucide-icon-picker__option--selected" : ""}`}
|
||||
onClick={() => handleSelect(iconName)}
|
||||
title={iconName}
|
||||
>
|
||||
<StatusIcon
|
||||
icon={iconName}
|
||||
lucideIcon={iconName}
|
||||
size={16}
|
||||
/>
|
||||
<span>{iconName}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{filteredIcons.length === 0 && (
|
||||
<div className="lucide-icon-picker__empty">
|
||||
No icons match "{query.trim()}"
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import React from "react";
|
||||
import { PluginSettings } from "@/types/pluginSettings";
|
||||
import { StatusIcon } from "@/components/atoms/StatusIcon";
|
||||
|
||||
export type StatusGroupProps = {
|
||||
statuses: PluginSettings["customStatuses"];
|
||||
|
|
@ -39,9 +40,12 @@ export const StatusGroup: React.FC<StatusGroupProps> = ({
|
|||
>
|
||||
<div className="status-selector__content">
|
||||
<div className="status-selector__status">
|
||||
<span className="status-selector__icon">
|
||||
{status.icon}
|
||||
</span>
|
||||
<StatusIcon
|
||||
icon={status.icon}
|
||||
lucideIcon={status.lucideIcon}
|
||||
className="status-selector__icon"
|
||||
size={16}
|
||||
/>
|
||||
<span className="status-selector__name">
|
||||
{status.name}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export const TemplateEditorModal: React.FC<TemplateEditorModalProps> = ({
|
|||
{
|
||||
name: "",
|
||||
icon: "",
|
||||
lucideIcon: "",
|
||||
color: "#888888",
|
||||
templateId: template?.id || "",
|
||||
},
|
||||
|
|
@ -63,6 +64,7 @@ export const TemplateEditorModal: React.FC<TemplateEditorModalProps> = ({
|
|||
{
|
||||
name: "",
|
||||
icon: "",
|
||||
lucideIcon: "",
|
||||
color: "#888888",
|
||||
templateId: template?.id || "",
|
||||
},
|
||||
|
|
@ -72,7 +74,7 @@ export const TemplateEditorModal: React.FC<TemplateEditorModalProps> = ({
|
|||
const handleStatusChange = useCallback(
|
||||
(
|
||||
index: number,
|
||||
column: "name" | "icon" | "color" | "description",
|
||||
column: "name" | "icon" | "color" | "description" | "lucideIcon",
|
||||
value: string,
|
||||
) => {
|
||||
setStatuses((prev) =>
|
||||
|
|
@ -145,7 +147,7 @@ export const TemplateEditorModal: React.FC<TemplateEditorModalProps> = ({
|
|||
|
||||
<SettingItem
|
||||
name="Statuses"
|
||||
description="Define the statuses included in this template"
|
||||
description="Define the statuses included in this template, including emojis and optional Lucide icons"
|
||||
vertical
|
||||
>
|
||||
<div className="template-editor-statuses">
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ export const UISettings: React.FC<Props> = ({ settings, onChange }) => {
|
|||
|
||||
<SettingItem
|
||||
name="Icon for unknown status"
|
||||
description="Icon displayed whenever a note does not have a status."
|
||||
description="Emoji or Lucide icon name displayed whenever a note does not have a status."
|
||||
>
|
||||
<Input
|
||||
variant="text"
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
Props as StatusBarContextProps,
|
||||
} from "./StatusBarContext";
|
||||
import { StatusBarGroup } from "./StatusBarGroup";
|
||||
import { StatusIcon } from "@/components/atoms/StatusIcon";
|
||||
|
||||
export type Props = {
|
||||
statuses: GroupedStatuses;
|
||||
|
|
@ -51,13 +52,14 @@ export const StatusBar: FC<Props> = ({
|
|||
}}
|
||||
>
|
||||
{noStatusConfig.showIcon && (
|
||||
<span
|
||||
<StatusIcon
|
||||
icon={noStatusConfig.icon}
|
||||
size={14}
|
||||
className="status-bar-no-status-icon"
|
||||
style={{
|
||||
marginRight: noStatusConfig.showText ? "4px" : "0",
|
||||
}}
|
||||
>
|
||||
{noStatusConfig.icon}
|
||||
</span>
|
||||
/>
|
||||
)}
|
||||
{noStatusConfig.showText && noStatusConfig.text}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { FC, memo } from "react";
|
||||
import { GroupedStatuses } from "@/types/noteStatus";
|
||||
import { StatusesInfoPopup } from "@/integrations/popups/statusesInfoPopupIntegration";
|
||||
import { StatusIcon } from "@/components/atoms/StatusIcon";
|
||||
|
||||
interface EditorToolbarButtonProps {
|
||||
statuses: GroupedStatuses;
|
||||
|
|
@ -40,12 +41,12 @@ export const EditorToolbarButton: FC<EditorToolbarButtonProps> = memo(
|
|||
onMouseLeave={handleMouseLeave}
|
||||
aria-label="Add status to note"
|
||||
>
|
||||
<span
|
||||
<StatusIcon
|
||||
icon={unknownStatusConfig.icon}
|
||||
size={16}
|
||||
className="editor-toolbar-button__icon"
|
||||
style={{ color: unknownStatusConfig.color }}
|
||||
>
|
||||
{unknownStatusConfig.icon}
|
||||
</span>
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
@ -63,16 +64,17 @@ export const EditorToolbarButton: FC<EditorToolbarButtonProps> = memo(
|
|||
onMouseLeave={handleMouseLeave}
|
||||
aria-label={`Current status: ${primaryStatus.name}. Click to change.`}
|
||||
>
|
||||
<span
|
||||
<StatusIcon
|
||||
icon={primaryStatus.icon}
|
||||
lucideIcon={primaryStatus.lucideIcon}
|
||||
size={16}
|
||||
className="editor-toolbar-button__icon editor-toolbar-button__icon--has-status"
|
||||
style={{
|
||||
color:
|
||||
primaryStatus.color ||
|
||||
"var(--interactive-accent)",
|
||||
}}
|
||||
>
|
||||
{primaryStatus.icon || "📝"}
|
||||
</span>
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
@ -88,16 +90,17 @@ export const EditorToolbarButton: FC<EditorToolbarButtonProps> = memo(
|
|||
aria-label={`${totalStatuses} statuses assigned. Click to change.`}
|
||||
>
|
||||
<div className="editor-toolbar-button__icon-container">
|
||||
<span
|
||||
<StatusIcon
|
||||
icon={primaryStatus.icon}
|
||||
lucideIcon={primaryStatus.lucideIcon}
|
||||
size={16}
|
||||
className="editor-toolbar-button__icon editor-toolbar-button__icon--has-status"
|
||||
style={{
|
||||
color:
|
||||
primaryStatus.color ||
|
||||
"var(--interactive-accent)",
|
||||
}}
|
||||
>
|
||||
{primaryStatus.icon || "📝"}
|
||||
</span>
|
||||
/>
|
||||
<span className="editor-toolbar-button__counter">
|
||||
{totalStatuses}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { NoteStatus } from "@/types/noteStatus";
|
|||
import { FC, memo, useState } from "react";
|
||||
import { getStatusTooltip } from "@/utils/statusUtils";
|
||||
import { ObsidianIcon } from "./ObsidianIcon";
|
||||
import { StatusIcon } from "./StatusIcon";
|
||||
|
||||
export type StatusDisplayVariant = "chip" | "badge" | "template";
|
||||
|
||||
|
|
@ -82,9 +83,12 @@ export const StatusDisplay: FC<StatusDisplayProps> = memo(
|
|||
}}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<span className="note-status-chip-icon">
|
||||
{status.icon ? status.icon : "📝"}
|
||||
</span>
|
||||
<StatusIcon
|
||||
icon={status.icon}
|
||||
lucideIcon={status.lucideIcon}
|
||||
className="note-status-chip-icon"
|
||||
size={16}
|
||||
/>
|
||||
<span className="note-status-chip-text">
|
||||
{getDisplayName()}
|
||||
</span>
|
||||
|
|
@ -121,9 +125,12 @@ export const StatusDisplay: FC<StatusDisplayProps> = memo(
|
|||
onClick={handleClick}
|
||||
>
|
||||
<div className="status-badge-item">
|
||||
<span className="status-badge-icon">
|
||||
{status.icon ? status.icon : "📝"}
|
||||
</span>
|
||||
<StatusIcon
|
||||
icon={status.icon}
|
||||
lucideIcon={status.lucideIcon}
|
||||
className="status-badge-icon"
|
||||
size={14}
|
||||
/>
|
||||
<span className="status-badge-text">
|
||||
{getDisplayName()}
|
||||
</span>
|
||||
|
|
@ -144,7 +151,13 @@ export const StatusDisplay: FC<StatusDisplayProps> = memo(
|
|||
}
|
||||
/>
|
||||
<span>
|
||||
{status.icon ? status.icon : "📝"} {getDisplayName()}
|
||||
<StatusIcon
|
||||
icon={status.icon}
|
||||
lucideIcon={status.lucideIcon}
|
||||
size={14}
|
||||
style={{ marginRight: "4px" }}
|
||||
/>
|
||||
{getDisplayName()}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
52
components/atoms/StatusIcon.tsx
Normal file
52
components/atoms/StatusIcon.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import React, { CSSProperties, FC } from "react";
|
||||
import { ObsidianIcon } from "./ObsidianIcon";
|
||||
import { resolveLucideIconName } from "@/utils/iconUtils";
|
||||
|
||||
interface StatusIconProps {
|
||||
icon?: string;
|
||||
lucideIcon?: string;
|
||||
fallbackIcon?: string;
|
||||
className?: string;
|
||||
size?: number;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export const StatusIcon: FC<StatusIconProps> = ({
|
||||
icon,
|
||||
lucideIcon,
|
||||
fallbackIcon = "📝",
|
||||
className = "",
|
||||
size,
|
||||
style = {},
|
||||
}) => {
|
||||
const lucideName = resolveLucideIconName(lucideIcon, icon);
|
||||
const resolvedSize = size ?? 16;
|
||||
const combinedClassName = ["status-icon", className]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
const wrapperStyle: CSSProperties = {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
lineHeight: 1,
|
||||
...style,
|
||||
};
|
||||
|
||||
if (size) {
|
||||
wrapperStyle.fontSize = `${size}px`;
|
||||
}
|
||||
|
||||
const fallbackValue =
|
||||
icon && icon.trim().length ? icon : (fallbackIcon ?? "");
|
||||
|
||||
return (
|
||||
<span className={combinedClassName} style={wrapperStyle}>
|
||||
{lucideName ? (
|
||||
<ObsidianIcon name={lucideName} size={resolvedSize} />
|
||||
) : (
|
||||
fallbackValue
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
import React, { CSSProperties, FC, memo } from "react";
|
||||
import { StatusIcon } from "./StatusIcon";
|
||||
|
||||
type IconFrameMode = "always" | "never";
|
||||
type IconColorMode = "status" | "theme";
|
||||
|
||||
export interface StatusIconPreviewProps {
|
||||
icon?: string;
|
||||
lucideIcon?: string;
|
||||
color?: string;
|
||||
count?: number;
|
||||
iconFrameMode?: IconFrameMode;
|
||||
|
|
@ -14,6 +16,7 @@ export interface StatusIconPreviewProps {
|
|||
iconClassName?: string;
|
||||
wrapperClassName?: string;
|
||||
style?: CSSProperties;
|
||||
iconSize?: number;
|
||||
onMouseEnter?: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
onMouseLeave?: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
}
|
||||
|
|
@ -21,6 +24,7 @@ export interface StatusIconPreviewProps {
|
|||
export const StatusIconPreview: FC<StatusIconPreviewProps> = memo(
|
||||
({
|
||||
icon,
|
||||
lucideIcon,
|
||||
color,
|
||||
count,
|
||||
iconFrameMode = "never",
|
||||
|
|
@ -30,10 +34,10 @@ export const StatusIconPreview: FC<StatusIconPreviewProps> = memo(
|
|||
iconClassName = "",
|
||||
wrapperClassName = "",
|
||||
style,
|
||||
iconSize,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
}) => {
|
||||
const iconDisplay = icon?.trim().length ? icon : "📝";
|
||||
const useStatusColor = iconColorMode === "status";
|
||||
const appliedColor = useStatusColor && color ? color.trim() : undefined;
|
||||
|
||||
|
|
@ -58,7 +62,12 @@ export const StatusIconPreview: FC<StatusIconPreviewProps> = memo(
|
|||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<span className="status-minimal__icon">{iconDisplay}</span>
|
||||
<StatusIcon
|
||||
icon={icon}
|
||||
lucideIcon={lucideIcon}
|
||||
size={iconSize ?? (compact ? 13 : 16)}
|
||||
className="status-minimal__icon"
|
||||
/>
|
||||
{count && count > 1 && (
|
||||
<span
|
||||
className="status-minimal__count"
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export const StatusModalOption: React.FC<StatusOptionProps> = memo(
|
|||
<div className="note-status-option__icon-wrapper">
|
||||
<StatusIconPreview
|
||||
icon={status.icon}
|
||||
lucideIcon={status.lucideIcon}
|
||||
color={status.color}
|
||||
iconFrameMode={iconFrameMode}
|
||||
iconColorMode={iconColorMode}
|
||||
|
|
@ -101,7 +102,7 @@ export const StatusSelector: React.FC<Props> = ({
|
|||
>
|
||||
{availableStatuses.map((status, index) => (
|
||||
<StatusModalOption
|
||||
key={`${status.templateId || "custom"}:${status.name}:${status.description}:${status.color}:${status.icon}`}
|
||||
key={`${status.templateId || "custom"}:${status.name}:${status.description}:${status.color}:${status.icon}:${status.lucideIcon ?? ""}`}
|
||||
status={status}
|
||||
isSelected={isStatusSelected(status, currentStatuses)}
|
||||
isFocused={index === focusedIndex}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ interface StatusItem {
|
|||
name: string;
|
||||
color: string;
|
||||
icon?: string;
|
||||
lucideIcon?: string;
|
||||
}
|
||||
|
||||
interface FilesByStatus {
|
||||
|
|
@ -57,6 +58,7 @@ export class GroupedDashboardView extends ItemView {
|
|||
name: status.name,
|
||||
color: status.color || "",
|
||||
icon: status.icon,
|
||||
lucideIcon: status.lucideIcon,
|
||||
});
|
||||
|
||||
private getAllFiles = (): FileItem[] => {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export class GroupedStatusView extends ItemView {
|
|||
name: status.name,
|
||||
color: status.color || "white",
|
||||
icon: status.icon,
|
||||
lucideIcon: status.lucideIcon,
|
||||
});
|
||||
|
||||
private getAllFiles = (): FileItem[] => {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@
|
|||
.status-minimal__icon {
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
filter: grayscale(0.2);
|
||||
transition: filter var(--anim-duration-fast) ease;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,12 +36,23 @@
|
|||
margin-bottom: var(--size-4-2);
|
||||
}
|
||||
|
||||
.custom-status-item__row--icons {
|
||||
align-items: stretch;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.custom-status-item__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-2-1);
|
||||
}
|
||||
|
||||
.custom-status-item__field--icon,
|
||||
.custom-status-item__field--lucide {
|
||||
flex: 1;
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
.custom-status-item__field--name {
|
||||
flex: 2;
|
||||
min-width: 120px;
|
||||
|
|
@ -82,9 +93,15 @@
|
|||
}
|
||||
|
||||
.custom-status-item__input--icon {
|
||||
width: 50px !important;
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
width: 100% !important;
|
||||
text-align: left;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.custom-status-item__hint {
|
||||
margin: 0;
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.custom-status-item__input--name {
|
||||
|
|
@ -203,6 +220,97 @@
|
|||
border-left: 3px solid var(--text-error);
|
||||
}
|
||||
|
||||
.lucide-icon-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-2-2);
|
||||
}
|
||||
|
||||
.lucide-icon-picker__control {
|
||||
display: flex;
|
||||
gap: var(--size-2-1);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.lucide-icon-picker__input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.lucide-icon-picker__clear {
|
||||
background: var(--background-modifier-border);
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--radius-s);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-ui-smaller);
|
||||
}
|
||||
|
||||
.lucide-icon-picker__clear:hover {
|
||||
background: var(--interactive-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.lucide-icon-picker__selected {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-2-1);
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.lucide-icon-picker__selected-preview {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--background-secondary);
|
||||
border-radius: var(--radius-s);
|
||||
}
|
||||
|
||||
.lucide-icon-picker__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: var(--size-2-1);
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
padding: var(--size-2-1);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-m);
|
||||
background: var(--background-primary-alt);
|
||||
}
|
||||
|
||||
.lucide-icon-picker__option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-2-1);
|
||||
padding: var(--size-2-1) var(--size-2-2);
|
||||
border-radius: var(--radius-s);
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: var(--font-ui-smaller);
|
||||
transition:
|
||||
border var(--anim-duration-fast) ease,
|
||||
background var(--anim-duration-fast) ease;
|
||||
}
|
||||
|
||||
.lucide-icon-picker__option:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.lucide-icon-picker__option--selected {
|
||||
border-color: var(--interactive-accent);
|
||||
background: var(--background-modifier-success);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.lucide-icon-picker__empty {
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
padding: var(--size-2-1);
|
||||
}
|
||||
|
||||
/* Custom Status List - BEM */
|
||||
.custom-status-list {
|
||||
/* Container for multiple custom status items */
|
||||
|
|
|
|||
6
styles/components/status-icon.css
Normal file
6
styles/components/status-icon.css
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
.status-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
|
@ -21,6 +21,9 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-muted);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Icon with status color */
|
||||
|
|
|
|||
|
|
@ -15,5 +15,6 @@
|
|||
@import "components/group-label.css";
|
||||
@import "components/collapsible-counter.css";
|
||||
@import "components/status-display.css";
|
||||
@import "components/status-icon.css";
|
||||
@import "components/obsidian-icon.css";
|
||||
@import "components/toolbar-button.css";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export type NoteStatus = {
|
||||
name: string;
|
||||
icon: string;
|
||||
lucideIcon?: string;
|
||||
color?: string; // Optional color property
|
||||
description?: string; // Optional description property
|
||||
templateId?: string; // Optional template scope for namespacing
|
||||
|
|
|
|||
51
utils/iconUtils.ts
Normal file
51
utils/iconUtils.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { IconName, getIcon, getIconIds } from "obsidian";
|
||||
|
||||
let cachedIconSet: Set<IconName> | null = null;
|
||||
|
||||
const getLucideIconSet = () => {
|
||||
if (!cachedIconSet || cachedIconSet.size === 0) {
|
||||
try {
|
||||
const ids = getIconIds();
|
||||
if (ids?.length) {
|
||||
cachedIconSet = new Set(ids);
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore – fallback detection will rely on getIcon
|
||||
cachedIconSet = null;
|
||||
}
|
||||
}
|
||||
return cachedIconSet;
|
||||
};
|
||||
|
||||
const iconExists = (iconName: string): boolean => {
|
||||
const iconSet = getLucideIconSet();
|
||||
if (iconSet?.size) {
|
||||
return iconSet.has(iconName as IconName);
|
||||
}
|
||||
try {
|
||||
return !!getIcon(iconName as IconName);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const resolveLucideIconName = (
|
||||
...candidates: Array<string | undefined | null>
|
||||
): IconName | null => {
|
||||
for (const candidate of candidates) {
|
||||
const trimmed = candidate?.trim();
|
||||
if (trimmed && iconExists(trimmed)) {
|
||||
return trimmed as IconName;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const isLucideIconName = (name?: string | null) => {
|
||||
const trimmed = name?.trim();
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
return iconExists(trimmed);
|
||||
};
|
||||
Loading…
Reference in a new issue