From 6ab760df98f8abac76fc97b679d20d75587a0ef4 Mon Sep 17 00:00:00 2001 From: Aleix Soler Date: Wed, 18 Mar 2026 16:45:47 +0100 Subject: [PATCH] feat: improve template marketplace UI/UX with badges, 'Modified' status, and read-only marketplace metadata --- .../SettingsUI/MarketplaceBrowseModal.tsx | 15 +++-- components/SettingsUI/TemplateEditorModal.tsx | 21 ++++++- components/SettingsUI/TemplateItem.tsx | 41 +++++++++---- components/atoms/Input.tsx | 3 + styles/components/template-settings.css | 60 ++++++++++++++++--- utils/templateUtils.ts | 58 ++++++++++++++++-- 6 files changed, 167 insertions(+), 31 deletions(-) diff --git a/components/SettingsUI/MarketplaceBrowseModal.tsx b/components/SettingsUI/MarketplaceBrowseModal.tsx index 47e3146..578568e 100644 --- a/components/SettingsUI/MarketplaceBrowseModal.tsx +++ b/components/SettingsUI/MarketplaceBrowseModal.tsx @@ -53,11 +53,16 @@ export const MarketplaceBrowseModal: React.FC = ({
{template.name}
- {template.author && ( -
- by {template.author} -
- )} +
+ + Marketplace + + {template.author && ( +
+ by {template.author} +
+ )} +
{template.description} diff --git a/components/SettingsUI/TemplateEditorModal.tsx b/components/SettingsUI/TemplateEditorModal.tsx index f758fb3..741a1f2 100644 --- a/components/SettingsUI/TemplateEditorModal.tsx +++ b/components/SettingsUI/TemplateEditorModal.tsx @@ -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 = ({ 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 = ({ diff --git a/components/SettingsUI/TemplateItem.tsx b/components/SettingsUI/TemplateItem.tsx index eeb2daa..874c589 100644 --- a/components/SettingsUI/TemplateItem.tsx +++ b/components/SettingsUI/TemplateItem.tsx @@ -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 = ({ 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 = ({ }; return ( -
+
= ({ {template.name} - {isCustomTemplate(template.id) && ( - - Custom - - )} +
+ {isEnabled ? ( + + Active + + ) : ( + + Inactive + + )} + {isCustom ? ( + + Custom + + ) : ( + + Marketplace + + )} + {isModified && ( + + Modified + + )} +
{template.description} diff --git a/components/atoms/Input.tsx b/components/atoms/Input.tsx index 142f210..4730079 100644 --- a/components/atoms/Input.tsx +++ b/components/atoms/Input.tsx @@ -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( style, onFocus, onBlur, + disabled, }, ref, ) => { @@ -67,6 +69,7 @@ export const Input = React.forwardRef( style={style} onFocus={onFocus} onBlur={onBlur} + disabled={disabled} /> ); }, diff --git a/styles/components/template-settings.css b/styles/components/template-settings.css index b26a7fb..8f1fb20 100644 --- a/styles/components/template-settings.css +++ b/styles/components/template-settings.css @@ -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 { diff --git a/utils/templateUtils.ts b/utils/templateUtils.ts index 168966e..82f1f2a 100644 --- a/utils/templateUtils.ts +++ b/utils/templateUtils.ts @@ -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; }; /**