mirror of
https://github.com/devonthesofa/obsidian-note-status.git
synced 2026-07-22 12:30:24 +00:00
feat: add custom template CRUD UI
This commit is contained in:
parent
392447c117
commit
ddcb49ab84
9 changed files with 807 additions and 84 deletions
|
|
@ -1,12 +1,10 @@
|
|||
import { NoteStatus } from "@/types/noteStatus";
|
||||
import { PluginSettings } from "@/types/pluginSettings";
|
||||
import React from "react";
|
||||
import { Input } from "@/components/atoms/Input";
|
||||
|
||||
export type Props = {
|
||||
status: NoteStatus;
|
||||
index: number;
|
||||
settings: PluginSettings;
|
||||
onCustomStatusChange: (
|
||||
index: number,
|
||||
column: "name" | "icon" | "color" | "description",
|
||||
|
|
|
|||
189
components/SettingsUI/TemplateEditorModal.tsx
Normal file
189
components/SettingsUI/TemplateEditorModal.tsx
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
import React, { useState, useCallback } 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";
|
||||
|
||||
interface TemplateEditorModalProps {
|
||||
template?: StatusTemplate;
|
||||
onSave: (template: StatusTemplate) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const TemplateEditorModal: React.FC<TemplateEditorModalProps> = ({
|
||||
template,
|
||||
onSave,
|
||||
onCancel,
|
||||
}) => {
|
||||
const [name, setName] = useState(template?.name || "");
|
||||
const [description, setDescription] = useState(template?.description || "");
|
||||
const [statuses, setStatuses] = useState<NoteStatus[]>(
|
||||
template?.statuses || [
|
||||
{
|
||||
name: "",
|
||||
icon: "",
|
||||
color: "#888888",
|
||||
templateId: template?.id || "",
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const isEditing = !!template;
|
||||
const isValid =
|
||||
name.trim().length > 0 &&
|
||||
description.trim().length > 0 &&
|
||||
statuses.some((s) => s.name.trim().length > 0);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
if (!isValid) return;
|
||||
|
||||
const templateId = template?.id || `custom-${Date.now()}`;
|
||||
const validStatuses = statuses.filter((s) => s.name.trim().length > 0);
|
||||
|
||||
// Update templateId for all statuses
|
||||
const statusesWithTemplateId = validStatuses.map((s) => ({
|
||||
...s,
|
||||
templateId,
|
||||
}));
|
||||
|
||||
const savedTemplate: StatusTemplate = {
|
||||
id: templateId,
|
||||
name: name.trim(),
|
||||
description: description.trim(),
|
||||
statuses: statusesWithTemplateId,
|
||||
isCustom: true,
|
||||
};
|
||||
|
||||
onSave(savedTemplate);
|
||||
}, [name, description, statuses, template, isValid, onSave]);
|
||||
|
||||
const handleAddStatus = useCallback(() => {
|
||||
setStatuses((prev) => [
|
||||
...prev,
|
||||
{
|
||||
name: "",
|
||||
icon: "",
|
||||
color: "#888888",
|
||||
templateId: template?.id || "",
|
||||
},
|
||||
]);
|
||||
}, [template?.id]);
|
||||
|
||||
const handleStatusChange = useCallback(
|
||||
(
|
||||
index: number,
|
||||
column: "name" | "icon" | "color" | "description",
|
||||
value: string,
|
||||
) => {
|
||||
setStatuses((prev) =>
|
||||
prev.map((status, i) =>
|
||||
i === index ? { ...status, [column]: value } : status,
|
||||
),
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleStatusRemove = useCallback((index: number) => {
|
||||
setStatuses((prev) => prev.filter((_, i) => i !== index));
|
||||
}, []);
|
||||
|
||||
const handleMoveUp = useCallback((index: number) => {
|
||||
if (index <= 0) return;
|
||||
setStatuses((prev) => {
|
||||
const newStatuses = [...prev];
|
||||
[newStatuses[index - 1], newStatuses[index]] = [
|
||||
newStatuses[index],
|
||||
newStatuses[index - 1],
|
||||
];
|
||||
return newStatuses;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleMoveDown = useCallback((index: number) => {
|
||||
setStatuses((prev) => {
|
||||
if (index >= prev.length - 1) return prev;
|
||||
const newStatuses = [...prev];
|
||||
[newStatuses[index], newStatuses[index + 1]] = [
|
||||
newStatuses[index + 1],
|
||||
newStatuses[index],
|
||||
];
|
||||
return newStatuses;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="template-editor-modal">
|
||||
<div className="template-editor-modal__header">
|
||||
<h2>{isEditing ? "Edit Template" : "Create New Template"}</h2>
|
||||
</div>
|
||||
|
||||
<div className="template-editor-modal__content">
|
||||
<SettingItem
|
||||
name="Template Name"
|
||||
description="A unique name for your template"
|
||||
>
|
||||
<Input
|
||||
variant="text"
|
||||
value={name}
|
||||
onChange={setName}
|
||||
placeholder="e.g. Project Workflow"
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
name="Description"
|
||||
description="Brief description of the template's purpose"
|
||||
>
|
||||
<Input
|
||||
variant="text"
|
||||
value={description}
|
||||
onChange={setDescription}
|
||||
placeholder="e.g. Status workflow for project management"
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
name="Statuses"
|
||||
description="Define the statuses included in this template"
|
||||
vertical
|
||||
>
|
||||
<div className="template-editor-statuses">
|
||||
{statuses.map((status, index) => (
|
||||
<CustomStatusItem
|
||||
key={index}
|
||||
status={status}
|
||||
index={index}
|
||||
onCustomStatusChange={handleStatusChange}
|
||||
onCustomStatusRemove={handleStatusRemove}
|
||||
onMoveUp={handleMoveUp}
|
||||
onMoveDown={handleMoveDown}
|
||||
canMoveUp={index > 0}
|
||||
canMoveDown={index < statuses.length - 1}
|
||||
/>
|
||||
))}
|
||||
|
||||
<button
|
||||
className="mod-cta template-editor-add-status"
|
||||
onClick={handleAddStatus}
|
||||
>
|
||||
+ Add Status
|
||||
</button>
|
||||
</div>
|
||||
</SettingItem>
|
||||
</div>
|
||||
|
||||
<div className="template-editor-modal__actions">
|
||||
<button
|
||||
className="mod-cta"
|
||||
onClick={handleSave}
|
||||
disabled={!isValid}
|
||||
>
|
||||
{isEditing ? "Save Changes" : "Create Template"}
|
||||
</button>
|
||||
<button onClick={onCancel}>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -2,47 +2,120 @@ import React from "react";
|
|||
import { StatusTemplate } from "@/types/pluginSettings";
|
||||
import { StatusDisplay } from "../atoms/StatusDisplay";
|
||||
import { SelectableListItem } from "../atoms/SelectableListItem";
|
||||
import { isCustomTemplate } from "@/utils/templateUtils";
|
||||
|
||||
interface TemplateItemProps {
|
||||
template: StatusTemplate;
|
||||
isEnabled: boolean;
|
||||
onToggle: (templateId: string, enabled: boolean) => void;
|
||||
onEdit?: (template: StatusTemplate) => void;
|
||||
onDelete?: (templateId: string) => void;
|
||||
}
|
||||
|
||||
export const TemplateItem: React.FC<TemplateItemProps> = ({
|
||||
template,
|
||||
isEnabled,
|
||||
onToggle,
|
||||
}) => (
|
||||
<SelectableListItem
|
||||
selected={isEnabled}
|
||||
onClick={() => onToggle(template.id, !isEnabled)}
|
||||
className={`template-item ${isEnabled ? "enabled" : ""}`}
|
||||
icon={
|
||||
<input
|
||||
type="checkbox"
|
||||
className="template-checkbox"
|
||||
checked={isEnabled}
|
||||
readOnly
|
||||
/>
|
||||
onEdit,
|
||||
onDelete,
|
||||
}) => {
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
// Don't toggle if clicking action buttons
|
||||
if ((e.target as HTMLElement).closest(".template-item-actions")) {
|
||||
return;
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<div className="template-header">
|
||||
<span className="setting-item-name">{template.name}</span>
|
||||
</div>
|
||||
<div className="setting-item-description">
|
||||
{template.description}:
|
||||
</div>
|
||||
<div className="template-statuses">
|
||||
{template.statuses.map((status, index) => (
|
||||
<StatusDisplay
|
||||
key={index}
|
||||
status={status}
|
||||
variant="template"
|
||||
onToggle(template.id, !isEnabled);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`template-item ${isEnabled ? "enabled" : ""}`}>
|
||||
<SelectableListItem
|
||||
selected={isEnabled}
|
||||
onClick={handleClick}
|
||||
className="template-item-content"
|
||||
icon={
|
||||
<input
|
||||
type="checkbox"
|
||||
className="template-checkbox"
|
||||
checked={isEnabled}
|
||||
readOnly
|
||||
/>
|
||||
))}
|
||||
}
|
||||
>
|
||||
<div className="template-item-main">
|
||||
<div className="template-header">
|
||||
<span className="setting-item-name">
|
||||
{template.name}
|
||||
</span>
|
||||
{isCustomTemplate(template.id) && (
|
||||
<span className="template-custom-badge">
|
||||
Custom
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="setting-item-description">
|
||||
{template.description}:
|
||||
</div>
|
||||
<div className="template-statuses">
|
||||
{template.statuses.map((status, index) => (
|
||||
<StatusDisplay
|
||||
key={index}
|
||||
status={status}
|
||||
variant="template"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</SelectableListItem>
|
||||
<div className="template-item-actions">
|
||||
{onEdit && (
|
||||
<button
|
||||
className="template-action-btn template-action-btn--edit"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit(template);
|
||||
}}
|
||||
title="Edit template"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||
<path d="m18.5 2.5 4 4L13 16l-4-4L18.5 2.5z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{onDelete && (
|
||||
<button
|
||||
className="template-action-btn template-action-btn--delete"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete(template.id);
|
||||
}}
|
||||
title="Delete template"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<polyline points="3,6 5,6 21,6" />
|
||||
<path d="M19,6l-2,14a2,2,0,0,1-2,2H9a2,2,0,0,1-2-2L5,6" />
|
||||
<path d="M10,11V17" />
|
||||
<path d="M14,11V17" />
|
||||
<path d="M9,6V4a2,2,0,0,1,2-2h2a2,2,0,0,1,2,2V6" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SelectableListItem>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,51 +1,204 @@
|
|||
import React, { useCallback } from "react";
|
||||
import { PluginSettings } from "@/types/pluginSettings";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { PluginSettings, StatusTemplate } from "@/types/pluginSettings";
|
||||
import { TemplateItem } from "./TemplateItem";
|
||||
import { TemplateEditorModal } from "./TemplateEditorModal";
|
||||
import {
|
||||
generateTemplateId,
|
||||
isTemplateNameUnique,
|
||||
} from "@/utils/templateUtils";
|
||||
|
||||
interface TemplateSettingsProps {
|
||||
settings: PluginSettings;
|
||||
onChange: (key: keyof PluginSettings, value: unknown) => void;
|
||||
templates: StatusTemplate[];
|
||||
}
|
||||
|
||||
export const TemplateSettings: React.FC<TemplateSettingsProps> = ({
|
||||
settings,
|
||||
onChange,
|
||||
}) => {
|
||||
const [showEditor, setShowEditor] = useState(false);
|
||||
const [editingTemplate, setEditingTemplate] = useState<
|
||||
StatusTemplate | undefined
|
||||
>();
|
||||
|
||||
const handleTemplateToggle = useCallback(
|
||||
(templateId: string, enabled: boolean) => {
|
||||
let newEnabledTemplated = [...settings.enabledTemplates];
|
||||
let newEnabledTemplates = [...settings.enabledTemplates];
|
||||
if (enabled) {
|
||||
if (!newEnabledTemplated.includes(templateId)) {
|
||||
newEnabledTemplated.push(templateId);
|
||||
if (!newEnabledTemplates.includes(templateId)) {
|
||||
newEnabledTemplates.push(templateId);
|
||||
}
|
||||
} else {
|
||||
newEnabledTemplated = newEnabledTemplated.filter(
|
||||
newEnabledTemplates = newEnabledTemplates.filter(
|
||||
(id: string) => id !== templateId,
|
||||
);
|
||||
}
|
||||
onChange("enabledTemplates", newEnabledTemplated);
|
||||
onChange("enabledTemplates", newEnabledTemplates);
|
||||
},
|
||||
[onChange, settings.enabledTemplates],
|
||||
);
|
||||
|
||||
const handleCreateTemplate = useCallback(() => {
|
||||
setEditingTemplate(undefined);
|
||||
setShowEditor(true);
|
||||
}, []);
|
||||
|
||||
const handleEditTemplate = useCallback((template: StatusTemplate) => {
|
||||
setEditingTemplate(template);
|
||||
setShowEditor(true);
|
||||
}, []);
|
||||
|
||||
const handleSaveTemplate = useCallback(
|
||||
(template: StatusTemplate) => {
|
||||
// Validate name uniqueness
|
||||
if (!isTemplateNameUnique(template.name, editingTemplate?.id)) {
|
||||
alert(
|
||||
"A template with this name already exists. Please choose a different name.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let finalTemplate = template;
|
||||
|
||||
if (editingTemplate) {
|
||||
// Update existing template - keep same ID
|
||||
finalTemplate = { ...template, id: editingTemplate.id };
|
||||
const updatedTemplates = settings.templates.map((t) =>
|
||||
t.id === editingTemplate.id ? finalTemplate : t,
|
||||
);
|
||||
onChange("templates", updatedTemplates);
|
||||
} else {
|
||||
// Add new template - generate unique ID
|
||||
const uniqueId = generateTemplateId(template.name);
|
||||
finalTemplate = { ...template, id: uniqueId };
|
||||
|
||||
// Update statuses with the final template ID
|
||||
finalTemplate.statuses = finalTemplate.statuses.map(
|
||||
(status) => ({
|
||||
...status,
|
||||
templateId: uniqueId,
|
||||
}),
|
||||
);
|
||||
|
||||
onChange("templates", [...settings.templates, finalTemplate]);
|
||||
// Auto-enable new templates
|
||||
handleTemplateToggle(uniqueId, true);
|
||||
}
|
||||
|
||||
setShowEditor(false);
|
||||
setEditingTemplate(undefined);
|
||||
},
|
||||
[editingTemplate, settings, onChange, handleTemplateToggle],
|
||||
);
|
||||
|
||||
const handleDeleteTemplate = useCallback(
|
||||
(templateId: string) => {
|
||||
// Show confirmation
|
||||
const confirmed = confirm(
|
||||
"Are you sure you want to delete this template? This action cannot be undone.",
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
// Remove from custom templates
|
||||
const updatedTemplates = (settings.templates || []).filter(
|
||||
(t) => t.id !== templateId,
|
||||
);
|
||||
onChange("templates", updatedTemplates);
|
||||
|
||||
// Remove from enabled templates
|
||||
const updatedEnabled = settings.enabledTemplates.filter(
|
||||
(id) => id !== templateId,
|
||||
);
|
||||
onChange("enabledTemplates", updatedEnabled);
|
||||
},
|
||||
[settings.templates, settings.enabledTemplates, onChange],
|
||||
);
|
||||
|
||||
const handleResetToDefaults = useCallback(() => {
|
||||
const confirmed = confirm(
|
||||
"Reset to default templates? This will:\n" +
|
||||
"• Remove all custom templates\n" +
|
||||
"• Reset enabled templates to defaults\n" +
|
||||
"This action cannot be undone.",
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
onChange("templates", []);
|
||||
onChange("enabledTemplates", ["colorful"]);
|
||||
}, [onChange]);
|
||||
|
||||
const handleCancelEditor = useCallback(() => {
|
||||
setShowEditor(false);
|
||||
setEditingTemplate(undefined);
|
||||
}, []);
|
||||
|
||||
if (showEditor) {
|
||||
return (
|
||||
<div>
|
||||
<h3>Status templates</h3>
|
||||
<TemplateEditorModal
|
||||
template={editingTemplate}
|
||||
onSave={handleSaveTemplate}
|
||||
onCancel={handleCancelEditor}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>Status templates</h3>
|
||||
<p>
|
||||
Enable predefined templates to quickly add common status
|
||||
workflows
|
||||
workflows, or create your own custom templates.
|
||||
</p>
|
||||
<div>
|
||||
{settings.templates.map((template) => (
|
||||
<TemplateItem
|
||||
key={template.id}
|
||||
template={template}
|
||||
isEnabled={settings.enabledTemplates.includes(
|
||||
template.id,
|
||||
)}
|
||||
onToggle={handleTemplateToggle}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className="template-settings-actions">
|
||||
<button
|
||||
className="mod-cta template-create-btn"
|
||||
onClick={handleCreateTemplate}
|
||||
>
|
||||
+ Create Template
|
||||
</button>
|
||||
<button
|
||||
className="template-reset-btn"
|
||||
onClick={handleResetToDefaults}
|
||||
>
|
||||
🔄 Reset to Defaults
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Custom Templates Section */}
|
||||
<div className="template-section">
|
||||
<h4 className="template-section-title">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M20 14.66V20a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h5.34" />
|
||||
<polygon points="18,2 22,6 12,16 8,16 8,12 18,2" />
|
||||
</svg>
|
||||
Custom Templates
|
||||
</h4>
|
||||
<div className="template-list">
|
||||
{settings.templates.map((template) => (
|
||||
<TemplateItem
|
||||
key={template.id}
|
||||
template={template}
|
||||
isEnabled={settings.enabledTemplates.includes(
|
||||
template.id,
|
||||
)}
|
||||
onToggle={handleTemplateToggle}
|
||||
onEdit={handleEditTemplate}
|
||||
onDelete={handleDeleteTemplate}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ interface SelectableListItemProps {
|
|||
focused?: boolean;
|
||||
icon?: ReactNode;
|
||||
children: ReactNode;
|
||||
onClick?: () => void;
|
||||
onClick?: (e: React.MouseEvent) => void;
|
||||
className?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
|
@ -21,10 +21,10 @@ export const SelectableListItem: React.FC<SelectableListItemProps> = ({
|
|||
}) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
if (onClick) {
|
||||
setTimeout(() => {
|
||||
onClick();
|
||||
onClick(e);
|
||||
}, 150);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,37 +15,6 @@
|
|||
gap: var(--size-2-2);
|
||||
}
|
||||
|
||||
/* Template Item */
|
||||
.template-item {
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-m);
|
||||
margin-bottom: var(--size-4-2);
|
||||
padding: var(--size-4-3);
|
||||
background: var(--background-primary-alt);
|
||||
cursor: pointer;
|
||||
transition: all var(--anim-duration-fast) ease;
|
||||
}
|
||||
|
||||
.template-item:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.template-item.enabled {
|
||||
background: var(--background-modifier-success);
|
||||
border-color: var(--interactive-success);
|
||||
}
|
||||
|
||||
.template-checkbox {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.template-statuses {
|
||||
margin-top: var(--size-2-2);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--size-2-1);
|
||||
}
|
||||
|
||||
/* Custom Status Item - BEM Methodology */
|
||||
.custom-status-item {
|
||||
background: var(--background-secondary);
|
||||
|
|
|
|||
288
styles/components/template-settings.css
Normal file
288
styles/components/template-settings.css
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
/* ==========================================================================
|
||||
Template Settings Components
|
||||
========================================================================== */
|
||||
|
||||
/* Template Settings Actions */
|
||||
.template-settings-actions {
|
||||
display: flex;
|
||||
gap: var(--size-4-2);
|
||||
margin-bottom: var(--size-4-4);
|
||||
padding: var(--size-4-2);
|
||||
background: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-m);
|
||||
}
|
||||
|
||||
.template-create-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-2-2);
|
||||
padding: var(--size-2-3) var(--size-4-3);
|
||||
font-weight: var(--font-medium);
|
||||
font-size: var(--font-ui-medium);
|
||||
}
|
||||
|
||||
.template-reset-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-2-2);
|
||||
padding: var(--size-2-3) var(--size-4-3);
|
||||
background: var(--background-modifier-border);
|
||||
border: 1px solid var(--background-modifier-border-hover);
|
||||
border-radius: var(--radius-s);
|
||||
color: var(--text-normal);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-ui-medium);
|
||||
transition: all var(--anim-duration-fast) ease;
|
||||
}
|
||||
|
||||
.template-reset-btn:hover {
|
||||
background: var(--background-modifier-border-hover);
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
/* Template Sections */
|
||||
.template-section {
|
||||
margin-bottom: var(--size-4-6);
|
||||
}
|
||||
|
||||
.template-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-2-2);
|
||||
font-size: var(--font-ui-large);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-normal);
|
||||
margin-bottom: var(--size-4-3);
|
||||
padding-bottom: var(--size-2-2);
|
||||
border-bottom: 2px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.template-section-title svg {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.template-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-4-2);
|
||||
}
|
||||
|
||||
/* Template Item Updates */
|
||||
.template-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-m);
|
||||
background: var(--background-primary-alt);
|
||||
transition: all var(--anim-duration-fast) ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.template-item:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.template-item.enabled {
|
||||
background: var(--background-modifier-success);
|
||||
border-color: var(--interactive-success);
|
||||
}
|
||||
|
||||
.template-item-content {
|
||||
flex: 1;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.template-item-main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.template-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-2-2);
|
||||
margin-bottom: var(--size-2-1);
|
||||
}
|
||||
|
||||
.template-custom-badge {
|
||||
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);
|
||||
border-radius: var(--radius-s);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* Template Item Actions */
|
||||
.template-item-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--size-4-2);
|
||||
gap: var(--size-2-1);
|
||||
background: var(--background-modifier-border);
|
||||
border-left: 1px solid var(--background-modifier-border-hover);
|
||||
min-width: 44px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.template-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
border-radius: var(--radius-s);
|
||||
cursor: pointer;
|
||||
transition: all var(--anim-duration-fast) ease;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.template-action-btn:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.template-action-btn--edit {
|
||||
background: var(--background-modifier-border-hover);
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
.template-action-btn--edit:hover {
|
||||
background: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
.template-action-btn--delete {
|
||||
background: var(--background-modifier-error);
|
||||
color: var(--text-error);
|
||||
}
|
||||
|
||||
.template-action-btn--delete:hover {
|
||||
background: var(--text-error);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
/* Template Editor Modal */
|
||||
.template-editor-modal {
|
||||
background: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-l);
|
||||
box-shadow: var(--shadow-l);
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.template-editor-modal__header {
|
||||
padding: var(--size-4-4) var(--size-4-6);
|
||||
background: var(--background-secondary);
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-l) var(--radius-l) 0 0;
|
||||
}
|
||||
|
||||
.template-editor-modal__header h2 {
|
||||
margin: 0;
|
||||
font-size: var(--font-ui-larger);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.template-editor-modal__content {
|
||||
padding: var(--size-4-6);
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.template-editor-statuses {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-4-3);
|
||||
}
|
||||
|
||||
.template-editor-add-status {
|
||||
align-self: flex-start;
|
||||
margin-top: var(--size-4-2);
|
||||
}
|
||||
|
||||
.template-editor-modal__actions {
|
||||
display: flex;
|
||||
gap: var(--size-4-2);
|
||||
justify-content: flex-end;
|
||||
padding: var(--size-4-4) var(--size-4-6);
|
||||
background: var(--background-secondary);
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
border-radius: 0 0 var(--radius-l) var(--radius-l);
|
||||
}
|
||||
|
||||
.template-editor-modal__actions button {
|
||||
padding: var(--size-2-3) var(--size-4-4);
|
||||
border-radius: var(--radius-m);
|
||||
font-size: var(--font-ui-medium);
|
||||
font-weight: var(--font-medium);
|
||||
cursor: pointer;
|
||||
transition: all var(--anim-duration-fast) ease;
|
||||
}
|
||||
|
||||
.template-editor-modal__actions button:not(.mod-cta) {
|
||||
background: var(--background-modifier-border);
|
||||
border: 1px solid var(--background-modifier-border-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.template-editor-modal__actions button:not(.mod-cta):hover {
|
||||
background: var(--background-modifier-border-hover);
|
||||
}
|
||||
|
||||
.template-editor-modal__actions .mod-cta:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.template-editor-modal__actions .mod-cta:disabled:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.template-settings-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.template-item {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.template-item-actions {
|
||||
flex-direction: row;
|
||||
border-left: none;
|
||||
border-top: 1px solid var(--background-modifier-border-hover);
|
||||
min-width: unset;
|
||||
padding: var(--size-2-3) var(--size-4-2);
|
||||
}
|
||||
|
||||
.template-editor-modal {
|
||||
margin: var(--size-4-2);
|
||||
max-width: calc(100% - var(--size-4-4));
|
||||
}
|
||||
}
|
||||
|
||||
/* Scrollbar for template editor */
|
||||
.template-editor-modal__content::-webkit-scrollbar {
|
||||
width: var(--scrollbar-width);
|
||||
}
|
||||
|
||||
.template-editor-modal__content::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-thumb-bg);
|
||||
border-radius: var(--radius-s);
|
||||
}
|
||||
|
||||
.template-editor-modal__content::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--scrollbar-active-bg);
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
/* Import component stylesheets */
|
||||
@import "components/status-bar.css";
|
||||
@import "components/settings.css";
|
||||
@import "components/template-settings.css";
|
||||
@import "components/dashboard.css";
|
||||
@import "components/grouped-view.css";
|
||||
@import "components/file-explorer.css";
|
||||
|
|
|
|||
52
utils/templateUtils.ts
Normal file
52
utils/templateUtils.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { StatusTemplate } from "@/types/pluginSettings";
|
||||
import settingsService from "@/core/settingsService";
|
||||
import { PREDEFINED_TEMPLATES } from "@/constants/predefinedTemplates";
|
||||
|
||||
/**
|
||||
* Get all enabled templates
|
||||
*/
|
||||
export const isCustomTemplate = (templateId: string): boolean => {
|
||||
const i = PREDEFINED_TEMPLATES.findIndex((t) => t.id === templateId);
|
||||
return i === -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get template by ID
|
||||
*/
|
||||
export const getTemplateById = (id: string): StatusTemplate | undefined => {
|
||||
const allTemplates = settingsService.settings.templates;
|
||||
return allTemplates.find((template) => template.id === id);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate template name uniqueness
|
||||
*/
|
||||
export const isTemplateNameUnique = (
|
||||
name: string,
|
||||
excludeId?: string,
|
||||
): boolean => {
|
||||
const allTemplates = settingsService.settings.templates;
|
||||
return !allTemplates.some(
|
||||
(template) =>
|
||||
template.name.toLowerCase() === name.toLowerCase() &&
|
||||
template.id !== excludeId,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate unique template ID
|
||||
*/
|
||||
export const generateTemplateId = (name: string): string => {
|
||||
const baseId = name.toLowerCase().replace(/[^a-z0-9]/g, "-");
|
||||
const allTemplates = settingsService.settings.templates;
|
||||
|
||||
let counter = 1;
|
||||
let id = `custom-${baseId}`;
|
||||
|
||||
while (allTemplates.some((template) => template.id === id)) {
|
||||
id = `custom-${baseId}-${counter}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
return id;
|
||||
};
|
||||
Loading…
Reference in a new issue