From e7a85bb7606e9870cd1ccca4bd4e65945958bb25 Mon Sep 17 00:00:00 2001 From: gavinpicard Date: Sun, 21 Dec 2025 19:20:58 -0600 Subject: [PATCH] settings update, better previews --- README.md | 143 ++++++++++++++++++------- main.ts | 134 ----------------------- manifest.json | 2 +- package.json | 8 +- src/main.ts | 103 +++++++++++++++++- src/ui/settingsTab.ts | 242 +++++++++++++++++++++++++----------------- styles.css | 31 ++++++ 7 files changed, 384 insertions(+), 279 deletions(-) delete mode 100644 main.ts diff --git a/README.md b/README.md index cb2c013..a4d2319 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/main.ts b/main.ts deleted file mode 100644 index 471bc8e..0000000 --- a/main.ts +++ /dev/null @@ -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(); - })); - } -} diff --git a/manifest.json b/manifest.json index 765f8a0..8ec7471 100644 --- a/manifest.json +++ b/manifest.json @@ -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 } diff --git a/package.json b/package.json index 6a00766..408803d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/main.ts b/src/main.ts index a805019..a91caa3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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 { + 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 = (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; + + constructor(app: App, profiles: SettingsProfile[], onSelect: (profileId: string) => Promise) { + 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(); + } +} + diff --git a/src/ui/settingsTab.ts b/src/ui/settingsTab.ts index 668f5aa..e967475 100644 --- a/src/ui/settingsTab.ts +++ b/src/ui/settingsTab.ts @@ -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(); } }); }); diff --git a/styles.css b/styles.css index 7f75010..7bfd3cc 100644 --- a/styles.css +++ b/styles.css @@ -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;