mirror of
https://github.com/devonthesofa/obsidian-note-status.git
synced 2026-07-22 12:30:24 +00:00
Merge pull request #73 from devonthesofa/feature/lucid-icons-support
[Feature] Support Lucide icons in statuses
This commit is contained in:
commit
6ea99c29fa
33 changed files with 850 additions and 1533 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { StatusIconPreview } from "@/components/atoms/StatusIconPreview";
|
||||
import { GroupedStatuses } from "@/types/noteStatus";
|
||||
import React, { FC, memo } from "react";
|
||||
import { StatusIcon } from "@/components/atoms/StatusIcon";
|
||||
|
||||
type Props = {
|
||||
statuses: GroupedStatuses;
|
||||
|
|
@ -9,6 +10,7 @@ type Props = {
|
|||
hideUnknownStatus?: boolean;
|
||||
unknownStatusConfig?: {
|
||||
icon: string;
|
||||
lucideIcon?: string;
|
||||
color: string;
|
||||
};
|
||||
iconFrameMode?: "always" | "never";
|
||||
|
|
@ -46,7 +48,13 @@ export const FileExplorerIcon: FC<Props> = memo(
|
|||
|
||||
return (
|
||||
<StatusIconPreview
|
||||
icon={icon}
|
||||
icon={
|
||||
<StatusIcon
|
||||
icon={icon}
|
||||
lucideIcon={unknownStatusConfig?.lucideIcon}
|
||||
size={16}
|
||||
/>
|
||||
}
|
||||
color={color}
|
||||
iconColorMode={iconColorMode}
|
||||
iconFrameMode={iconFrameMode}
|
||||
|
|
@ -64,7 +72,13 @@ export const FileExplorerIcon: FC<Props> = memo(
|
|||
|
||||
return (
|
||||
<StatusIconPreview
|
||||
icon={primaryStatus.icon}
|
||||
icon={
|
||||
<StatusIcon
|
||||
icon={primaryStatus.icon}
|
||||
lucideIcon={primaryStatus.lucideIcon}
|
||||
size={16}
|
||||
/>
|
||||
}
|
||||
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 { IconSelectionField } from "./IconSelectionField";
|
||||
|
||||
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,28 @@ 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>
|
||||
<Input
|
||||
variant="text"
|
||||
value={status.icon}
|
||||
onChange={(value) =>
|
||||
onCustomStatusChange(index, "icon", value || "")
|
||||
}
|
||||
placeholder="📝"
|
||||
className="custom-status-item__input custom-status-item__input--icon"
|
||||
/>
|
||||
</div>
|
||||
<IconSelectionField
|
||||
className="custom-status-item__icon-fields"
|
||||
emojiValue={status.icon}
|
||||
onEmojiChange={(value) =>
|
||||
onCustomStatusChange(index, "icon", value || "")
|
||||
}
|
||||
emojiLabel="Emoji icon"
|
||||
emojiPlaceholder="Example: ✅ or 🚧"
|
||||
emojiHint="Shown anywhere Lucide is not available."
|
||||
lucideValue={status.lucideIcon || ""}
|
||||
onLucideChange={(value) =>
|
||||
onCustomStatusChange(index, "lucideIcon", value)
|
||||
}
|
||||
lucideLabel="Lucide icon (optional)"
|
||||
lucidePlaceholder="Browse Lucide icons"
|
||||
lucideHint="Matches Obsidian's toolbar icons so your status button blends in."
|
||||
/>
|
||||
|
||||
{/* 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 +143,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">
|
||||
|
|
|
|||
75
components/SettingsUI/IconSelectionField.tsx
Normal file
75
components/SettingsUI/IconSelectionField.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import React from "react";
|
||||
import { Input } from "@/components/atoms/Input";
|
||||
import { LucideIconPicker } from "./LucideIconPicker";
|
||||
|
||||
export interface IconSelectionFieldProps {
|
||||
className?: string;
|
||||
emojiValue: string;
|
||||
onEmojiChange: (value: string) => void;
|
||||
emojiLabel?: string;
|
||||
emojiPlaceholder?: string;
|
||||
emojiHint?: string;
|
||||
lucideValue?: string;
|
||||
onLucideChange?: (value: string) => void;
|
||||
lucideLabel?: string;
|
||||
lucidePlaceholder?: string;
|
||||
lucideHint?: string;
|
||||
lucideAllowClear?: boolean;
|
||||
}
|
||||
|
||||
export const IconSelectionField: React.FC<IconSelectionFieldProps> = ({
|
||||
className = "",
|
||||
emojiValue,
|
||||
onEmojiChange,
|
||||
emojiLabel = "Emoji fallback",
|
||||
emojiPlaceholder = "Example: ✅",
|
||||
emojiHint,
|
||||
lucideValue,
|
||||
onLucideChange,
|
||||
lucideLabel = "Lucide icon",
|
||||
lucidePlaceholder = "Choose Lucide icon",
|
||||
lucideHint,
|
||||
lucideAllowClear = true,
|
||||
}) => {
|
||||
const containerClass = ["icon-selection-field", className]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
return (
|
||||
<div className={containerClass}>
|
||||
<div className="icon-selection-field__column">
|
||||
<label className="icon-selection-field__label">
|
||||
{emojiLabel}
|
||||
</label>
|
||||
<Input
|
||||
variant="text"
|
||||
value={emojiValue}
|
||||
onChange={onEmojiChange}
|
||||
placeholder={emojiPlaceholder}
|
||||
className="icon-selection-field__emoji-input"
|
||||
/>
|
||||
{emojiHint && (
|
||||
<p className="icon-selection-field__hint">{emojiHint}</p>
|
||||
)}
|
||||
</div>
|
||||
{onLucideChange && (
|
||||
<div className="icon-selection-field__column">
|
||||
<label className="icon-selection-field__label">
|
||||
{lucideLabel}
|
||||
</label>
|
||||
<LucideIconPicker
|
||||
value={lucideValue}
|
||||
onChange={onLucideChange}
|
||||
placeholder={lucidePlaceholder}
|
||||
allowClear={lucideAllowClear}
|
||||
/>
|
||||
{lucideHint && (
|
||||
<p className="icon-selection-field__hint">
|
||||
{lucideHint}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
223
components/SettingsUI/LucideIconModal.tsx
Normal file
223
components/SettingsUI/LucideIconModal.tsx
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
import React, {
|
||||
FC,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { App, Modal } from "obsidian";
|
||||
import { IconName, getIconIds } from "obsidian";
|
||||
import { Input } from "@/components/atoms/Input";
|
||||
import { StatusIcon } from "@/components/atoms/StatusIcon";
|
||||
import { ObsidianIcon } from "@/components/atoms/ObsidianIcon";
|
||||
import { createRoot, Root } from "react-dom/client";
|
||||
|
||||
const BATCH_SIZE = 80;
|
||||
|
||||
interface LucideIconModalProps {
|
||||
initialValue?: string;
|
||||
onSelect: (value: string) => void;
|
||||
onCloseRequest: () => void;
|
||||
}
|
||||
|
||||
const LucideIconModalContent: FC<LucideIconModalProps> = ({
|
||||
initialValue = "",
|
||||
onSelect,
|
||||
onCloseRequest,
|
||||
}) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const [visibleCount, setVisibleCount] = useState(BATCH_SIZE);
|
||||
const [showLabels, setShowLabels] = useState(true);
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const iconIds = useMemo(() => {
|
||||
try {
|
||||
return getIconIds()
|
||||
.slice()
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch Lucide icon list", error);
|
||||
return [] as IconName[];
|
||||
}
|
||||
}, []);
|
||||
|
||||
const computeFuzzyScore = useCallback(
|
||||
(needle: string, haystack: string): number | null => {
|
||||
let lastIndex = -1;
|
||||
let score = 0;
|
||||
|
||||
for (let i = 0; i < needle.length; i += 1) {
|
||||
const char = needle[i];
|
||||
const idx = haystack.indexOf(char, lastIndex + 1);
|
||||
if (idx === -1) {
|
||||
return null;
|
||||
}
|
||||
score += idx - lastIndex;
|
||||
lastIndex = idx;
|
||||
}
|
||||
|
||||
return score + (haystack.length - lastIndex);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const filteredIcons = useMemo(() => {
|
||||
if (!iconIds.length) {
|
||||
return [];
|
||||
}
|
||||
const normalized = query.trim().toLowerCase();
|
||||
if (!normalized.length) {
|
||||
return iconIds;
|
||||
}
|
||||
|
||||
const matches = iconIds
|
||||
.map((name) => {
|
||||
const target = name.toLowerCase();
|
||||
const score = computeFuzzyScore(normalized, target);
|
||||
return score === null ? null : { name, score };
|
||||
})
|
||||
.filter(
|
||||
(
|
||||
entry,
|
||||
): entry is {
|
||||
name: string;
|
||||
score: number;
|
||||
} => entry !== null,
|
||||
)
|
||||
.sort((a, b) => a.score - b.score || a.name.localeCompare(b.name));
|
||||
|
||||
return matches.map((entry) => entry.name);
|
||||
}, [iconIds, query, computeFuzzyScore]);
|
||||
|
||||
const visibleIcons = useMemo(
|
||||
() => filteredIcons.slice(0, visibleCount),
|
||||
[filteredIcons, visibleCount],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setVisibleCount(BATCH_SIZE);
|
||||
listRef.current?.scrollTo({ top: 0 });
|
||||
}, [query, filteredIcons.length]);
|
||||
|
||||
const handleScroll = useCallback(
|
||||
(event: React.UIEvent<HTMLDivElement>) => {
|
||||
const target = event.currentTarget;
|
||||
if (
|
||||
target.scrollTop + target.clientHeight >=
|
||||
target.scrollHeight - 64
|
||||
) {
|
||||
setVisibleCount((prev) =>
|
||||
Math.min(prev + BATCH_SIZE, filteredIcons.length),
|
||||
);
|
||||
}
|
||||
},
|
||||
[filteredIcons.length],
|
||||
);
|
||||
|
||||
const handleSelect = (iconName: string) => {
|
||||
onSelect(iconName);
|
||||
onCloseRequest();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="lucide-icon-modal">
|
||||
<div className="lucide-icon-modal__header">
|
||||
<div>
|
||||
<h3 className="modal-title">Select Lucide icon</h3>
|
||||
<p className="lucide-icon-modal__count">
|
||||
Showing {visibleIcons.length} of {filteredIcons.length}{" "}
|
||||
( Total {iconIds.length})
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="modal-close-button lucide-icon-modal__close"
|
||||
onClick={onCloseRequest}
|
||||
aria-label="Close"
|
||||
>
|
||||
<ObsidianIcon name="x" size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
variant="search"
|
||||
value={query}
|
||||
onChange={setQuery}
|
||||
placeholder="Search icons…"
|
||||
className="lucide-icon-modal__search"
|
||||
/>
|
||||
|
||||
<label className="lucide-icon-modal__toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showLabels}
|
||||
onChange={(e) => setShowLabels(e.target.checked)}
|
||||
/>
|
||||
Show icon names
|
||||
</label>
|
||||
|
||||
<div
|
||||
ref={listRef}
|
||||
className={`lucide-icon-modal__list${showLabels ? "" : " lucide-icon-modal__list--icons-only"}`}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{visibleIcons.map((iconName) => (
|
||||
<button
|
||||
key={iconName}
|
||||
type="button"
|
||||
className={`lucide-icon-modal__option${iconName === initialValue ? " lucide-icon-modal__option--selected" : ""}${showLabels ? "" : " lucide-icon-modal__option--compact"}`}
|
||||
onClick={() => handleSelect(iconName)}
|
||||
>
|
||||
<StatusIcon
|
||||
icon={iconName}
|
||||
lucideIcon={iconName}
|
||||
size={16}
|
||||
/>
|
||||
<span>{iconName}</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
{visibleIcons.length === 0 && (
|
||||
<div className="lucide-icon-modal__empty">
|
||||
No icons match “{query.trim()}”.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type ModalOptions = {
|
||||
initialValue?: string;
|
||||
onSelect: (value: string) => void;
|
||||
};
|
||||
|
||||
export class LucideIconModal extends Modal {
|
||||
private root: Root | null = null;
|
||||
private readonly options: ModalOptions;
|
||||
|
||||
constructor(app: App, options: ModalOptions) {
|
||||
super(app);
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.contentEl.empty();
|
||||
this.contentEl.addClass("lucide-icon-modal-wrapper");
|
||||
this.root = createRoot(this.contentEl);
|
||||
this.root.render(
|
||||
<LucideIconModalContent
|
||||
initialValue={this.options.initialValue}
|
||||
onSelect={this.options.onSelect}
|
||||
onCloseRequest={() => this.close()}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
this.root?.unmount();
|
||||
this.root = null;
|
||||
this.contentEl.empty();
|
||||
}
|
||||
}
|
||||
79
components/SettingsUI/LucideIconPicker.tsx
Normal file
79
components/SettingsUI/LucideIconPicker.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import React from "react";
|
||||
import { StatusIcon } from "@/components/atoms/StatusIcon";
|
||||
import { LucideIconModal } from "./LucideIconModal";
|
||||
import { App } from "obsidian";
|
||||
import { Input } from "@/components/atoms/Input";
|
||||
|
||||
const getAppInstance = (): App | undefined => {
|
||||
return (window as typeof window & { app?: App }).app;
|
||||
};
|
||||
|
||||
export interface LucideIconPickerProps {
|
||||
value?: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
allowTextInput?: boolean;
|
||||
allowClear?: boolean;
|
||||
}
|
||||
|
||||
export const LucideIconPicker: React.FC<LucideIconPickerProps> = ({
|
||||
value = "",
|
||||
onChange,
|
||||
placeholder = "Choose Lucide icon",
|
||||
allowTextInput = false,
|
||||
allowClear = true,
|
||||
}) => {
|
||||
const openModal = () => {
|
||||
const app = getAppInstance();
|
||||
if (!app) {
|
||||
console.warn("Cannot open Lucide icon modal without Obsidian app");
|
||||
return;
|
||||
}
|
||||
const modal = new LucideIconModal(app, {
|
||||
initialValue: value,
|
||||
onSelect: (icon) => onChange(icon),
|
||||
});
|
||||
modal.open();
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
onChange("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="lucide-icon-picker">
|
||||
{allowTextInput && (
|
||||
<Input
|
||||
variant="text"
|
||||
value={value}
|
||||
onChange={(val) => onChange(val)}
|
||||
placeholder="Emoji or icon name"
|
||||
className="lucide-icon-picker__text-input"
|
||||
/>
|
||||
)}
|
||||
<div className="lucide-icon-picker__row">
|
||||
<button
|
||||
type="button"
|
||||
className="lucide-icon-picker__modal-button"
|
||||
onClick={openModal}
|
||||
>
|
||||
<span className="lucide-icon-picker__preview">
|
||||
<StatusIcon icon={value} lucideIcon={value} size={18} />
|
||||
</span>
|
||||
<span className="lucide-icon-picker__label">
|
||||
{value || placeholder}
|
||||
</span>
|
||||
</button>
|
||||
{allowClear && value && (
|
||||
<button
|
||||
type="button"
|
||||
className="lucide-icon-picker__clear-trigger"
|
||||
onClick={handleClear}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</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">
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import React from "react";
|
|||
import { Input } from "../atoms/Input";
|
||||
import { Select } from "../atoms/Select";
|
||||
import { SettingItem } from "./SettingItem";
|
||||
import { IconSelectionField } from "./IconSelectionField";
|
||||
|
||||
export type Props = {
|
||||
settings: PluginSettings;
|
||||
|
|
@ -268,14 +269,22 @@ 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."
|
||||
vertical
|
||||
>
|
||||
<Input
|
||||
variant="text"
|
||||
value={settings.unknownStatusIcon}
|
||||
onChange={(value) => onChange("unknownStatusIcon", value)}
|
||||
placeholder="❓"
|
||||
style={{ maxWidth: "150px" }}
|
||||
<IconSelectionField
|
||||
emojiValue={settings.unknownStatusIcon}
|
||||
onEmojiChange={(value) =>
|
||||
onChange("unknownStatusIcon", value)
|
||||
}
|
||||
emojiPlaceholder="❓"
|
||||
emojiHint="Type any emoji or text fallback."
|
||||
lucideValue={settings.unknownStatusLucideIcon || ""}
|
||||
onLucideChange={(value) =>
|
||||
onChange("unknownStatusLucideIcon", value)
|
||||
}
|
||||
lucidePlaceholder="Choose Lucide icon"
|
||||
lucideHint="Matches the Obsidian toolbar style."
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -17,6 +18,7 @@ export type Props = {
|
|||
showIcon: boolean;
|
||||
showText: boolean;
|
||||
icon: string;
|
||||
lucideIcon?: string;
|
||||
color: string;
|
||||
};
|
||||
};
|
||||
|
|
@ -51,13 +53,15 @@ export const StatusBar: FC<Props> = ({
|
|||
}}
|
||||
>
|
||||
{noStatusConfig.showIcon && (
|
||||
<span
|
||||
<StatusIcon
|
||||
icon={noStatusConfig.icon}
|
||||
lucideIcon={noStatusConfig.lucideIcon}
|
||||
size={14}
|
||||
className="status-bar-no-status-icon"
|
||||
style={{
|
||||
marginRight: noStatusConfig.showText ? "4px" : "0",
|
||||
}}
|
||||
>
|
||||
{noStatusConfig.icon}
|
||||
</span>
|
||||
/>
|
||||
)}
|
||||
{noStatusConfig.showText && noStatusConfig.text}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
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;
|
||||
onClick: () => void;
|
||||
unknownStatusConfig: {
|
||||
icon: string;
|
||||
lucideIcon?: string;
|
||||
color: string;
|
||||
};
|
||||
}
|
||||
|
|
@ -40,12 +42,13 @@ export const EditorToolbarButton: FC<EditorToolbarButtonProps> = memo(
|
|||
onMouseLeave={handleMouseLeave}
|
||||
aria-label="Add status to note"
|
||||
>
|
||||
<span
|
||||
<StatusIcon
|
||||
icon={unknownStatusConfig.icon}
|
||||
lucideIcon={unknownStatusConfig.lucideIcon}
|
||||
size={16}
|
||||
className="editor-toolbar-button__icon"
|
||||
style={{ color: unknownStatusConfig.color }}
|
||||
>
|
||||
{unknownStatusConfig.icon}
|
||||
</span>
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
@ -63,16 +66,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 +92,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>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { NoteStatus } from "@/types/noteStatus";
|
||||
import { FC, memo, useState } from "react";
|
||||
import { FC, ReactNode, memo, useMemo, useState } from "react";
|
||||
import { getStatusTooltip } from "@/utils/statusUtils";
|
||||
import { ObsidianIcon } from "./ObsidianIcon";
|
||||
import { StatusIcon } from "./StatusIcon";
|
||||
|
||||
export type StatusDisplayVariant = "chip" | "badge" | "template";
|
||||
|
||||
|
|
@ -11,6 +12,7 @@ interface StatusDisplayProps {
|
|||
removable?: boolean;
|
||||
hasNameConflicts?: boolean;
|
||||
templateNameMode?: "always" | "never" | "auto";
|
||||
icon?: ReactNode;
|
||||
onRemove?: () => void;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
|
@ -22,6 +24,7 @@ export const StatusDisplay: FC<StatusDisplayProps> = memo(
|
|||
removable = false,
|
||||
hasNameConflicts = false,
|
||||
templateNameMode = "auto",
|
||||
icon,
|
||||
onRemove,
|
||||
onClick,
|
||||
}) => {
|
||||
|
|
@ -62,6 +65,19 @@ export const StatusDisplay: FC<StatusDisplayProps> = memo(
|
|||
}
|
||||
};
|
||||
|
||||
const defaultIcon = useMemo(() => {
|
||||
const baseSize = variant === "chip" ? 16 : 14;
|
||||
return (
|
||||
<StatusIcon
|
||||
icon={status.icon}
|
||||
lucideIcon={status.lucideIcon}
|
||||
size={baseSize}
|
||||
/>
|
||||
);
|
||||
}, [status.icon, status.lucideIcon, variant]);
|
||||
|
||||
const iconNode = icon ?? defaultIcon;
|
||||
|
||||
if (variant === "chip") {
|
||||
return (
|
||||
<div
|
||||
|
|
@ -82,9 +98,7 @@ export const StatusDisplay: FC<StatusDisplayProps> = memo(
|
|||
}}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<span className="note-status-chip-icon">
|
||||
{status.icon ? status.icon : "📝"}
|
||||
</span>
|
||||
<span className="note-status-chip-icon">{iconNode}</span>
|
||||
<span className="note-status-chip-text">
|
||||
{getDisplayName()}
|
||||
</span>
|
||||
|
|
@ -121,9 +135,7 @@ export const StatusDisplay: FC<StatusDisplayProps> = memo(
|
|||
onClick={handleClick}
|
||||
>
|
||||
<div className="status-badge-item">
|
||||
<span className="status-badge-icon">
|
||||
{status.icon ? status.icon : "📝"}
|
||||
</span>
|
||||
<span className="status-badge-icon">{iconNode}</span>
|
||||
<span className="status-badge-text">
|
||||
{getDisplayName()}
|
||||
</span>
|
||||
|
|
@ -144,7 +156,8 @@ export const StatusDisplay: FC<StatusDisplayProps> = memo(
|
|||
}
|
||||
/>
|
||||
<span>
|
||||
{status.icon ? status.icon : "📝"} {getDisplayName()}
|
||||
<span style={{ marginRight: "4px" }}>{iconNode}</span>
|
||||
{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,10 @@
|
|||
import React, { CSSProperties, FC, memo } from "react";
|
||||
import React, { CSSProperties, FC, ReactNode, memo } from "react";
|
||||
|
||||
type IconFrameMode = "always" | "never";
|
||||
type IconColorMode = "status" | "theme";
|
||||
|
||||
export interface StatusIconPreviewProps {
|
||||
icon?: string;
|
||||
icon?: ReactNode;
|
||||
color?: string;
|
||||
count?: number;
|
||||
iconFrameMode?: IconFrameMode;
|
||||
|
|
@ -33,7 +33,6 @@ export const StatusIconPreview: FC<StatusIconPreviewProps> = memo(
|
|||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
}) => {
|
||||
const iconDisplay = icon?.trim().length ? icon : "📝";
|
||||
const useStatusColor = iconColorMode === "status";
|
||||
const appliedColor = useStatusColor && color ? color.trim() : undefined;
|
||||
|
||||
|
|
@ -58,7 +57,7 @@ export const StatusIconPreview: FC<StatusIconPreviewProps> = memo(
|
|||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<span className="status-minimal__icon">{iconDisplay}</span>
|
||||
<span className="status-minimal__icon">{icon ?? "📝"}</span>
|
||||
{count && count > 1 && (
|
||||
<span
|
||||
className="status-minimal__count"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { NoteStatus } from "@/types/noteStatus";
|
|||
import { SelectableListItem } from "./SelectableListItem";
|
||||
import { getStatusTooltip, isStatusSelected } from "@/utils/statusUtils";
|
||||
import { StatusIconPreview } from "./StatusIconPreview";
|
||||
import { StatusIcon } from "./StatusIcon";
|
||||
|
||||
interface StatusOptionProps {
|
||||
status: NoteStatus;
|
||||
|
|
@ -33,7 +34,13 @@ export const StatusModalOption: React.FC<StatusOptionProps> = memo(
|
|||
icon={
|
||||
<div className="note-status-option__icon-wrapper">
|
||||
<StatusIconPreview
|
||||
icon={status.icon}
|
||||
icon={
|
||||
<StatusIcon
|
||||
icon={status.icon}
|
||||
lucideIcon={status.lucideIcon}
|
||||
size={13}
|
||||
/>
|
||||
}
|
||||
color={status.color}
|
||||
iconFrameMode={iconFrameMode}
|
||||
iconColorMode={iconColorMode}
|
||||
|
|
@ -101,7 +108,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}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export const DEFAULT_PLUGIN_SETTINGS: PluginSettings = {
|
|||
quickStatusCommands: ["active", "completed"], // Add default quick commands
|
||||
// Unknown status customization
|
||||
unknownStatusIcon: "❓",
|
||||
unknownStatusLucideIcon: "",
|
||||
unknownStatusColor: "#8b949e",
|
||||
statusBarNoStatusText: "No status",
|
||||
statusBarShowNoStatusIcon: false,
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ export class FileExplorerIntegration implements IElementProcessor {
|
|||
key === "fileExplorerIconFrame" ||
|
||||
key === "fileExplorerIconColorMode" ||
|
||||
key === "unknownStatusIcon" ||
|
||||
key === "unknownStatusLucideIcon" ||
|
||||
key === "unknownStatusColor" ||
|
||||
key === "templates"
|
||||
) {
|
||||
|
|
@ -117,6 +118,7 @@ export class FileExplorerIntegration implements IElementProcessor {
|
|||
const settings = settingsService.settings;
|
||||
return {
|
||||
icon: settings.unknownStatusIcon || "❓",
|
||||
lucideIcon: settings.unknownStatusLucideIcon || "",
|
||||
color: settings.unknownStatusColor || "#8b949e",
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ export class StatusBarIntegration {
|
|||
}
|
||||
if (
|
||||
key === "unknownStatusIcon" ||
|
||||
key === "unknownStatusLucideIcon" ||
|
||||
key === "unknownStatusColor" ||
|
||||
key === "statusBarNoStatusText" ||
|
||||
key === "statusBarShowNoStatusIcon" ||
|
||||
|
|
@ -123,6 +124,7 @@ export class StatusBarIntegration {
|
|||
showIcon: settings.statusBarShowNoStatusIcon || false,
|
||||
showText: settings.statusBarShowNoStatusText ?? true,
|
||||
icon: settings.unknownStatusIcon || "❓",
|
||||
lucideIcon: settings.unknownStatusLucideIcon || "",
|
||||
color: settings.unknownStatusColor || "#8b949e",
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ export class EditorToolbarIntegration {
|
|||
}
|
||||
if (
|
||||
key === "unknownStatusIcon" ||
|
||||
key === "unknownStatusLucideIcon" ||
|
||||
key === "unknownStatusColor"
|
||||
) {
|
||||
this.renderAllButtons();
|
||||
|
|
@ -378,6 +379,7 @@ export class EditorToolbarIntegration {
|
|||
const settings = settingsService.settings;
|
||||
return {
|
||||
icon: settings.unknownStatusIcon || "❓",
|
||||
lucideIcon: settings.unknownStatusLucideIcon || "",
|
||||
color: settings.unknownStatusColor || "#8b949e",
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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[] => {
|
||||
|
|
|
|||
1463
styles.css
1463
styles.css
File diff suppressed because one or more lines are too long
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
186
styles/components/lucide-icon-picker.css
Normal file
186
styles/components/lucide-icon-picker.css
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
.lucide-icon-picker {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-2-1);
|
||||
}
|
||||
|
||||
.lucide-icon-picker__text-input {
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.lucide-icon-picker__row {
|
||||
display: flex;
|
||||
gap: var(--size-2-1);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.lucide-icon-picker__modal-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--size-2-1);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-m);
|
||||
padding: var(--size-2-1) var(--size-4-1);
|
||||
background: var(--background-secondary);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border var(--anim-duration-fast) ease,
|
||||
background var(--anim-duration-fast) ease;
|
||||
min-width: 220px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.lucide-icon-picker__modal-button:hover {
|
||||
border-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.lucide-icon-picker__preview {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.lucide-icon-picker__label {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.lucide-icon-picker__clear-trigger {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-smaller);
|
||||
cursor: pointer;
|
||||
padding: var(--size-2-1) var(--size-2-2);
|
||||
}
|
||||
|
||||
.lucide-icon-picker__clear-trigger:hover {
|
||||
color: var(--text-normal);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.lucide-icon-modal-wrapper {
|
||||
padding: var(--size-4-2);
|
||||
}
|
||||
|
||||
.lucide-icon-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-2-1);
|
||||
}
|
||||
|
||||
.lucide-icon-modal__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--size-2-2);
|
||||
}
|
||||
|
||||
.lucide-icon-modal__count {
|
||||
margin: 0;
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.lucide-icon-modal__close {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.lucide-icon-modal__search {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.lucide-icon-modal__toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--size-2-1);
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.lucide-icon-modal__list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: var(--size-2-1);
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
padding-right: var(--size-2-1);
|
||||
}
|
||||
|
||||
.lucide-icon-modal__list--icons-only {
|
||||
grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
|
||||
}
|
||||
|
||||
.lucide-icon-modal__option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-2-1);
|
||||
padding: var(--size-2-1);
|
||||
border-radius: var(--radius-s);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
background: var(--background-secondary);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-ui-smaller);
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.lucide-icon-modal__option:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.lucide-icon-modal__option--selected {
|
||||
border-color: var(--interactive-accent);
|
||||
background: var(--background-modifier-success);
|
||||
}
|
||||
|
||||
.lucide-icon-modal__option--compact {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.lucide-icon-modal__option--compact span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.lucide-icon-modal__empty {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
padding: var(--size-4-2);
|
||||
}
|
||||
|
||||
.icon-selection-field {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--size-4-2);
|
||||
}
|
||||
|
||||
.icon-selection-field__column {
|
||||
flex: 1;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.icon-selection-field__label {
|
||||
display: block;
|
||||
font-size: var(--font-ui-smaller);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-muted);
|
||||
margin-bottom: var(--size-2-1);
|
||||
}
|
||||
|
||||
.icon-selection-field__emoji-input {
|
||||
max-width: 260px;
|
||||
}
|
||||
|
||||
.icon-selection-field__hint {
|
||||
margin-top: var(--size-2-1);
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
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,7 @@
|
|||
@import "components/group-label.css";
|
||||
@import "components/collapsible-counter.css";
|
||||
@import "components/status-display.css";
|
||||
@import "components/lucide-icon-picker.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
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export type PluginSettings = {
|
|||
quickStatusCommands: string[];
|
||||
// Unknown status customization
|
||||
unknownStatusIcon: string; // Custom icon for unknown status
|
||||
unknownStatusLucideIcon?: string; // Optional Lucide icon for unknown status
|
||||
unknownStatusColor: string; // Custom hex color for unknown status
|
||||
statusBarNoStatusText: string; // Custom text for status bar when no status
|
||||
statusBarShowNoStatusIcon: boolean; // Whether to show icon in status bar for no status
|
||||
|
|
|
|||
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