settings update, better previews

This commit is contained in:
gavinpicard 2025-12-21 19:20:58 -06:00
parent 23100950c6
commit e7a85bb760
7 changed files with 384 additions and 279 deletions

143
README.md
View file

@ -1,50 +1,34 @@
# Iconocolor
An all-in-one Obsidian plugin for customizing folder icons, icon colors, and folder colors in the file explorer.
An all-in-one Obsidian plugin for customizing folder icons, icon colors, and folder colors in the file explorer with powerful color transformation and inheritance systems.
## Features
- 🎨 **Folder Icons**: Add custom icons (SVG, PNG, or URLs) to any folder
- 🔍 **Icon Search**: Search and preview icons from Lucide (built-in) and SimpleIcons
- 🌈 **Icon Colors**: Customize the color of folder icons
- 🎨 **Folder Colors**: Set background and text colors for folders
- 📁 **Subfolder Support**: Optionally apply configurations to subfolders
- ⚙️ **Easy Configuration**: Right-click any folder to configure it
## Installation
### From Obsidian
1. Open Obsidian Settings
2. Navigate to **Community plugins** and disable Safe Mode
3. Click **Browse** and search for "Iconocolor"
4. Install the plugin
5. Enable the plugin in the **Community plugins** tab
### Manual Installation
1. Download the latest release
2. Extract the files to your vault's `.obsidian/plugins/iconocolor/` folder
3. Reload Obsidian
4. Enable the plugin in **Settings → Community plugins**
- 🔍 **Icon Search**: Search and preview icons from Lucide (Obsidian's built-in icon library)
- 🌈 **Color Palettes**: Create and manage color palettes for automatic color assignment
- 🎯 **Auto-Coloring**: Automatically assign colors from palettes to root folders
- 🔄 **Color Transformations**: Derive icon, folder, and text colors from base colors using HSL or lightness adjustments
- 🌊 **Child Color Inheritance**: Child folders automatically inherit and transform colors from parent folders
- 📐 **Gradient Mode**: Smooth color interpolation between sibling folders
- 💾 **Profiles**: Save and switch between different color schemes and transformations
- 📋 **Default Icon Rules**: Automatically apply icons based on folder/file name patterns
- ⚙️ **Easy Configuration**: Right-click any folder to configure it, or use the settings panel
## Usage
### Setting Icons and Colors for a Folder
1. Right-click on any folder in the file explorer
2. Select **"Set icon and colors"**
3. In the icon picker:
- **Lucide tab**: Search and browse built-in Lucide icons (Obsidian's native icon library)
- **SimpleIcons tab**: Search and browse SimpleIcons (popular brand and technology icons)
- **Custom tab**: Enter the path to an icon file (e.g., `/path/to/icon.svg`) or a URL
- Optionally set an icon color (hex format like `#FF0000`)
2. Select **"Set Icon and Colors"**
3. In the Icon/Color Modal:
- Search and browse built-in Lucide icons or added icon packs
- Icons are displayed with live previews as you search
4. In the color picker:
- Optionally set a folder background color
- Optionally set a text color for the folder name
5. Choose whether to apply to subfolders
6. Click **Apply**
- Set a **base color** (the foundation color for this folder)
- Optionally set explicit icon, folder background, or text colors (these override transformations)
- Choose whether child folders should inherit the base color
5. Click **Apply**
### Managing Configurations
@ -52,13 +36,86 @@ An all-in-one Obsidian plugin for customizing folder icons, icon colors, and fol
- Edit or remove configurations from the settings tab
- Right-click a configured folder and select **"Remove icon and colors"** to clear its configuration
### Color Palettes and Auto-Coloring
1. Go to **Settings → Iconocolor → Color palettes**
2. Create or edit color palettes (collections of colors with consistent lightness)
3. Enable **Auto-color root folders** to automatically assign palette colors to root folders
4. Choose between **Gradient** (smooth color transitions) or **Repeat** (cycle through colors) mode
### Profiles
Save and switch between different color schemes:
1. Configure your desired settings (transformations, palettes, etc.)
2. Go to **Settings → Iconocolor → Profiles**
3. Click **Create profile** and give it a name
4. Switch between profiles anytime to instantly apply different color schemes
## Understanding Color Transformations
Iconocolor uses a powerful two-level color transformation system:
### Base Color
Every folder has a **base color** - the fundamental color that serves as the foundation. This can be:
- Set explicitly for a folder
- Inherited from a parent folder (if enabled)
- Automatically assigned from a color palette (for root folders)
### Base Transformations (Icon, Folder, Text Colors)
Base transformations determine how the **icon color**, **folder background color**, and **text color** are derived from the base color. These are global settings that apply to all folders.
**Transformation Types:**
- **None**: Use the base color directly
- **Lightness adjustment**: Make the color lighter or darker by a percentage (e.g., +20% lighter, -15% darker)
- **HSL transformation**: Adjust hue, saturation, and lightness independently
**Example:**
- Base color: `#3B82F6` (blue)
- Icon transformation: `None` → Icon is blue
- Folder transformation: `Lightness -15%` → Folder background is darker blue
- Text transformation: `Lightness +30%` → Text is lighter blue for contrast
### Child Base Transformation (Inheritance)
Child base transformation controls how child folders get their base color from parent folders. This creates a hierarchical color system.
**Transformation Types:**
- **None**: Children don't inherit colors (each folder is independent)
- **Lightness adjustment**: Each child's base color is adjusted from its parent's base color (cumulative - each level gets progressively lighter/darker)
- **HSL transformation**: Each child's base color is transformed using HSL adjustments (cumulative)
**Gradient Mode:**
When enabled, child folders interpolate between the parent's base color and the next sibling's base color before applying the transformation. This creates smooth color gradients across sibling folders.
**Example with Lightness Adjustment:**
```
Root folder: Base = #3B82F6 (blue)
├─ Child 1: Base = #5B9FF6 (10% lighter)
│ ├─ Grandchild 1: Base = #7BBFF6 (10% lighter than child, 20% lighter than root)
│ └─ Grandchild 2: Base = #9DDFF6 (10% lighter than child, 20% lighter than root)
└─ Child 2: Base = #5B9FF6 (10% lighter)
```
**Example with Gradient Mode:**
```
Root folder: Base = #3B82F6 (blue)
Next sibling: Base = #EC4899 (pink)
├─ Child 1: Base = interpolated between blue and pink (step 1), then transformed
├─ Child 2: Base = interpolated between blue and pink (step 2), then transformed
└─ Child 3: Base = interpolated between blue and pink (step 3), then transformed
```
The gradient automatically distributes across all children, creating smooth color transitions.
## Icon Sources
The plugin supports multiple icon sources:
### Built-in Icon Libraries
- **Lucide Icons**: Built into Obsidian, no download required. Includes hundreds of icons like folder, file, home, settings, etc.
- **SimpleIcons**: Popular brand and technology icons (fetched from CDN). Includes icons for GitHub, Docker, JavaScript, React, and thousands more.
### Custom Icons
- **SVG files**: Best for icons as they can be colored easily
@ -99,11 +156,19 @@ npm run build
## How It Works
The plugin:
1. Observes the file explorer DOM for changes
2. Applies custom styles and icons to folders based on your configuration
3. Stores all configurations in the plugin's data file
4. Supports inheritance from parent folders when "apply to subfolders" is enabled
The plugin uses a sophisticated color system:
1. **Base Color Assignment**: Each folder gets a base color (explicit, inherited, or from palette)
2. **Color Transformations**: Base transformations derive icon, folder, and text colors from the base color
3. **Child Inheritance**: Child folders inherit and transform their parent's base color using child base transformations
4. **DOM Observation**: The plugin observes the file explorer DOM and applies styles in real-time
5. **Configuration Storage**: All settings are stored in the plugin's data file
The transformation system allows you to:
- Create cohesive color schemes across your folder structure
- Automatically generate contrasting colors for readability
- Build visual hierarchies through cumulative color transformations
- Experiment with different color schemes using profiles
## License

134
main.ts
View file

@ -1,134 +0,0 @@
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
// Remember to rename these classes and interfaces!
interface MyPluginSettings {
mySetting: string;
}
const DEFAULT_SETTINGS: MyPluginSettings = {
mySetting: 'default'
}
export default class MyPlugin extends Plugin {
settings: MyPluginSettings;
async onload() {
await this.loadSettings();
// This creates an icon in the left ribbon.
const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (_evt: MouseEvent) => {
// Called when the user clicks the icon.
new Notice('This is a notice!');
});
// Perform additional things with the ribbon
ribbonIconEl.addClass('my-plugin-ribbon-class');
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
const statusBarItemEl = this.addStatusBarItem();
statusBarItemEl.setText('Status Bar Text');
// This adds a simple command that can be triggered anywhere
this.addCommand({
id: 'open-sample-modal-simple',
name: 'Open sample modal (simple)',
callback: () => {
new SampleModal(this.app).open();
}
});
// This adds an editor command that can perform some operation on the current editor instance
this.addCommand({
id: 'sample-editor-command',
name: 'Sample editor command',
editorCallback: (editor: Editor, _view: MarkdownView) => {
console.log(editor.getSelection());
editor.replaceSelection('Sample Editor Command');
}
});
// This adds a complex command that can check whether the current state of the app allows execution of the command
this.addCommand({
id: 'open-sample-modal-complex',
name: 'Open sample modal (complex)',
checkCallback: (checking: boolean) => {
// Conditions to check
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (markdownView) {
// If checking is true, we're simply "checking" if the command can be run.
// If checking is false, then we want to actually perform the operation.
if (!checking) {
new SampleModal(this.app).open();
}
// This command will only show up in Command Palette when the check function returns true
return true;
}
}
});
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new SampleSettingTab(this.app, this));
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
// Using this function will automatically remove the event listener when this plugin is disabled.
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
console.log('click', evt);
});
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
}
onunload() {
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
}
class SampleModal extends Modal {
constructor(app: App) {
super(app);
}
onOpen() {
const {contentEl} = this;
contentEl.setText('Woah!');
}
onClose() {
const {contentEl} = this;
contentEl.empty();
}
}
class SampleSettingTab extends PluginSettingTab {
plugin: MyPlugin;
constructor(app: App, plugin: MyPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
containerEl.empty();
new Setting(containerEl)
.setName('Setting #1')
.setDesc('It\'s a secret')
.addText(text => text
.setPlaceholder('Enter your secret')
.setValue(this.plugin.settings.mySetting)
.onChange(async (value) => {
this.plugin.settings.mySetting = value;
await this.plugin.saveSettings();
}));
}
}

View file

@ -4,6 +4,6 @@
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "All-in-one solution for folder icons, icon colors, and folder colors in Obsidian.",
"author": "",
"author": "Gavin Picard",
"isDesktopOnly": false
}

View file

@ -1,15 +1,15 @@
{
"name": "obsidian-sample-plugin",
"name": "iconocolor",
"version": "1.0.0",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"description": "All-in-one solution for folder icons, icon colors, and folder colors in Obsidian.",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [],
"author": "",
"keywords": ["icon", "color", "folder", "icons", "colors", "folder colors", "icon colors", "folder icons"],
"author": "Gavin Picard",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",

View file

@ -1,4 +1,4 @@
import { Plugin, TFolder } from 'obsidian';
import { Plugin, TFolder, Modal, Notice, App } from 'obsidian';
import { IconocolorSettings, FolderConfig, SettingsProfile } from './types';
import { DEFAULT_SETTINGS } from './settings';
import { FolderManager } from './folderManager';
@ -62,6 +62,68 @@ export default class IconocolorPlugin extends Plugin {
this.app.setting.openTabById(this.manifest.id);
},
});
// Add command to switch profiles
this.addCommand({
id: 'switch-profile',
name: 'Switch profile',
callback: async () => {
const profiles = this.settings.profiles || [];
if (profiles.length === 0) {
new Notice('No profiles available. Create a profile in settings first.');
return;
}
// Create a simple modal to select profile
const modal = new ProfileSwitchModal(this.app, profiles, async (profileId: string) => {
await this.loadProfile(profileId);
});
modal.open();
},
});
}
/**
* Load a profile (public method for command palette)
*/
async loadProfile(profileId: string): Promise<void> {
const profile = this.settings.profiles?.find(p => p.id === profileId);
if (!profile) {
new Notice('Profile not found');
return;
}
// Helper function for deep cloning
const deepClone = <T>(obj: T): T => {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj.getTime()) as unknown as T;
if (obj instanceof Array) return obj.map(item => deepClone(item)) as unknown as T;
const cloned = {} as T;
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
cloned[key] = deepClone(obj[key]);
}
}
return cloned;
};
// Apply profile settings
// Note: colorPalettes are NOT loaded from profiles - they remain global
if (profile.iconSize !== undefined) this.settings.iconSize = profile.iconSize;
if (profile.activePaletteIndex !== undefined) this.settings.activePaletteIndex = profile.activePaletteIndex;
if (profile.autoColorEnabled !== undefined) this.settings.autoColorEnabled = profile.autoColorEnabled;
if (profile.autoColorMode) this.settings.autoColorMode = profile.autoColorMode;
if (profile.iconColorTransformation) this.settings.iconColorTransformation = deepClone(profile.iconColorTransformation);
if (profile.folderColorTransformation) this.settings.folderColorTransformation = deepClone(profile.folderColorTransformation);
if (profile.textColorTransformation) this.settings.textColorTransformation = deepClone(profile.textColorTransformation);
if (profile.childBaseTransformation) this.settings.childBaseTransformation = deepClone(profile.childBaseTransformation);
if (profile.folderColorOpacity !== undefined) this.settings.folderColorOpacity = profile.folderColorOpacity;
if (profile.defaultIconRules) this.settings.defaultIconRules = deepClone(profile.defaultIconRules);
this.settings.activeProfileId = profileId;
await this.saveSettings();
await this.folderManager.updateSettings(this.settings);
new Notice(`Profile "${profile.name}" loaded`);
}
onunload() {
@ -240,3 +302,42 @@ export default class IconocolorPlugin extends Plugin {
// Export for use in settings tab
export { IconocolorPlugin };
/**
* Modal for switching profiles from command palette
*/
class ProfileSwitchModal extends Modal {
private profiles: SettingsProfile[];
private onSelect: (profileId: string) => Promise<void>;
constructor(app: App, profiles: SettingsProfile[], onSelect: (profileId: string) => Promise<void>) {
super(app);
this.profiles = profiles;
this.onSelect = onSelect;
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.addClass('iconocolor-profile-switch-modal');
contentEl.createEl('h2', { text: 'Switch Profile' });
const profilesList = contentEl.createDiv('iconocolor-profiles-list');
this.profiles.forEach(profile => {
const profileItem = profilesList.createDiv('iconocolor-profile-item');
profileItem.createEl('div', { text: profile.name, cls: 'iconocolor-profile-name' });
profileItem.onclick = async () => {
await this.onSelect(profile.id);
this.close();
};
});
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}

View file

@ -38,61 +38,95 @@ export class IconocolorSettingTab extends PluginSettingTab {
.setHeading()
.setName('General');
new Setting(containerEl)
const iconSizeSetting = new Setting(containerEl)
.setName('Icon size')
.setDesc('Global size for all folder icons in pixels')
.addSlider(slider => {
slider
.setLimits(12, 32, 1)
.setValue(this.plugin.settings.iconSize || 16)
.setDynamicTooltip()
.onChange(async (value) => {
this.plugin.settings.iconSize = value;
.setDesc('Global size for all folder icons in pixels');
let iconSizeTextInput: HTMLInputElement | null = null;
let iconSizeSlider: HTMLInputElement | null = null;
iconSizeSetting.addSlider(slider => {
slider
.setLimits(12, 32, 1)
.setValue(this.plugin.settings.iconSize !== undefined ? this.plugin.settings.iconSize : 16)
.setDynamicTooltip()
.onChange(async (value) => {
this.plugin.settings.iconSize = value;
// Update text input to match slider
if (iconSizeTextInput) {
iconSizeTextInput.value = String(value);
}
await this.plugin.saveSettings();
this.plugin.folderManager.applyAllStyles();
});
// Store reference to slider element
iconSizeSlider = slider.sliderEl;
});
iconSizeSetting.addText(text => {
text
.setValue(String(this.plugin.settings.iconSize !== undefined ? this.plugin.settings.iconSize : 16))
.setPlaceholder('16')
.onChange(async (value) => {
const numValue = parseInt(value, 10);
if (!isNaN(numValue) && numValue >= 12 && numValue <= 32) {
this.plugin.settings.iconSize = numValue;
// Update slider to match text input
if (iconSizeSlider) {
iconSizeSlider.value = String(numValue);
}
await this.plugin.saveSettings();
this.plugin.folderManager.applyAllStyles();
});
})
.addText(text => {
text
.setValue(String(this.plugin.settings.iconSize || 16))
.setPlaceholder('16')
.onChange(async (value) => {
const numValue = parseInt(value, 10);
if (!isNaN(numValue) && numValue >= 12 && numValue <= 32) {
this.plugin.settings.iconSize = numValue;
await this.plugin.saveSettings();
this.plugin.folderManager.applyAllStyles();
}
});
});
}
});
// Store reference to text input element
iconSizeTextInput = text.inputEl;
});
new Setting(containerEl)
const opacitySetting = new Setting(containerEl)
.setName('Folder background opacity')
.setDesc('Global opacity for folder background colors (0-100%). Only applies if a folder has a background color set.')
.addSlider(slider => {
slider
.setLimits(0, 100, 1)
.setValue(this.plugin.settings.folderColorOpacity || 100)
.setDynamicTooltip()
.onChange(async (value) => {
this.plugin.settings.folderColorOpacity = value;
.setDesc('Global opacity for folder background colors (0-100%). Only applies if a folder has a background color set.');
let opacityTextInput: HTMLInputElement | null = null;
let opacitySlider: HTMLInputElement | null = null;
opacitySetting.addSlider(slider => {
slider
.setLimits(0, 100, 1)
.setValue(this.plugin.settings.folderColorOpacity !== undefined ? this.plugin.settings.folderColorOpacity : 100)
.setDynamicTooltip()
.onChange(async (value) => {
this.plugin.settings.folderColorOpacity = value;
// Update text input to match slider
if (opacityTextInput) {
opacityTextInput.value = String(value);
}
await this.plugin.saveSettings();
this.plugin.folderManager.applyAllStyles();
});
// Store reference to slider element
opacitySlider = slider.sliderEl;
});
opacitySetting.addText(text => {
text
.setValue(String(this.plugin.settings.folderColorOpacity !== undefined ? this.plugin.settings.folderColorOpacity : 100))
.setPlaceholder('100')
.onChange(async (value) => {
const numValue = parseInt(value, 10);
if (!isNaN(numValue) && numValue >= 0 && numValue <= 100) {
this.plugin.settings.folderColorOpacity = numValue;
// Update slider to match text input
if (opacitySlider) {
opacitySlider.value = String(numValue);
}
await this.plugin.saveSettings();
this.plugin.folderManager.applyAllStyles();
});
})
.addText(text => {
text
.setValue(String(this.plugin.settings.folderColorOpacity || 100))
.setPlaceholder('100')
.onChange(async (value) => {
const numValue = parseInt(value, 10);
if (!isNaN(numValue) && numValue >= 0 && numValue <= 100) {
this.plugin.settings.folderColorOpacity = numValue;
await this.plugin.saveSettings();
this.plugin.folderManager.applyAllStyles();
}
});
});
}
});
// Store reference to text input element
opacityTextInput = text.inputEl;
});
// Color Palettes section (moved up - foundation for colors)
new Setting(containerEl)
@ -756,29 +790,26 @@ export class IconocolorSettingTab extends PluginSettingTab {
* Add preview showing how base color transforms into element colors
*/
private addBaseTransformationPreview(containerEl: HTMLElement): void {
const previewContainer = containerEl.createDiv();
previewContainer.addClass('transformation-preview');
previewContainer.style.marginBottom = '12px';
previewContainer.style.padding = '8px';
previewContainer.style.background = 'var(--background-secondary)';
previewContainer.style.borderRadius = '4px';
previewContainer.style.border = '1px solid var(--background-modifier-border)';
// Sample base color (use first color from active palette)
const activePalette = this.plugin.settings.colorPalettes[this.plugin.settings.activePaletteIndex || 0];
const sampleBaseColor = activePalette?.colors[0] || '#4ECDC4';
const previewTitle = previewContainer.createEl('div', { text: 'Preview: Base Color → Element Colors' });
previewTitle.style.fontSize = '11px';
previewTitle.style.fontWeight = '600';
previewTitle.style.marginBottom = '8px';
previewTitle.style.color = 'var(--text-normal)';
const previewSetting = new Setting(containerEl)
.setName('Preview: Base Color → Element Colors')
.setDesc('');
const previewContent = previewContainer.createDiv();
// Remove the description element to make it cleaner
const descEl = previewSetting.descEl;
if (descEl) {
descEl.remove();
}
const previewContent = previewSetting.controlEl;
previewContent.style.display = 'flex';
previewContent.style.alignItems = 'center';
previewContent.style.gap = '8px';
previewContent.style.flexWrap = 'wrap';
// Sample base color (use first color from active palette)
const activePalette = this.plugin.settings.colorPalettes[this.plugin.settings.activePaletteIndex || 0];
const sampleBaseColor = activePalette?.colors[0] || '#4ECDC4';
previewContent.style.width = '100%';
// Base color swatch
const baseSwatch = previewContent.createDiv();
@ -790,15 +821,20 @@ export class IconocolorSettingTab extends PluginSettingTab {
const baseColorBox = baseSwatch.createDiv();
baseColorBox.style.width = '36px';
baseColorBox.style.height = '36px';
baseColorBox.style.borderRadius = '3px';
baseColorBox.style.borderRadius = '4px';
baseColorBox.style.border = '1px solid var(--background-modifier-border)';
baseColorBox.style.backgroundColor = sampleBaseColor;
baseColorBox.style.boxShadow = '0 1px 2px rgba(0,0,0,0.1)';
baseColorBox.style.boxShadow = '0 1px 3px rgba(0,0,0,0.12)';
const baseLabel = baseSwatch.createEl('span', { text: 'Base' });
baseLabel.style.fontSize = '9px';
baseLabel.style.color = 'var(--text-muted)';
const baseColorValue = baseSwatch.createEl('span', { text: sampleBaseColor });
baseColorValue.style.fontSize = '8px';
baseColorValue.style.fontFamily = 'var(--font-monospace)';
baseColorValue.style.color = 'var(--text-faint)';
// Arrow
const arrow = previewContent.createEl('span', { text: '→' });
arrow.style.fontSize = '16px';
@ -829,7 +865,7 @@ export class IconocolorSettingTab extends PluginSettingTab {
updatePreview();
// Store update function for later use
(previewContainer as any).updatePreview = updatePreview;
(previewSetting.settingEl as any).updatePreview = updatePreview;
}
/**
@ -845,10 +881,10 @@ export class IconocolorSettingTab extends PluginSettingTab {
const colorBox = preview.createDiv();
colorBox.style.width = '36px';
colorBox.style.height = '36px';
colorBox.style.borderRadius = '3px';
colorBox.style.borderRadius = '4px';
colorBox.style.border = '1px solid var(--background-modifier-border)';
colorBox.style.backgroundColor = color;
colorBox.style.boxShadow = '0 1px 2px rgba(0,0,0,0.1)';
colorBox.style.boxShadow = '0 1px 3px rgba(0,0,0,0.12)';
colorBox.style.display = 'flex';
colorBox.style.alignItems = 'center';
colorBox.style.justifyContent = 'center';
@ -868,15 +904,13 @@ export class IconocolorSettingTab extends PluginSettingTab {
* Update all previews in the settings
*/
private updateAllPreviews(): void {
const basePreview = this.containerEl.querySelector('.transformation-preview') as any;
if (basePreview && basePreview.updatePreview) {
basePreview.updatePreview();
}
const childPreview = this.containerEl.querySelector('.child-transformation-preview') as any;
if (childPreview && childPreview.updatePreview) {
childPreview.updatePreview();
}
// Find previews by checking for updatePreview function on setting elements
const allSettings = this.containerEl.querySelectorAll('.setting-item');
allSettings.forEach(setting => {
if ((setting as any).updatePreview) {
(setting as any).updatePreview();
}
});
}
/**
@ -899,31 +933,27 @@ export class IconocolorSettingTab extends PluginSettingTab {
* Add preview showing how child base colors are derived from parent
*/
private addChildBaseTransformationPreview(containerEl: HTMLElement): void {
const previewContainer = containerEl.createDiv();
previewContainer.addClass('child-transformation-preview');
previewContainer.style.marginBottom = '12px';
previewContainer.style.padding = '8px';
previewContainer.style.background = 'var(--background-secondary)';
previewContainer.style.borderRadius = '4px';
previewContainer.style.border = '1px solid var(--background-modifier-border)';
const previewTitle = previewContainer.createEl('div', { text: 'Preview: Parent → Child Colors' });
previewTitle.style.fontSize = '11px';
previewTitle.style.fontWeight = '600';
previewTitle.style.marginBottom = '8px';
previewTitle.style.color = 'var(--text-normal)';
// Sample parent color
const activePalette = this.plugin.settings.colorPalettes[this.plugin.settings.activePaletteIndex || 0];
const parentColor = activePalette?.colors[0] || '#4ECDC4';
const nextSiblingColor = activePalette?.colors[1] || '#45B7D1';
// Create static parent swatch and arrow (don't recreate on update)
const previewContent = previewContainer.createDiv();
const previewSetting = new Setting(containerEl)
.setName('Preview: Parent → Child Colors')
.setDesc('');
// Remove the description element to make it cleaner
const descEl = previewSetting.descEl;
if (descEl) {
descEl.remove();
}
const previewContent = previewSetting.controlEl;
previewContent.style.display = 'flex';
previewContent.style.alignItems = 'center';
previewContent.style.gap = '8px';
previewContent.style.flexWrap = 'wrap';
previewContent.style.width = '100%';
// Parent color (static)
const parentSwatch = previewContent.createDiv();
@ -935,15 +965,20 @@ export class IconocolorSettingTab extends PluginSettingTab {
const parentBox = parentSwatch.createDiv();
parentBox.style.width = '36px';
parentBox.style.height = '36px';
parentBox.style.borderRadius = '3px';
parentBox.style.borderRadius = '4px';
parentBox.style.border = '1px solid var(--background-modifier-border)';
parentBox.style.backgroundColor = parentColor;
parentBox.style.boxShadow = '0 1px 2px rgba(0,0,0,0.1)';
parentBox.style.boxShadow = '0 1px 3px rgba(0,0,0,0.12)';
const parentLabel = parentSwatch.createEl('span', { text: 'Parent' });
parentLabel.style.fontSize = '9px';
parentLabel.style.color = 'var(--text-muted)';
const parentColorValue = parentSwatch.createEl('span', { text: parentColor });
parentColorValue.style.fontSize = '8px';
parentColorValue.style.fontFamily = 'var(--font-monospace)';
parentColorValue.style.color = 'var(--text-faint)';
// Arrow (static)
const arrow = previewContent.createEl('span', { text: '→' });
arrow.style.fontSize = '16px';
@ -995,21 +1030,26 @@ export class IconocolorSettingTab extends PluginSettingTab {
const childBox = childSwatch.createDiv();
childBox.style.width = '36px';
childBox.style.height = '36px';
childBox.style.borderRadius = '3px';
childBox.style.borderRadius = '4px';
childBox.style.border = '1px solid var(--background-modifier-border)';
childBox.style.backgroundColor = childBaseColor;
childBox.style.boxShadow = '0 1px 2px rgba(0,0,0,0.1)';
childBox.style.boxShadow = '0 1px 3px rgba(0,0,0,0.12)';
const childLabel = childSwatch.createEl('span', { text: `C${i + 1}` });
childLabel.style.fontSize = '9px';
childLabel.style.color = 'var(--text-muted)';
const childColorValue = childSwatch.createEl('span', { text: childBaseColor });
childColorValue.style.fontSize = '8px';
childColorValue.style.fontFamily = 'var(--font-monospace)';
childColorValue.style.color = 'var(--text-faint)';
}
};
updatePreview();
// Store update function for later use
(previewContainer as any).updatePreview = updatePreview;
(previewSetting.settingEl as any).updatePreview = updatePreview;
}
/**
@ -1098,6 +1138,7 @@ export class IconocolorSettingTab extends PluginSettingTab {
(this.plugin.settings[settingKey] as any).saturation = Math.max(-100, Math.min(100, numValue));
await this.plugin.saveSettings();
await this.plugin.folderManager.updateSettings(this.plugin.settings);
this.updateAllPreviews();
}
});
});
@ -1114,6 +1155,7 @@ export class IconocolorSettingTab extends PluginSettingTab {
(this.plugin.settings[settingKey] as any).lightness = Math.max(-100, Math.min(100, numValue));
await this.plugin.saveSettings();
await this.plugin.folderManager.updateSettings(this.plugin.settings);
this.updateAllPreviews();
}
});
});

View file

@ -519,6 +519,37 @@
margin: 16px 0;
}
/* Profile switch modal */
.iconocolor-profile-switch-modal .modal-content {
padding: 20px;
}
.iconocolor-profiles-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 16px;
}
.iconocolor-profile-item {
padding: 12px 16px;
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
background-color: var(--background-secondary);
cursor: pointer;
transition: all 0.2s ease;
}
.iconocolor-profile-item:hover {
background-color: var(--background-modifier-hover);
border-color: var(--interactive-accent);
}
.iconocolor-profile-name {
font-weight: 500;
color: var(--text-normal);
}
/* Folder config modal - modern compact layout */
.folder-config-modal {
max-width: 800px;