diff --git a/README.md b/README.md index e73ee19..dc931f5 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,39 @@ It works on both Markdown and non-Markdown files, integrates into multiple UI su - ![status-dashboard-2](images/status-dashboard-2.png) - ![status-dashboard-3](images/status-dashboard-3.png) +## Template Marketplace + +You can contribute your own status templates to the plugin! + +### How to contribute (The Easy Way) + +1. Create a custom template in the plugin settings. +2. Click the **Share** (πŸ“€) icon on your custom template. +3. Follow the instructions in the modal to copy the JSON and submit a Pull Request to the [official repository](https://github.com/devonthesofa/obsidian-note-status). +4. Maintainers will review your submission and accept it if it passes the revision! + +### How to contribute (Manual) + +1. Fork the repository. + ... +2. Create a new JSON file in the `templates/` folder (e.g., `templates/my-awesome-workflow.json`). +3. Follow this format: + ```json + { + "id": "my-awesome-workflow", + "name": "My Awesome Workflow", + "description": "A workflow for doing awesome things", + "authorGithub": "your-username", + "statuses": [ + { "name": "todo", "icon": "πŸ“", "color": "#ff0000", "templateId": "my-awesome-workflow" }, + { "name": "done", "icon": "βœ…", "color": "#00ff00", "templateId": "my-awesome-workflow" } + ] + } + ``` +4. Submit a Pull Request with your JSON file! + +Once accepted and merged, your template will be automatically included in the next build and available in the marketplace for all users. + ## Installation ### Community Plugin Store (recommended) diff --git a/components/SettingsUI/MarketplaceBrowseModal.tsx b/components/SettingsUI/MarketplaceBrowseModal.tsx new file mode 100644 index 0000000..f3bfa65 --- /dev/null +++ b/components/SettingsUI/MarketplaceBrowseModal.tsx @@ -0,0 +1,131 @@ +import React, { useState, useMemo } from "react"; +import { StatusTemplate } from "@/types/pluginSettings"; +import { PREDEFINED_TEMPLATES } from "@/constants/predefinedTemplates"; +import { StatusDisplay } from "../atoms/StatusDisplay"; +import { ObsidianIcon } from "../atoms/ObsidianIcon"; +import { SearchFilter } from "../atoms/SearchFilter"; + +interface MarketplaceBrowseModalProps { + installedTemplates: StatusTemplate[]; + onInstall: (template: StatusTemplate) => void; + onClose: () => void; +} + +export const MarketplaceBrowseModal: React.FC = ({ + installedTemplates, + onInstall, + onClose, +}) => { + const [searchQuery, setSearchQuery] = useState(""); + + const filteredTemplates = useMemo(() => { + return PREDEFINED_TEMPLATES.filter( + (t) => + t.name.toLowerCase().includes(searchQuery.toLowerCase()) || + t.description.toLowerCase().includes(searchQuery.toLowerCase()), + ); + }, [searchQuery]); + + return ( +
+
+
+

Template Marketplace

+ +
+
+ +
+
+ {filteredTemplates.map((template) => { + const isInstalled = installedTemplates.some( + (t) => + t.name.toLowerCase() === + template.name.toLowerCase(), + ); + return ( +
+
+
+ {template.name} +
+
+ + Marketplace + + {template.authorGithub && ( + + )} +
+
+
+ {template.description} +
+
+ {template.statuses.map((status, idx) => ( + + ))} +
+
+ +
+
+ ); + })} +
+ {filteredTemplates.length === 0 && ( +
+ +

No templates found matching your search.

+
+ )} +
+ +
+ +
+
+ ); +}; diff --git a/components/SettingsUI/MarketplaceShareModal.tsx b/components/SettingsUI/MarketplaceShareModal.tsx new file mode 100644 index 0000000..84bdc31 --- /dev/null +++ b/components/SettingsUI/MarketplaceShareModal.tsx @@ -0,0 +1,107 @@ +import React, { useState, useCallback } from "react"; +import { StatusTemplate } from "@/types/pluginSettings"; +import { ObsidianIcon } from "../atoms/ObsidianIcon"; + +interface MarketplaceShareModalProps { + template: StatusTemplate; + onClose: () => void; +} + +export const MarketplaceShareModal: React.FC = ({ + template, + onClose, +}) => { + const [copied, setCopied] = useState(false); + + const marketplaceTemplate = { + ...template, + id: template.id.replace(/^custom-/, ""), + statuses: template.statuses.map((s) => ({ + ...s, + templateId: template.id.replace(/^custom-/, ""), + })), + }; + + const jsonString = JSON.stringify(marketplaceTemplate, null, 2); + const filename = `${marketplaceTemplate.id}.json`; + + const handleCopy = useCallback(() => { + navigator.clipboard.writeText(jsonString); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }, [jsonString]); + + return ( +
+
+

Submit to Marketplace

+
+ +
+

+ Share your template with the community by following these + steps: +

+ +
    +
  1. + Copy the template JSON data below. +
  2. +
  3. + Go to the templates folder in the + official repository: +
    + + github.com/devonthesofa/obsidian-note-status/templates + +
  4. +
  5. + Click Add file >{" "} + Create new file. +
  6. +
  7. + Name it {filename} and{" "} + paste the content. +
  8. +
  9. + Click Propose new file and submit the + Pull Request. +
  10. +
+ +

+ + Note: Submissions are reviewed by maintainers. Once + verified, your template will be included in the next + plugin update for all users! + +

+ +
+
+ {filename} + +
+
{jsonString}
+
+
+ +
+ +
+
+ ); +}; diff --git a/components/SettingsUI/TemplateEditorModal.tsx b/components/SettingsUI/TemplateEditorModal.tsx index ab05d0c..67a3900 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,8 +17,15 @@ 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 [authorGithub, setAuthorGithub] = useState( + template?.authorGithub || "", + ); const [statuses, setStatuses] = useState( template?.statuses || [ { @@ -52,6 +60,7 @@ export const TemplateEditorModal: React.FC = ({ id: templateId, name: name.trim(), description: description.trim(), + authorGithub: authorGithub.trim() || undefined, statuses: statusesWithTemplateId, }; @@ -145,6 +154,23 @@ export const TemplateEditorModal: React.FC = ({ /> + + + + void; onEdit?: (template: StatusTemplate) => void; onDelete?: (templateId: string) => void; + onShare?: (template: StatusTemplate) => void; } export const TemplateItem: React.FC = ({ @@ -19,7 +20,14 @@ export const TemplateItem: React.FC = ({ onToggle, onEdit, onDelete, + onShare, }) => { + const isCustom = useMemo(() => isCustomTemplate(template), [template]); + const isModified = useMemo( + () => !isCustom && isTemplateModified(template), + [isCustom, template], + ); + const handleClick = (e: React.MouseEvent) => { // Don't toggle if clicking action buttons if ((e.target as HTMLElement).closest(".template-item-actions")) { @@ -29,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}: + {template.description} + {template.authorGithub && ( + + )}
{template.statuses.map((status, index) => ( @@ -69,6 +110,18 @@ export const TemplateItem: React.FC = ({
+ {isCustom && onShare && ( + + )} {onEdit && ( + -
{(settings.templates || []).map((template) => ( @@ -186,6 +259,7 @@ export const TemplateSettings: React.FC = ({ onToggle={handleTemplateToggle} onEdit={handleEditTemplate} onDelete={handleDeleteTemplate} + onShare={handleShareTemplate} /> ))}
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/constants/defaultSettings.ts b/constants/defaultSettings.ts index f8fc3d4..714fcc1 100644 --- a/constants/defaultSettings.ts +++ b/constants/defaultSettings.ts @@ -1,8 +1,4 @@ import { PluginSettings } from "types/pluginSettings"; -import { - DEFAULT_ENABLED_TEMPLATES, - PREDEFINED_TEMPLATES, -} from "./predefinedTemplates"; export const DEFAULT_PLUGIN_SETTINGS: PluginSettings = { fileExplorerIconPosition: "absolute-right", @@ -18,7 +14,7 @@ export const DEFAULT_PLUGIN_SETTINGS: PluginSettings = { showStatusBar: true, autoHideStatusBar: false, enableStatusOverviewPopup: true, - templates: [...PREDEFINED_TEMPLATES], + templates: [], customStatuses: [], showStatusIconsInExplorer: true, hideUnknownStatusInExplorer: true, // Default to hide unknown status @@ -30,7 +26,7 @@ export const DEFAULT_PLUGIN_SETTINGS: PluginSettings = { enableExperimentalFeatures: true, enableStatusDashboard: true, enableGroupedStatusView: true, - enabledTemplates: DEFAULT_ENABLED_TEMPLATES, + enabledTemplates: [], useCustomStatusesOnly: false, useMultipleStatuses: true, singleStatusStorageMode: "list", diff --git a/constants/predefinedTemplates.ts b/constants/predefinedTemplates.ts index a1ca1eb..e6a914e 100644 --- a/constants/predefinedTemplates.ts +++ b/constants/predefinedTemplates.ts @@ -1,209 +1,278 @@ +/** + * THIS FILE IS AUTOMATICALLY GENERATED. DO NOT EDIT MANUALLY. + * To add a new template, create a JSON file in the /templates directory. + */ import { StatusTemplate } from "types/pluginSettings"; -/** - * Predefined status templates - */ export const PREDEFINED_TEMPLATES: StatusTemplate[] = [ { - id: "colorful", - name: "Colorful workflow", - description: - "A colorful set of workflow statuses with descriptive icons", - statuses: [ + "id": "academic", + "name": "Academic research", + "description": "Status workflow for academic research and writing", + "authorGithub": "soler1212", + "statuses": [ { - name: "idea", - icon: "πŸ’‘", - color: "#FFEB3B", - templateId: "colorful", + "name": "research", + "icon": "πŸ”", + "color": "#2196F3", + "templateId": "academic" }, { - name: "draft", - icon: "πŸ“", - color: "#E0E0E0", - templateId: "colorful", + "name": "outline", + "icon": "πŸ“‘", + "color": "#9E9E9E", + "templateId": "academic" }, { - name: "inProgress", - icon: "πŸ”§", - color: "#FFC107", - templateId: "colorful", + "name": "draft", + "icon": "✏️", + "color": "#FFC107", + "templateId": "academic" }, { - name: "editing", - icon: "πŸ–ŠοΈ", - color: "#2196F3", - templateId: "colorful", + "name": "review", + "icon": "πŸ”¬", + "color": "#9C27B0", + "templateId": "academic" }, { - name: "pending", - icon: "⏳", - color: "#9C27B0", - templateId: "colorful", + "name": "revision", + "icon": "πŸ“", + "color": "#FF5722", + "templateId": "academic" }, { - name: "onHold", - icon: "⏸", - color: "#9E9E9E", - templateId: "colorful", + "name": "final", + "icon": "πŸ“š", + "color": "#4CAF50", + "templateId": "academic" }, { - name: "needsUpdate", - icon: "πŸ”„", - color: "#FF5722", - templateId: "colorful", - }, - { - name: "completed", - icon: "βœ…", - color: "#4CAF50", - templateId: "colorful", - }, - { - name: "archived", - icon: "πŸ“¦", - color: "#795548", - templateId: "colorful", - }, - ], + "name": "published", + "icon": "πŸŽ“", + "color": "#795548", + "templateId": "academic" + } + ] }, { - id: "minimal", - name: "Minimal workflow", - description: "A simplified set of essential workflow statuses", - statuses: [ + "id": "colorful", + "name": "Colorful workflow", + "description": "A colorful set of workflow statuses with descriptive icons", + "authorGithub": "soler1212", + "statuses": [ { - name: "todo", - icon: "πŸ“Œ", - color: "#F44336", - templateId: "minimal", + "name": "idea", + "icon": "πŸ’‘", + "color": "#FFEB3B", + "templateId": "colorful" }, { - name: "inProgress", - icon: "βš™οΈ", - color: "#2196F3", - templateId: "minimal", + "name": "draft", + "icon": "πŸ“", + "color": "#E0E0E0", + "templateId": "colorful" }, { - name: "review", - icon: "πŸ‘€", - color: "#9C27B0", - templateId: "minimal", + "name": "inProgress", + "icon": "πŸ”§", + "color": "#FFC107", + "templateId": "colorful" }, { - name: "done", - icon: "βœ“", - color: "#4CAF50", - templateId: "minimal", + "name": "editing", + "icon": "πŸ–ŠοΈ", + "color": "#2196F3", + "templateId": "colorful" }, - ], + { + "name": "pending", + "icon": "⏳", + "color": "#9C27B0", + "templateId": "colorful" + }, + { + "name": "onHold", + "icon": "⏸", + "color": "#9E9E9E", + "templateId": "colorful" + }, + { + "name": "needsUpdate", + "icon": "πŸ”„", + "color": "#FF5722", + "templateId": "colorful" + }, + { + "name": "completed", + "icon": "βœ…", + "color": "#4CAF50", + "templateId": "colorful" + }, + { + "name": "archived", + "icon": "πŸ“¦", + "color": "#795548", + "templateId": "colorful" + } + ] }, { - id: "academic", - name: "Academic research", - description: "Status workflow for academic research and writing", - statuses: [ + "id": "creative-writing", + "name": "Creative Writing", + "description": "Workflow for novelists and creative writers", + "authorGithub": "soler1212", + "statuses": [ { - name: "research", - icon: "πŸ”", - color: "#2196F3", - templateId: "academic", + "name": "idea", + "icon": "🧠", + "color": "#FFD700", + "templateId": "creative-writing" }, { - name: "outline", - icon: "πŸ“‘", - color: "#9E9E9E", - templateId: "academic", + "name": "outline", + "icon": "πŸ—ΊοΈ", + "color": "#87CEEB", + "templateId": "creative-writing" }, { - name: "draft", - icon: "✏️", - color: "#FFC107", - templateId: "academic", + "name": "first-draft", + "icon": "✍️", + "color": "#FFA07A", + "templateId": "creative-writing" }, { - name: "review", - icon: "πŸ”¬", - color: "#9C27B0", - templateId: "academic", + "name": "revision", + "icon": "πŸ”", + "color": "#DA70D6", + "templateId": "creative-writing" }, { - name: "revision", - icon: "πŸ“", - color: "#FF5722", - templateId: "academic", - }, - { - name: "final", - icon: "πŸ“š", - color: "#4CAF50", - templateId: "academic", - }, - { - name: "published", - icon: "πŸŽ“", - color: "#795548", - templateId: "academic", - }, - ], + "name": "final-polish", + "icon": "✨", + "color": "#32CD32", + "templateId": "creative-writing" + } + ] }, { - id: "project", - name: "Project management", - description: "Status workflow for project management and tracking", - statuses: [ + "id": "default-starter", + "name": "Starter Template", + "description": "A simplified set of essential workflow statuses to get you started.", + "authorGithub": "soler1212", + "statuses": [ { - name: "planning", - icon: "πŸ—“οΈ", - color: "#9E9E9E", - templateId: "project", + "name": "todo", + "icon": "πŸ“Œ", + "color": "#F44336", + "templateId": "default-starter" }, { - name: "backlog", - icon: "πŸ“‹", - color: "#E0E0E0", - templateId: "project", + "name": "inProgress", + "icon": "βš™οΈ", + "color": "#2196F3", + "templateId": "default-starter" }, { - name: "ready", - icon: "🚦", - color: "#8BC34A", - templateId: "project", + "name": "review", + "icon": "πŸ‘€", + "color": "#9C27B0", + "templateId": "default-starter" }, { - name: "inDevelopment", - icon: "πŸ‘¨β€πŸ’»", - color: "#2196F3", - templateId: "project", - }, - { - name: "testing", - icon: "πŸ§ͺ", - color: "#9C27B0", - templateId: "project", - }, - { - name: "review", - icon: "πŸ‘οΈ", - color: "#FFC107", - templateId: "project", - }, - { - name: "approved", - icon: "πŸ‘", - color: "#4CAF50", - templateId: "project", - }, - { - name: "live", - icon: "πŸš€", - color: "#3F51B5", - templateId: "project", - }, - ], + "name": "done", + "icon": "βœ“", + "color": "#4CAF50", + "templateId": "default-starter" + } + ] }, + { + "id": "minimal", + "name": "Minimal workflow", + "description": "A simplified set of essential workflow statuses", + "authorGithub": "soler1212", + "statuses": [ + { + "name": "todo", + "icon": "πŸ“Œ", + "color": "#F44336", + "templateId": "minimal" + }, + { + "name": "inProgress", + "icon": "βš™οΈ", + "color": "#2196F3", + "templateId": "minimal" + }, + { + "name": "review", + "icon": "πŸ‘€", + "color": "#9C27B0", + "templateId": "minimal" + }, + { + "name": "done", + "icon": "βœ“", + "color": "#4CAF50", + "templateId": "minimal" + } + ] + }, + { + "id": "project", + "name": "Project management", + "description": "Status workflow for project management and tracking", + "authorGithub": "soler1212", + "statuses": [ + { + "name": "planning", + "icon": "πŸ—“οΈ", + "color": "#9E9E9E", + "templateId": "project" + }, + { + "name": "backlog", + "icon": "πŸ“‹", + "color": "#E0E0E0", + "templateId": "project" + }, + { + "name": "ready", + "icon": "🚦", + "color": "#8BC34A", + "templateId": "project" + }, + { + "name": "inDevelopment", + "icon": "πŸ‘¨β€πŸ’»", + "color": "#2196F3", + "templateId": "project" + }, + { + "name": "testing", + "icon": "πŸ§ͺ", + "color": "#9C27B0", + "templateId": "project" + }, + { + "name": "review", + "icon": "πŸ‘οΈ", + "color": "#FFC107", + "templateId": "project" + }, + { + "name": "approved", + "icon": "πŸ‘", + "color": "#4CAF50", + "templateId": "project" + }, + { + "name": "live", + "icon": "πŸš€", + "color": "#3F51B5", + "templateId": "project" + } + ] + } ]; - -/** - * Default template IDs that should be enabled by default - */ -export const DEFAULT_ENABLED_TEMPLATES = ["colorful"]; diff --git a/core/settingsService.ts b/core/settingsService.ts index 42c8348..141599c 100644 --- a/core/settingsService.ts +++ b/core/settingsService.ts @@ -1,5 +1,6 @@ import { DEFAULT_PLUGIN_SETTINGS } from "@/constants/defaultSettings"; import { PluginSettings, SyncGroup } from "@/types/pluginSettings"; +import { PREDEFINED_TEMPLATES } from "@/constants/predefinedTemplates"; import { Plugin, TFile, normalizePath } from "obsidian"; import eventBus from "./eventBus"; @@ -65,7 +66,8 @@ class SettingsService { async initialize(plugin: Plugin): Promise { this.plugin = plugin; - await this.loadSettings(); + const loadedData = await this.loadSettings(); + this.migrateLegacySettings(loadedData); if (this.settings.enableExternalStatusSync) { await this.loadFromExternalFile(); @@ -104,7 +106,129 @@ class SettingsService { private async loadSettings() { const loadedData = await this.plugin.loadData(); this.settings = this.mergeSettings(DEFAULT_PLUGIN_SETTINGS, loadedData); - return this.settings; + this.deduplicateTemplates(); + return loadedData; + } + + /** + * Migrates legacy settings and ensures new users have a starter template. + */ + private migrateLegacySettings(loadedData: Partial | null) { + const hasTemplates = + this.settings.templates && this.settings.templates.length > 0; + + // 1. If it's a truly new user (no data at all), give them the starter template + if (!loadedData) { + this.injectStarterTemplate(); + return; + } + + // 2. If it's an existing user but has no templates, they were using the old defaults + if (!hasTemplates) { + const legacyIds = [ + "colorful", + "minimal", + "academic", + "project", + "creative-writing", + ]; + const enabledLegacy = (this.settings.enabledTemplates || []).filter( + (id) => legacyIds.includes(id), + ); + + if (enabledLegacy.length > 0) { + // Restore the ones they had enabled from the marketplace list + const toRestore = PREDEFINED_TEMPLATES.filter((t) => + enabledLegacy.includes(t.id), + ).map((t) => ({ + ...t, + id: t.id, // Keep original ID for legacy compatibility + statuses: t.statuses.map((s) => ({ + ...s, + templateId: t.id, + })), + })); + + this.settings.templates = toRestore; + // Update enabled list + this.settings.enabledTemplates = toRestore.map((t) => t.id); + this.saveSettings().catch(console.error); + } else { + // If nothing was enabled, just give them the starter template + this.injectStarterTemplate(); + } + } + } + + /** + * Injects the default starter template. + */ + private injectStarterTemplate() { + const starterTemplate = { + id: "starter", + name: "Starter Template", + description: + "A simplified set of essential workflow statuses to get you started.", + authorGithub: "soler1212", + statuses: [ + { + name: "todo", + icon: "πŸ“Œ", + color: "#F44336", + templateId: "starter", + }, + { + name: "inProgress", + icon: "βš™οΈ", + color: "#2196F3", + templateId: "starter", + }, + { + name: "review", + icon: "πŸ‘€", + color: "#9C27B0", + templateId: "starter", + }, + { + name: "done", + icon: "βœ“", + color: "#4CAF50", + templateId: "starter", + }, + ], + }; + + this.settings.templates = [starterTemplate]; + if (!this.settings.enabledTemplates.includes(starterTemplate.id)) { + this.settings.enabledTemplates.push(starterTemplate.id); + } + this.saveSettings().catch(console.error); + } + + /** + * Removes duplicate templates with the same name, keeping the first occurrence. + */ + private deduplicateTemplates() { + if (!this.settings.templates) return; + + const seenNames = new Set(); + const uniqueTemplates = []; + let hasDuplicates = false; + + for (const template of this.settings.templates) { + const lowerName = template.name.toLowerCase().trim(); + if (!seenNames.has(lowerName)) { + seenNames.add(lowerName); + uniqueTemplates.push(template); + } else { + hasDuplicates = true; + } + } + + if (hasDuplicates) { + this.settings.templates = uniqueTemplates; + this.saveSettings().catch(console.error); + } } /** diff --git a/esbuild.config.mjs b/esbuild.config.mjs index f6dca29..6b64ec6 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -1,6 +1,8 @@ import esbuild from "esbuild"; import process from "process"; import builtins from "builtin-modules"; +import fs from "fs"; +import path from "path"; const banner = `/* @@ -9,7 +11,45 @@ if you want to view the source, please visit the github repository of this plugi */ `; -const prod = (process.argv[2] === "production"); +/** + * Automatically generates the predefinedTemplates.ts file by scanning the templates directory. + */ +const generateTemplatesIndex = () => { + const templatesDir = "./templates"; + const outputFile = "./constants/predefinedTemplates.ts"; + + if (!fs.existsSync(templatesDir)) return; + + const files = fs.readdirSync(templatesDir); + const templates = []; + + files.forEach((file) => { + if (file.endsWith(".json")) { + const filePath = path.join(templatesDir, file); + const content = JSON.parse(fs.readFileSync(filePath, "utf8")); + templates.push(content); + } + }); + + const content = `/** + * THIS FILE IS AUTOMATICALLY GENERATED. DO NOT EDIT MANUALLY. + * To add a new template, create a JSON file in the /templates directory. + */ +import { StatusTemplate } from "types/pluginSettings"; + +export const PREDEFINED_TEMPLATES: StatusTemplate[] = ${JSON.stringify(templates, null, "\t")}; +`; + + fs.writeFileSync(outputFile, content); + console.log( + `\x1b[36m[Templates]\x1b[0m Automatically registered ${templates.length} templates.`, + ); +}; + +// Generate templates index before build +generateTemplatesIndex(); + +const prod = process.argv[2] === "production"; // CSS bundling function const buildStyles = async () => { diff --git a/styles/components/template-settings.css b/styles/components/template-settings.css index 516115a..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; @@ -141,6 +176,31 @@ justify-content: center; } +.template-item-actions button, +.template-item-actions a { + display: flex; + align-items: center; + justify-content: center; + padding: var(--size-4-1); + border-radius: var(--radius-s); + color: var(--text-muted); + background: transparent; + border: none; + cursor: pointer; + transition: all var(--anim-duration-fast) ease; + text-decoration: none !important; +} + +.template-item-actions button:hover, +.template-item-actions a:hover { + background: var(--interactive-hover); + color: var(--text-normal); +} + +.template-marketplace-btn { + color: var(--text-accent) !important; +} + /* Template Editor Modal */ .template-editor-modal { background: var(--background-primary); @@ -229,6 +289,214 @@ transform: none; } +.template-author-info { + display: block; + font-size: var(--font-ui-smaller); + color: var(--text-muted); + margin-top: var(--size-2-1); + font-style: italic; +} + +.template-author-info a { + color: var(--text-accent); + text-decoration: none; +} + +.template-author-info a:hover { + text-decoration: underline; +} + +/* Marketplace Share Modal */ +.marketplace-steps { + padding-left: var(--size-4-4); + margin-bottom: var(--size-4-4); + color: var(--text-normal); +} + +.marketplace-steps li { + margin-bottom: var(--size-2-2); +} + +.marketplace-steps li a { + color: var(--text-accent); + word-break: break-all; +} + +.marketplace-note { + margin: var(--size-4-4) 0; + padding: var(--size-4-2); + background: var(--background-primary-alt); + border-left: 4px solid var(--interactive-accent); + border-radius: var(--radius-s); + font-size: var(--font-ui-small); +} + +.marketplace-json-preview { + background: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-m); + overflow: hidden; + margin-top: var(--size-4-2); +} + +.json-preview-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--size-2-2) var(--size-4-2); + background: var(--background-primary-alt); + border-bottom: 1px solid var(--background-modifier-border); + font-family: var(--font-monospace); + font-size: var(--font-ui-smaller); + color: var(--text-muted); +} + +.copy-btn { + display: flex; + align-items: center; + gap: var(--size-2-1); + padding: var(--size-2-1) var(--size-2-3) !important; + font-size: var(--font-ui-smaller) !important; + background: var(--interactive-accent) !important; + color: var(--text-on-accent) !important; + border: none !important; +} + +.copy-btn.success { + background: var(--text-success) !important; +} + +.marketplace-json-preview pre { + margin: 0; + padding: var(--size-4-2); + max-height: 300px; + overflow-y: auto; + font-family: var(--font-monospace); + font-size: var(--font-ui-smaller); + white-space: pre-wrap; + word-break: break-all; +} + +.marketplace-github-link { + display: flex; + align-items: center; + gap: var(--size-2-2); + text-decoration: none !important; +} + +/* Marketplace Browse Modal */ +.marketplace-header-content { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + gap: var(--size-4-4); +} + +.marketplace-header-content h2 { + margin: 0; + white-space: nowrap; +} + +.marketplace-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: var(--size-4-4); + padding: var(--size-2-2); +} + +.marketplace-card { + display: flex; + flex-direction: column; + background: var(--background-primary-alt); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-m); + padding: var(--size-4-4); + transition: all var(--anim-duration-fast) ease; +} + +.marketplace-card:hover { + border-color: var(--interactive-accent); + box-shadow: var(--shadow-s); +} + +.marketplace-card-header { + margin-bottom: var(--size-4-2); +} + +.marketplace-card-title { + font-weight: var(--font-bold); + 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 { + font-size: var(--font-ui-smaller); + color: var(--text-muted); + font-style: italic; +} + +.marketplace-card-description { + font-size: var(--font-ui-small); + color: var(--text-muted); + margin-bottom: var(--size-4-2); + flex-grow: 1; +} + +.marketplace-card-statuses { + display: flex; + flex-wrap: wrap; + gap: var(--size-2-1); + margin-bottom: var(--size-4-4); +} + +.marketplace-card-actions { + margin-top: auto; +} + +.marketplace-install-btn { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: var(--size-2-2); +} + +.marketplace-install-btn.installed { + background: var(--background-modifier-success-accent) !important; + color: var(--text-on-accent) !important; + opacity: 0.8; + cursor: default; +} + +.marketplace-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--size-4-12); + color: var(--text-muted); + text-align: center; +} + +.marketplace-empty p { + margin-top: var(--size-4-2); + font-size: var(--font-ui-large); +} + +.marketplace-browse-btn { + display: flex; + align-items: center; + gap: var(--size-2-2); +} + /* Responsive Design */ @media (max-width: 768px) { .template-settings-actions { diff --git a/templates/academic.json b/templates/academic.json new file mode 100644 index 0000000..f3e5337 --- /dev/null +++ b/templates/academic.json @@ -0,0 +1,50 @@ +{ + "id": "academic", + "name": "Academic research", + "description": "Status workflow for academic research and writing", + "authorGithub": "soler1212", + "statuses": [ + { + "name": "research", + "icon": "πŸ”", + "color": "#2196F3", + "templateId": "academic" + }, + { + "name": "outline", + "icon": "πŸ“‘", + "color": "#9E9E9E", + "templateId": "academic" + }, + { + "name": "draft", + "icon": "✏️", + "color": "#FFC107", + "templateId": "academic" + }, + { + "name": "review", + "icon": "πŸ”¬", + "color": "#9C27B0", + "templateId": "academic" + }, + { + "name": "revision", + "icon": "πŸ“", + "color": "#FF5722", + "templateId": "academic" + }, + { + "name": "final", + "icon": "πŸ“š", + "color": "#4CAF50", + "templateId": "academic" + }, + { + "name": "published", + "icon": "πŸŽ“", + "color": "#795548", + "templateId": "academic" + } + ] +} diff --git a/templates/colorful.json b/templates/colorful.json new file mode 100644 index 0000000..27ecd66 --- /dev/null +++ b/templates/colorful.json @@ -0,0 +1,62 @@ +{ + "id": "colorful", + "name": "Colorful workflow", + "description": "A colorful set of workflow statuses with descriptive icons", + "authorGithub": "soler1212", + "statuses": [ + { + "name": "idea", + "icon": "πŸ’‘", + "color": "#FFEB3B", + "templateId": "colorful" + }, + { + "name": "draft", + "icon": "πŸ“", + "color": "#E0E0E0", + "templateId": "colorful" + }, + { + "name": "inProgress", + "icon": "πŸ”§", + "color": "#FFC107", + "templateId": "colorful" + }, + { + "name": "editing", + "icon": "πŸ–ŠοΈ", + "color": "#2196F3", + "templateId": "colorful" + }, + { + "name": "pending", + "icon": "⏳", + "color": "#9C27B0", + "templateId": "colorful" + }, + { + "name": "onHold", + "icon": "⏸", + "color": "#9E9E9E", + "templateId": "colorful" + }, + { + "name": "needsUpdate", + "icon": "πŸ”„", + "color": "#FF5722", + "templateId": "colorful" + }, + { + "name": "completed", + "icon": "βœ…", + "color": "#4CAF50", + "templateId": "colorful" + }, + { + "name": "archived", + "icon": "πŸ“¦", + "color": "#795548", + "templateId": "colorful" + } + ] +} diff --git a/templates/creative-writing.json b/templates/creative-writing.json new file mode 100644 index 0000000..10c8cd0 --- /dev/null +++ b/templates/creative-writing.json @@ -0,0 +1,38 @@ +{ + "id": "creative-writing", + "name": "Creative Writing", + "description": "Workflow for novelists and creative writers", + "authorGithub": "soler1212", + "statuses": [ + { + "name": "idea", + "icon": "🧠", + "color": "#FFD700", + "templateId": "creative-writing" + }, + { + "name": "outline", + "icon": "πŸ—ΊοΈ", + "color": "#87CEEB", + "templateId": "creative-writing" + }, + { + "name": "first-draft", + "icon": "✍️", + "color": "#FFA07A", + "templateId": "creative-writing" + }, + { + "name": "revision", + "icon": "πŸ”", + "color": "#DA70D6", + "templateId": "creative-writing" + }, + { + "name": "final-polish", + "icon": "✨", + "color": "#32CD32", + "templateId": "creative-writing" + } + ] +} diff --git a/templates/default-starter.json b/templates/default-starter.json new file mode 100644 index 0000000..744a3de --- /dev/null +++ b/templates/default-starter.json @@ -0,0 +1,32 @@ +{ + "id": "default-starter", + "name": "Starter Template", + "description": "A simplified set of essential workflow statuses to get you started.", + "authorGithub": "soler1212", + "statuses": [ + { + "name": "todo", + "icon": "πŸ“Œ", + "color": "#F44336", + "templateId": "default-starter" + }, + { + "name": "inProgress", + "icon": "βš™οΈ", + "color": "#2196F3", + "templateId": "default-starter" + }, + { + "name": "review", + "icon": "πŸ‘€", + "color": "#9C27B0", + "templateId": "default-starter" + }, + { + "name": "done", + "icon": "βœ“", + "color": "#4CAF50", + "templateId": "default-starter" + } + ] +} diff --git a/templates/minimal.json b/templates/minimal.json new file mode 100644 index 0000000..bcc3bd8 --- /dev/null +++ b/templates/minimal.json @@ -0,0 +1,32 @@ +{ + "id": "minimal", + "name": "Minimal workflow", + "description": "A simplified set of essential workflow statuses", + "authorGithub": "soler1212", + "statuses": [ + { + "name": "todo", + "icon": "πŸ“Œ", + "color": "#F44336", + "templateId": "minimal" + }, + { + "name": "inProgress", + "icon": "βš™οΈ", + "color": "#2196F3", + "templateId": "minimal" + }, + { + "name": "review", + "icon": "πŸ‘€", + "color": "#9C27B0", + "templateId": "minimal" + }, + { + "name": "done", + "icon": "βœ“", + "color": "#4CAF50", + "templateId": "minimal" + } + ] +} diff --git a/templates/project.json b/templates/project.json new file mode 100644 index 0000000..8f309d8 --- /dev/null +++ b/templates/project.json @@ -0,0 +1,56 @@ +{ + "id": "project", + "name": "Project management", + "description": "Status workflow for project management and tracking", + "authorGithub": "soler1212", + "statuses": [ + { + "name": "planning", + "icon": "πŸ—“οΈ", + "color": "#9E9E9E", + "templateId": "project" + }, + { + "name": "backlog", + "icon": "πŸ“‹", + "color": "#E0E0E0", + "templateId": "project" + }, + { + "name": "ready", + "icon": "🚦", + "color": "#8BC34A", + "templateId": "project" + }, + { + "name": "inDevelopment", + "icon": "πŸ‘¨β€πŸ’»", + "color": "#2196F3", + "templateId": "project" + }, + { + "name": "testing", + "icon": "πŸ§ͺ", + "color": "#9C27B0", + "templateId": "project" + }, + { + "name": "review", + "icon": "πŸ‘οΈ", + "color": "#FFC107", + "templateId": "project" + }, + { + "name": "approved", + "icon": "πŸ‘", + "color": "#4CAF50", + "templateId": "project" + }, + { + "name": "live", + "icon": "πŸš€", + "color": "#3F51B5", + "templateId": "project" + } + ] +} diff --git a/tsconfig.json b/tsconfig.json index f83eaba..7f0cec4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,7 +26,8 @@ "lib": ["DOM", "ES5", "ES6", "ES7"], "jsx": "react-jsx", "esModuleInterop": true, - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true }, "include": ["**/*.ts", "**/*.tsx"] } diff --git a/types/pluginSettings.ts b/types/pluginSettings.ts index f93e121..03a738f 100644 --- a/types/pluginSettings.ts +++ b/types/pluginSettings.ts @@ -19,6 +19,7 @@ export interface StatusTemplate { id: string; name: string; description: string; + authorGithub?: string; statuses: NoteStatus[]; } diff --git a/utils/templateUtils.ts b/utils/templateUtils.ts index fe7649a..a4d9d1a 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.authorGithub; + 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; }; /** @@ -36,9 +86,13 @@ export const isTemplateNameUnique = ( /** * Generate unique template ID */ -export const generateTemplateId = (name: string): string => { +export const generateTemplateId = ( + name: string, + existingTemplates?: StatusTemplate[], +): string => { const baseId = name.toLowerCase().replace(/[^a-z0-9]/g, "-"); - const allTemplates = settingsService.settings.templates; + const allTemplates = + existingTemplates || settingsService.settings.templates; let counter = 1; let id = `custom-${baseId}`; @@ -50,3 +104,4 @@ export const generateTemplateId = (name: string): string => { return id; }; +