mirror of
https://github.com/devonthesofa/obsidian-note-status.git
synced 2026-07-22 05:45:04 +00:00
feat: improve template marketplace UI/UX with badges, 'Modified' status, and read-only marketplace metadata
This commit is contained in:
parent
4cf73df730
commit
6ab760df98
6 changed files with 167 additions and 31 deletions
|
|
@ -53,11 +53,16 @@ export const MarketplaceBrowseModal: React.FC<MarketplaceBrowseModalProps> = ({
|
|||
<div className="marketplace-card-title">
|
||||
{template.name}
|
||||
</div>
|
||||
{template.author && (
|
||||
<div className="marketplace-card-author">
|
||||
by {template.author}
|
||||
</div>
|
||||
)}
|
||||
<div className="marketplace-card-meta">
|
||||
<span className="template-badge badge-info">
|
||||
Marketplace
|
||||
</span>
|
||||
{template.author && (
|
||||
<div className="marketplace-card-author">
|
||||
by {template.author}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="marketplace-card-description">
|
||||
{template.description}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import React, { useState, useCallback } from "react";
|
||||
import React, { useState, useCallback, useMemo } from "react";
|
||||
import { StatusTemplate } from "@/types/pluginSettings";
|
||||
import { NoteStatus } from "@/types/noteStatus";
|
||||
import { Input } from "@/components/atoms/Input";
|
||||
import { SettingItem } from "./SettingItem";
|
||||
import { CustomStatusItem } from "./CustomStatusItem";
|
||||
import { isCustomTemplate } from "@/utils/templateUtils";
|
||||
|
||||
interface TemplateEditorModalProps {
|
||||
template?: StatusTemplate;
|
||||
|
|
@ -16,6 +17,10 @@ export const TemplateEditorModal: React.FC<TemplateEditorModalProps> = ({
|
|||
onSave,
|
||||
onCancel,
|
||||
}) => {
|
||||
const isCustom = useMemo(
|
||||
() => (template ? isCustomTemplate(template) : true),
|
||||
[template],
|
||||
);
|
||||
const [name, setName] = useState(template?.name || "");
|
||||
const [description, setDescription] = useState(template?.description || "");
|
||||
const [author, setAuthor] = useState(template?.author || "");
|
||||
|
|
@ -151,25 +156,35 @@ export const TemplateEditorModal: React.FC<TemplateEditorModalProps> = ({
|
|||
|
||||
<SettingItem
|
||||
name="Author (Optional)"
|
||||
description="Your name or community name"
|
||||
description={
|
||||
isCustom
|
||||
? "Your name or community name"
|
||||
: "Original author of the marketplace template"
|
||||
}
|
||||
>
|
||||
<Input
|
||||
variant="text"
|
||||
value={author}
|
||||
onChange={setAuthor}
|
||||
placeholder="e.g. Jane Doe"
|
||||
disabled={!isCustom}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
name="GitHub URL (Optional)"
|
||||
description="Link to your GitHub profile or repository"
|
||||
description={
|
||||
isCustom
|
||||
? "Link to your GitHub profile or repository"
|
||||
: "Official repository of the marketplace template"
|
||||
}
|
||||
>
|
||||
<Input
|
||||
variant="text"
|
||||
value={github}
|
||||
onChange={setGithub}
|
||||
placeholder="e.g. https://github.com/janedoe"
|
||||
disabled={!isCustom}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { useMemo } from "react";
|
|||
import { StatusTemplate } from "@/types/pluginSettings";
|
||||
import { StatusDisplay } from "../atoms/StatusDisplay";
|
||||
import { SelectableListItem } from "../atoms/SelectableListItem";
|
||||
import { isCustomTemplate } from "@/utils/templateUtils";
|
||||
import { isCustomTemplate, isTemplateModified } from "@/utils/templateUtils";
|
||||
import { ObsidianIcon } from "../atoms/ObsidianIcon";
|
||||
|
||||
interface TemplateItemProps {
|
||||
|
|
@ -22,9 +22,10 @@ export const TemplateItem: React.FC<TemplateItemProps> = ({
|
|||
onDelete,
|
||||
onShare,
|
||||
}) => {
|
||||
const isCustom = useMemo(
|
||||
() => isCustomTemplate(template.id),
|
||||
[template.id],
|
||||
const isCustom = useMemo(() => isCustomTemplate(template), [template]);
|
||||
const isModified = useMemo(
|
||||
() => !isCustom && isTemplateModified(template),
|
||||
[isCustom, template],
|
||||
);
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
|
|
@ -36,7 +37,7 @@ export const TemplateItem: React.FC<TemplateItemProps> = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className={`template-item ${isEnabled ? "enabled" : ""}`}>
|
||||
<div className={`template-item ${isEnabled ? "enabled" : "disabled"}`}>
|
||||
<SelectableListItem
|
||||
selected={isEnabled}
|
||||
onClick={handleClick}
|
||||
|
|
@ -55,11 +56,31 @@ export const TemplateItem: React.FC<TemplateItemProps> = ({
|
|||
<span className="setting-item-name">
|
||||
{template.name}
|
||||
</span>
|
||||
{isCustomTemplate(template.id) && (
|
||||
<span className="template-custom-badge">
|
||||
Custom
|
||||
</span>
|
||||
)}
|
||||
<div className="template-badges">
|
||||
{isEnabled ? (
|
||||
<span className="template-badge badge-success">
|
||||
Active
|
||||
</span>
|
||||
) : (
|
||||
<span className="template-badge badge-muted">
|
||||
Inactive
|
||||
</span>
|
||||
)}
|
||||
{isCustom ? (
|
||||
<span className="template-badge badge-accent">
|
||||
Custom
|
||||
</span>
|
||||
) : (
|
||||
<span className="template-badge badge-info">
|
||||
Marketplace
|
||||
</span>
|
||||
)}
|
||||
{isModified && (
|
||||
<span className="template-badge badge-warning">
|
||||
Modified
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="setting-item-description">
|
||||
{template.description}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ interface BaseInputProps {
|
|||
style?: React.CSSProperties;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface TextInputProps extends BaseInputProps {
|
||||
|
|
@ -38,6 +39,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||
style,
|
||||
onFocus,
|
||||
onBlur,
|
||||
disabled,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
|
|
@ -67,6 +69,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||
style={style}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -84,12 +84,20 @@
|
|||
box-shadow: 0 2px 8px var(--shadow-color);
|
||||
}
|
||||
|
||||
.template-item.disabled {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.template-item.enabled {
|
||||
background: var(--background-modifier-hover);
|
||||
border-color: var(--interactive-accent);
|
||||
box-shadow: 0 0 0 1px var(--interactive-accent);
|
||||
}
|
||||
|
||||
.template-item.enabled .setting-item-name {
|
||||
font-weight: var(--font-bold);
|
||||
}
|
||||
|
||||
.template-item.enabled:hover {
|
||||
box-shadow:
|
||||
0 2px 8px var(--shadow-color),
|
||||
|
|
@ -114,20 +122,47 @@
|
|||
margin-bottom: var(--size-2-1);
|
||||
}
|
||||
|
||||
.template-custom-badge {
|
||||
.template-badges {
|
||||
display: flex;
|
||||
gap: var(--size-2-2);
|
||||
}
|
||||
|
||||
.template-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--size-2-1);
|
||||
padding: var(--size-2-1) var(--size-2-2);
|
||||
background: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
font-size: var(--font-ui-smaller);
|
||||
font-weight: var(--font-medium);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-s);
|
||||
font-size: 10px;
|
||||
font-weight: var(--font-bold);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background-color: var(--background-modifier-success);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
.badge-muted {
|
||||
background-color: var(--background-modifier-border);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.badge-accent {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background-color: var(--background-modifier-warning);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
background-color: var(--background-modifier-info);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
/* Template Item Actions */
|
||||
.template-item-actions {
|
||||
display: flex;
|
||||
|
|
@ -386,13 +421,20 @@
|
|||
}
|
||||
|
||||
.marketplace-card-header {
|
||||
margin-bottom: var(--size-2-2);
|
||||
margin-bottom: var(--size-4-2);
|
||||
}
|
||||
|
||||
.marketplace-card-title {
|
||||
font-weight: var(--font-bold);
|
||||
font-size: var(--font-ui-medium);
|
||||
font-size: var(--font-ui-large);
|
||||
color: var(--text-normal);
|
||||
margin-bottom: var(--size-2-2);
|
||||
}
|
||||
|
||||
.marketplace-card-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-2-2);
|
||||
}
|
||||
|
||||
.marketplace-card-author {
|
||||
|
|
|
|||
|
|
@ -3,11 +3,61 @@ import settingsService from "@/core/settingsService";
|
|||
import { PREDEFINED_TEMPLATES } from "@/constants/predefinedTemplates";
|
||||
|
||||
/**
|
||||
* Get all enabled templates
|
||||
* Check if a template is a custom user template (not from marketplace)
|
||||
*/
|
||||
export const isCustomTemplate = (templateId: string): boolean => {
|
||||
const i = PREDEFINED_TEMPLATES.findIndex((t) => t.id === templateId);
|
||||
return i === -1;
|
||||
export const isCustomTemplate = (template: StatusTemplate): boolean => {
|
||||
// Either matches an original marketplace template ID
|
||||
const matchesPredefined = PREDEFINED_TEMPLATES.some(
|
||||
(pt) =>
|
||||
pt.id === template.id ||
|
||||
pt.name.toLowerCase() === template.name.toLowerCase(),
|
||||
);
|
||||
if (matchesPredefined) return false;
|
||||
|
||||
// Or has metadata from marketplace
|
||||
const hasMetadata = !!(template.author || template.github);
|
||||
if (hasMetadata) return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a marketplace template has been modified by the user
|
||||
*/
|
||||
export const isTemplateModified = (template: StatusTemplate): boolean => {
|
||||
const original = PREDEFINED_TEMPLATES.find(
|
||||
(pt) =>
|
||||
pt.id === template.id ||
|
||||
pt.name.toLowerCase() === template.name.toLowerCase(),
|
||||
);
|
||||
|
||||
if (!original) return false;
|
||||
|
||||
// Compare relevant fields to see if anything changed
|
||||
// We stringify for a deep comparison of statuses
|
||||
const originalData = JSON.stringify({
|
||||
name: original.name,
|
||||
description: original.description,
|
||||
statuses: original.statuses.map((s) => ({
|
||||
name: s.name,
|
||||
icon: s.icon,
|
||||
lucideIcon: s.lucideIcon,
|
||||
color: s.color,
|
||||
})),
|
||||
});
|
||||
|
||||
const currentData = JSON.stringify({
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
statuses: template.statuses.map((s) => ({
|
||||
name: s.name,
|
||||
icon: s.icon,
|
||||
lucideIcon: s.lucideIcon,
|
||||
color: s.color,
|
||||
})),
|
||||
});
|
||||
|
||||
return originalData !== currentData;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue