├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmrc ├── .prettierrc.js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets ├── ColorfulNoteBordersDemo800.gif ├── ColorfulNoteBordersDemov0.3.gif └── PopupWindow.png ├── esbuild.config.mjs ├── jest.config.js ├── manifest.json ├── package-lock.json ├── package.json ├── release.sh ├── src ├── main.ts └── settingsTab.ts ├── styles.css ├── tests ├── __mocks__ │ └── obsidian.ts ├── global-setup.js └── main.test.ts ├── tsconfig.json ├── version-bump.mjs └── versions.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # See http://EditorConfig.org for more information about .editorconfig files. 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | indent_size = 4 11 | tab_width = 4 12 | 13 | [*.xml] 14 | indent_size = 2 15 | 16 | [*.json] 17 | indent_size = 4 18 | # Stop IDEs adding newlines to end of Obsidian .json config files: 19 | insert_final_newline = false 20 | 21 | [*.{yml,yaml}] 22 | indent_size = 2 23 | 24 | [*.{md,mdx}] 25 | trim_trailing_whitespace = true 26 | 27 | [*.{htm,html,js,jsm,ts,tsx,mjs}] 28 | indent_size = 4 29 | 30 | [*.{cmd,bat,ps1}] 31 | end_of_line = crlf 32 | 33 | [*.sh] 34 | end_of_line = lf 35 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | src/main.js 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'all', // "es5" 3 | printWidth: 120, 4 | tabWidth: 4, 5 | useTabs: false, 6 | singleQuote: true, 7 | bracketSpacing: true, 8 | semi: triggerAsyncId, 9 | }; 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## NEXT (NEXT) 4 | 5 | ### Bugfixes 6 | 7 | - Fixed issue #4: Folder string that is part of a filename will be selected 8 | 9 | ## 0.2.4 (2023-03-30) 10 | 11 | ### Bugfixes 12 | 13 | - Improved responsiveness; color rules are now applied to all views. 14 | 15 | ## 0.2.3 (2023-03-28) 16 | 17 | - code cleanup 18 | 19 | ## 0.2.1 (2023-03-27) 20 | 21 | ### Bugfixes 22 | 23 | - Multiple matching rules are now applied correctly based on their order - first rule takes precedence. 24 | 25 | ## 0.2.0 (2023-03-27) 26 | 27 | ### Features 28 | 29 | - Added ability to re-order the rules to prioritize which rule takes precedence when multiple rules match. The last rule takes precedence. 30 | 31 | ### Bugfixes 32 | 33 | - updated the settings tab layout 34 | - fixed color input and color picker so that they update properly when the other is changed 35 | 36 | ## 0.1.1 (2023-03-27) 37 | 38 | Initial Release 39 | 40 | ### Features 41 | 42 | - Apply colorful borders to notes based on customizable rules 43 | - Supports two types of rules: 44 | - Folder location 45 | - Frontmatter metadata 46 | - Add and remove rules as needed 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Released under MIT License 2 | 3 | Copyright (c) 2013 Mark Otto. 4 | 5 | Copyright (c) 2017 Andrew Fong. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of 8 | this software and associated documentation files (the "Software"), to deal in 9 | the Software without restriction, including without limitation the rights to 10 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 11 | the Software, and to permit persons to whom the Software is furnished to do so, 12 | subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 19 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 20 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 21 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 22 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Colorful Note Borders Plugin for Obsidian 2 | 3 | The Colorful Note Borders plugin for Obsidian is designed to help you visually distinguish your notes based on custom rules. By applying colored borders around your notes, you can easily recognize and categorize them based on their folder location or specific frontmatter metadata. 4 | 5 | This plugin supports two types of rules: 6 | 7 | 1. **Folder-based rules**: Apply a colorful border to notes based on their folder location. For example, you can configure a green border to be displayed around notes located in the "Inbox" folder. 8 | 2. **Frontmatter metadata-based rules**: Apply a colorful border to notes based on their frontmatter metadata. For example, you can configure a red border to be displayed around notes that have "private: true" property in the frontmatter metadata. 9 | 10 | By using the Colorful Note Borders plugin, you can create a more organized and visually appealing workspace in Obsidian. Customize your note appearance with an easy-to-configure settings page that allows you to define your color rules dynamically. 11 | 12 | ## Demo 13 | 14 | 15 | 16 | 17 | 18 | ## Features 19 | 20 | The Colorful Note Borders plugin for Obsidian offers the following features: 21 | 22 | - Apply colorful borders to notes based on customizable rules. 23 | - Supports two types of rules: 24 | - Folder location 25 | - Frontmatter metadata 26 | - Users can add, edit, and remove rules from the settings page. 27 | - Users can re-order the rules to prioritize which rule takes precedence when multiple rules match. The first rule takes precedence. 28 | - Compatible with Obsidian's light and dark modes. 29 | 30 | ## Installation 31 | 32 | To install the Colorful Borders plugin, follow these steps: 33 | 34 | 1. Open your Obsidian vault 35 | 2. Go to the Settings page (click the gear icon in the left sidebar) 36 | 3. Navigate to Third-party plugins and make sure the "Safe mode" toggle is off 37 | 4. Click "Browse" and search for "Colorful Note Borders" 38 | 5. Click "Install" on the Colorful Note Borders plugin 39 | 6. After the installation is complete, click "Enable" to activate the plugin 40 | 41 | ## Manual Installation using BRAT 42 | 43 | BRAT (Beta Reviewers Auto-update Tester) is a plugin for Obsidian that allows you to install and manage plugins that are not yet approved and included in the Obsidian Plugin Directory. You can use BRAT to install the Colorful Borders plugin manually. 44 | 45 | ### Prerequisites 46 | 47 | - Obsidian 0.9.7 or later 48 | 49 | ### Installation Steps 50 | 51 | 1. Open your Obsidian vault. 52 | 2. Go to the Settings page (click the gear icon in the left sidebar). 53 | 3. Navigate to Third-party plugins and make sure the "Safe mode" toggle is off. 54 | 4. Click "Browse" and search for "BRAT". 55 | 5. Click "Install" on the BRAT plugin. 56 | 6. After the installation is complete, click "Enable" to activate the BRAT plugin. 57 | 7. Navigate to Plugin Options and click on "BRAT". 58 | 8. In the "Plugin Repository URL" field, enter the GitHub repository URL for the Colorful Borders plugin (`https://github.com/rusi/obsidian-colorful-note-borders`). 59 | 9. Click "Add plugin". 60 | 10. Click "Update plugins" to download and install the Colorful Note Borders plugin. 61 | 11. Navigate to Third-party plugins in the Obsidian settings. 62 | 12. Find the Colorful Note Borders plugin in the "Installed plugins" list and click "Enable" to activate it. 63 | 64 | Now the Colorful Note Borders plugin should be installed and activated. Follow the usage instructions in the previous sections to configure the plugin. 65 | 66 | ## Usage 67 | 68 | To configure the Colorful Note Borders plugin, follow these steps: 69 | 70 | 1. Go to the Settings page in your Obsidian vault 71 | 2. Navigate to Plugin Options and click on "Colorful Note Borders" 72 | 3. In the settings page, you can add or remove rules by clicking the "Add new rule" button or the "Remove" button next to each rule 73 | 4. Configure each rule by providing: 74 | - A name for the rule 75 | - A value to match (e.g., folder name or frontmatter metadata value) 76 | - The rule type (either "Path" for folder location or "Frontmatter" for frontmatter metadata) 77 | - A color for the border (use the color picker or enter a color hex code) 78 | 5. Save your settings 79 | 80 | The plugin will automatically apply the colorful borders to your notes based on the rules you've configured. If a note matches a rule, the border will be displayed around the note's content. 81 | 82 | ## Support 83 | 84 | If you encounter any issues or have feature requests, please create an issue on the plugin's GitHub repository. 85 | 86 | ## License 87 | 88 | This plugin is licensed under the MIT License. For more information, see the LICENSE file in the plugin's GitHub repository. 89 | -------------------------------------------------------------------------------- /assets/ColorfulNoteBordersDemo800.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusi/obsidian-colorful-note-borders/d804202b1bb9ee634a9c7b5b3c1ced77adab46b9/assets/ColorfulNoteBordersDemo800.gif -------------------------------------------------------------------------------- /assets/ColorfulNoteBordersDemov0.3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusi/obsidian-colorful-note-borders/d804202b1bb9ee634a9c7b5b3c1ced77adab46b9/assets/ColorfulNoteBordersDemov0.3.gif -------------------------------------------------------------------------------- /assets/PopupWindow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rusi/obsidian-colorful-note-borders/d804202b1bb9ee634a9c7b5b3c1ced77adab46b9/assets/PopupWindow.png -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | 5 | const banner = 6 | `/* 7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 8 | if you want to view the source, please visit the github repository of this plugin 9 | */ 10 | `; 11 | 12 | const prod = (process.argv[2] === "production"); 13 | 14 | const context = await esbuild.context({ 15 | banner: { 16 | js: banner, 17 | }, 18 | entryPoints: ["src/main.ts"], 19 | bundle: true, 20 | external: [ 21 | "obsidian", 22 | "electron", 23 | "@codemirror/autocomplete", 24 | "@codemirror/collab", 25 | "@codemirror/commands", 26 | "@codemirror/language", 27 | "@codemirror/lint", 28 | "@codemirror/search", 29 | "@codemirror/state", 30 | "@codemirror/view", 31 | "@lezer/common", 32 | "@lezer/highlight", 33 | "@lezer/lr", 34 | ...builtins], 35 | format: "cjs", 36 | target: "es2018", 37 | logLevel: "info", 38 | sourcemap: prod ? false : "inline", 39 | treeShaking: true, 40 | outfile: "main.js", 41 | }); 42 | 43 | if (prod) { 44 | await context.rebuild(); 45 | process.exit(0); 46 | } else { 47 | await context.watch(); 48 | } 49 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | preset: 'ts-jest', 4 | // testEnvironment: 'node', 5 | // roots: ['/tests'], 6 | transform: { 7 | '^.+\\.ts$': 'ts-jest' 8 | }, 9 | moduleFileExtensions: ['js', 'ts', 'svelte'], 10 | collectCoverage: true, // Enable coverage collection 11 | coverageDirectory: 'coverage', // Directory where coverage reports will be stored 12 | collectCoverageFrom: [ 13 | 'src/**/*.{js,ts}', // Include all JavaScript and TypeScript files in src/ 14 | '!src/**/*.d.ts' // Exclude TypeScript declaration files 15 | ], 16 | coverageReporters: ['json', 'lcov', 'text', 'clover'], // Specify coverage reporters 17 | 18 | globalSetup: './tests/global-setup.js', 19 | }; 20 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "colorful-note-borders", 3 | "name": "Colorful Note Borders", 4 | "version": "0.2.4", 5 | "minAppVersion": "0.15.0", 6 | "description": "Add customizable colorful borders to notes based on folder location or frontmatter metadata, enhancing visual organization in Obsidian.", 7 | "author": "rusi", 8 | "isDesktopOnly": false 9 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "colorful-note-borders", 3 | "version": "0.2.4", 4 | "description": "Add customizable colorful borders to notes based on folder location or frontmatter metadata, enhancing visual organization in Obsidian.", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "lint": "eslint . --ext .ts", 10 | "test": "jest --ci", 11 | "test:dev": "jest --watch", 12 | "test:coverage": "jest --coverage", 13 | "version": "node version-bump.mjs && git add manifest.json versions.json", 14 | "release": "standard-version -t ''" 15 | }, 16 | "keywords": [ 17 | "obsidian", 18 | "note-colors" 19 | ], 20 | "author": "rusi", 21 | "license": "MIT", 22 | "devDependencies": { 23 | "@types/jest": "^29.5.11", 24 | "@types/node": "^16.11.6", 25 | "@typescript-eslint/eslint-plugin": "5.29.0", 26 | "@typescript-eslint/parser": "5.29.0", 27 | "builtin-modules": "3.3.0", 28 | "esbuild": "0.17.3", 29 | "eslint-config-prettier": "^9.1.0", 30 | "eslint-plugin-prettier": "^5.1.3", 31 | "jest": "^29.7.0", 32 | "jest-environment-jsdom": "^29.7.0", 33 | "obsidian": "latest", 34 | "prettier": "^3.2.2", 35 | "standard-version": "^9.1.1", 36 | "ts-jest": "^29.1.1", 37 | "tslib": "2.4.0", 38 | "typescript": "4.7.4" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | set -o pipefail 4 | npm run build 5 | npm version patch 6 | # npm run release 7 | git push origin master --tags 8 | version=$(cat manifest.json | jq -r ".version") 9 | gh release create ${version} -F CHANGELOG.md manifest.json main.js styles.css 10 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Plugin, TFile, MarkdownView, WorkspaceLeaf } from 'obsidian'; 2 | 3 | import { SettingsTab, ColorBorderSettings, DEFAULT_SETTINGS, ColorRule, RuleType } from './settingsTab'; 4 | 5 | export const checkPath = (currentPath: string, folder: string): boolean => { 6 | // return currentPath.includes(folder); 7 | const parts = currentPath.split(/[/\\]/); 8 | return parts.includes(folder); 9 | } 10 | 11 | export default class ColorfulNoteBordersPlugin extends Plugin { 12 | settings: ColorBorderSettings; 13 | 14 | async onload() { 15 | await this.loadSettings(); 16 | 17 | // This adds a settings tab so the user can configure various aspects of the plugin 18 | this.addSettingTab(new SettingsTab(this.app, this)); 19 | 20 | this.registerEvent( 21 | this.app.workspace.on("active-leaf-change", this.onActiveLeafChange.bind(this)) 22 | ); 23 | this.registerEvent( 24 | this.app.metadataCache.on("changed", this.onMetadataChange.bind(this)) 25 | ); 26 | this.registerEvent( 27 | this.app.vault.on("rename", this.onFileRename.bind(this)) 28 | ); 29 | } 30 | 31 | async onunload() { 32 | // cleanup all custom styles 33 | this.settings.colorRules.forEach((rule) => { 34 | this.removeStyle(rule); 35 | }); 36 | } 37 | 38 | async removeStyle(rule: ColorRule) { 39 | const style = this.makeStyleName(rule); 40 | const styleElement = document.getElementById(style); 41 | if (styleElement) { 42 | styleElement.remove(); 43 | } 44 | } 45 | 46 | async onActiveLeafChange(activeLeaf: WorkspaceLeaf) { 47 | // console.log("+ active leaf change: ", activeLeaf); 48 | this.applyRules(); 49 | } 50 | 51 | async onMetadataChange(file: TFile) { 52 | // console.log("+ metadata change"); 53 | this.applyRules(file); 54 | } 55 | 56 | async onFileRename(file: TFile) { 57 | // console.log("+ filename change"); 58 | this.applyRules(); 59 | } 60 | 61 | async loadSettings() { 62 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); 63 | this.updateStyles(); 64 | } 65 | 66 | async saveSettings() { 67 | await this.saveData(this.settings); 68 | this.updateStyles(); 69 | const activeFile = this.app.workspace.getActiveFile(); 70 | if (activeFile) { 71 | this.onFileRename(activeFile); 72 | } 73 | } 74 | 75 | async updateStyles() { 76 | this.settings.colorRules.forEach((rule) => this.updateStyle(rule)); 77 | } 78 | async updateStyle(rule: ColorRule) { 79 | const styleName = this.makeStyleName(rule); 80 | this.updateCustomCSS(styleName, ` 81 | .${styleName} { 82 | border: 5px solid ${rule.color} !important; 83 | } 84 | `); 85 | } 86 | 87 | addCustomCSS(cssstylename: string, css: string) { 88 | const styleElement = document.createElement('style'); 89 | styleElement.id = cssstylename; 90 | styleElement.innerText = css; 91 | document.head.appendChild(styleElement); 92 | } 93 | updateCustomCSS(cssstylename: string, css: string) { 94 | const styleElement = document.getElementById(cssstylename); 95 | if (styleElement) { 96 | styleElement.innerText = css; 97 | } else { 98 | this.addCustomCSS(cssstylename, css); 99 | } 100 | } 101 | 102 | async applyRules(file: TFile | null = null) { 103 | this.app.workspace.getLeavesOfType("markdown").forEach((value: WorkspaceLeaf) => { 104 | if (!(value.view instanceof MarkdownView)) return; 105 | const activeView = value.view as MarkdownView; 106 | const viewFile = activeView.file; 107 | if (file && file !== viewFile) return; 108 | const contentView = activeView.containerEl.querySelector(".view-content"); 109 | if (!contentView) return; 110 | 111 | this.unhighlightNote(contentView); 112 | this.settings.colorRules.some((rule) => { 113 | return this.applyRule(viewFile, rule, contentView); 114 | }); 115 | }); 116 | } 117 | 118 | applyRule(file: TFile, rule: ColorRule, contentView: Element): boolean { 119 | switch (rule.type) { 120 | case RuleType.Folder: { 121 | if (checkPath(file.path, rule.value)) { 122 | // console.log("- folder -", file); 123 | // console.log(file.path); 124 | // console.log(rule.value); 125 | this.highlightNote(contentView, rule); 126 | return true; 127 | } 128 | break; 129 | } 130 | case RuleType.Frontmatter: { 131 | // console.log("- front-matter -", file); 132 | // console.log(rule.value); 133 | const [key, value] = rule.value.split(":", 2); 134 | const frontMatterValue = this.app.metadataCache.getFileCache(file)?.frontmatter?.[key]; 135 | const normalizedFrontMatterValue = frontMatterValue?.toString().toLowerCase().trim(); 136 | const normalizedValueToHighlight = value?.toString().toLowerCase().trim(); 137 | // console.log(`++ front matter: ${key}, ${value} :: ${normalizedFrontMatterValue} === ${normalizedValueToHighlight}`); 138 | if (normalizedFrontMatterValue === normalizedValueToHighlight) { 139 | this.highlightNote(contentView, rule); 140 | return true; 141 | } 142 | break 143 | } 144 | } 145 | return false; 146 | } 147 | 148 | highlightNote(element: Element, rule: ColorRule) { 149 | element.classList.add(this.makeStyleName(rule)); 150 | } 151 | 152 | unhighlightNote(element: Element) { 153 | this.settings.colorRules.forEach((rule) => { 154 | element.classList.remove(this.makeStyleName(rule)); 155 | }); 156 | } 157 | 158 | makeStyleName(rule: ColorRule): string { 159 | return `cnb-${rule.id}-style`; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/settingsTab.ts: -------------------------------------------------------------------------------- 1 | import { App, PluginSettingTab, Setting, TextComponent, ButtonComponent, DropdownComponent, ColorComponent } from 'obsidian'; 2 | import ColorfulNoteBordersPlugin from './main'; 3 | 4 | export enum RuleType { 5 | Folder = "folder", 6 | Frontmatter = "frontmatter" 7 | } 8 | 9 | export interface ColorRule { 10 | id: string; 11 | value: string; 12 | type: RuleType; 13 | color: string; 14 | } 15 | 16 | export class ColorBorderSettings { 17 | colorRules: ColorRule[] = []; 18 | } 19 | 20 | export const DEFAULT_SETTINGS: ColorBorderSettings = { 21 | colorRules: [ 22 | { 23 | id: "inbox-ffb300", 24 | value: "Inbox", 25 | type: RuleType.Folder, 26 | color: "#ffb300" 27 | }, 28 | { 29 | id: "frontmatter-public-499749", 30 | value: "category: public", 31 | type: RuleType.Frontmatter, 32 | color: "#499749" 33 | }, 34 | { 35 | id: "frontmatter-private-c44545", 36 | value: "category: private", 37 | type: RuleType.Frontmatter, 38 | color: "#c44545" 39 | } 40 | ], 41 | }; 42 | 43 | export class SettingsTab extends PluginSettingTab { 44 | plugin: ColorfulNoteBordersPlugin; 45 | 46 | constructor(app: App, plugin: ColorfulNoteBordersPlugin) { 47 | super(app, plugin); 48 | this.plugin = plugin; 49 | } 50 | 51 | display(): void { 52 | let { containerEl } = this; 53 | containerEl.empty(); 54 | containerEl.createEl('h1', { text: 'Colorful Note Borders Settings' }); 55 | 56 | // Create a header row 57 | const headerRow = containerEl.createEl('div', { cls: 'cnb-rule-settings-header-row' }); 58 | 59 | // Add labels for each column 60 | headerRow.createEl('span', { text: 'Rule Type', cls: 'cnb-rule-settings-column-rule-type' }); 61 | headerRow.createEl('span', { text: 'Value', cls: 'cnb-rule-settings-column-rule-value' }); 62 | headerRow.createEl('span', { text: 'Color', cls: 'cnb-rule-settings-column-rule-color' }); 63 | headerRow.createEl('span', { text: '', cls: 'cnb-rule-settings-column-rule-button' }); 64 | 65 | const rulesContainer = containerEl.createEl('div', { cls: 'cnb-rules-container' }); 66 | 67 | // Display existing rules 68 | this.plugin.settings.colorRules.forEach((rule, index) => this.addRuleSetting(rulesContainer, rule, index)); 69 | 70 | // Add new rule button 71 | new ButtonComponent(containerEl) 72 | .setButtonText('Add new rule') 73 | .onClick(() => { 74 | const newRule: ColorRule = { 75 | id: Date.now().toString(), 76 | value: '', 77 | type: RuleType.Folder, 78 | color: '#000000', 79 | }; 80 | this.plugin.settings.colorRules.push(newRule); 81 | this.addRuleSetting(rulesContainer, newRule); 82 | this.plugin.saveSettings(); 83 | }); 84 | } 85 | 86 | addRuleSetting( 87 | containerEl: HTMLElement, 88 | rule: ColorRule, 89 | index: number = this.plugin.settings.colorRules.length - 1, 90 | ): void { 91 | const ruleSettingDiv = containerEl.createEl('div', { cls: 'cnb-rule-settings-row' }); 92 | 93 | new Setting(ruleSettingDiv) 94 | // .setName('Type') 95 | .setClass('cnb-rule-setting-item') 96 | .addDropdown((dropdown: DropdownComponent) => { 97 | dropdown.addOption(RuleType.Folder, 'Folder'); 98 | dropdown.addOption(RuleType.Frontmatter, 'Frontmatter'); 99 | dropdown.setValue(rule.type); 100 | dropdown.onChange((value) => { 101 | rule.type = value as RuleType; 102 | this.plugin.saveSettings(); 103 | }); 104 | dropdown.selectEl.classList.add('cnb-rule-type-dropdown'); 105 | }); 106 | 107 | new Setting(ruleSettingDiv) 108 | // .setName('Value') 109 | .setClass('cnb-rule-setting-item') 110 | .addText((text) => { 111 | text.setPlaceholder('Enter rule value'); 112 | text.setValue(rule.value); 113 | text.onChange((value) => { 114 | rule.value = value; 115 | this.plugin.saveSettings(); 116 | }); 117 | text.inputEl.classList.add('cnb-rule-value-input'); 118 | }); 119 | 120 | const colorSetting = new Setting(ruleSettingDiv) 121 | .setClass('cnb-rule-setting-item'); 122 | // .setName('Color'); 123 | // colorSetting.settingEl.style.gridColumn = '3'; 124 | 125 | const colorInput = new TextComponent(colorSetting.controlEl) 126 | .setPlaceholder('Enter color hex code') 127 | .setValue(rule.color); 128 | colorInput.inputEl.classList.add('cnb-rule-setting-item-text-input'); 129 | 130 | const picker = new ColorComponent(colorSetting.controlEl) 131 | .setValue(rule.color) 132 | .onChange((color) => { 133 | rule.color = color; 134 | colorInput.setValue(color); 135 | this.plugin.saveSettings(); 136 | }); 137 | 138 | colorInput.onChange((value: string) => { 139 | if (/^#(?:[0-9a-fA-F]{3}){1,2}$/.test(value)) { 140 | rule.color = value; 141 | picker.setValue(value); 142 | this.plugin.saveSettings(); 143 | } 144 | }); 145 | 146 | new ButtonComponent(ruleSettingDiv) 147 | .setButtonText('▲') 148 | // .setIcon("up-arrow") 149 | .setTooltip("Move Up") 150 | .setClass('cnb-rule-setting-item-up-button') 151 | .setDisabled(index == 0) 152 | .onClick(() => { 153 | if (index > 0) { 154 | this.plugin.settings.colorRules.splice(index, 1); 155 | this.plugin.settings.colorRules.splice(index - 1, 0, rule); 156 | this.plugin.saveSettings(); 157 | this.display(); 158 | } 159 | }); 160 | 161 | new ButtonComponent(ruleSettingDiv) 162 | .setButtonText('▼') 163 | // .setIcon("down-arrow") 164 | .setTooltip("Move Down") 165 | .setClass('cnb-rule-setting-item-down-button') 166 | .setDisabled(index == this.plugin.settings.colorRules.length - 1) 167 | .onClick(() => { 168 | if (index < this.plugin.settings.colorRules.length - 1) { 169 | this.plugin.settings.colorRules.splice(index, 1); 170 | this.plugin.settings.colorRules.splice(index + 1, 0, rule); 171 | this.plugin.saveSettings(); 172 | this.display(); 173 | } 174 | }); 175 | 176 | new ButtonComponent(ruleSettingDiv) 177 | .setButtonText('Remove') 178 | // .setIcon('remove') 179 | // .setTooltip("Remove") 180 | .setClass('cnb-rule-setting-item-remove-button') 181 | .setCta().onClick(() => { 182 | this.plugin.settings.colorRules = this.plugin.settings.colorRules.filter((r) => r.id !== rule.id); 183 | this.plugin.saveSettings(); 184 | this.plugin.removeStyle(rule); 185 | ruleSettingDiv.remove(); 186 | }); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | 2 | .cnb-rules-container { 3 | margin-bottom: 1em; 4 | border-top: 1px solid var(--background-modifier-border); 5 | } 6 | 7 | .cnb-rule-setting-item, 8 | .cnb-rule-setting-item:first-child { 9 | padding: 0.35em 0; 10 | justify-content: center; 11 | border-top: 1px solid var(--background-modifier-border); 12 | } 13 | 14 | .cnb-rule-setting-item .setting-item-info { 15 | display: none; 16 | } 17 | .cnb-rule-setting-item .setting-item-control { 18 | flex: none; 19 | } 20 | 21 | .cnb-rule-settings-header-row { 22 | display: grid; 23 | grid-template-columns: 2fr 2fr 2fr 0.5fr 0.5fr 0.5fr; 24 | /* gap: 10px; */ 25 | font-weight: bold; 26 | align-items: center; 27 | margin-bottom: 10px; 28 | } 29 | 30 | .cnb-rule-settings-row { 31 | display: grid; 32 | grid-template-columns: 2fr 2fr 2fr 0.5fr 0.5fr 0.5fr; 33 | /* gap: 10px; */ 34 | align-items: center; 35 | /* margin-bottom: 5px; */ 36 | } 37 | 38 | .cnb-rule-type-dropdown { 39 | width: 10em; 40 | } 41 | .cnb-rule-value-input { 42 | width: 10em; 43 | } 44 | .cnb-rule-setting-item-text-input { 45 | width: 6em; 46 | } 47 | 48 | /* 49 | button.cnd-rule-setting-item-up-button, 50 | button.cnd-rule-setting-item-down-button, 51 | button.cnd-rule-setting-item-delete-button 52 | { 53 | padding: 0; 54 | } */ 55 | 56 | /* .cnb-rule-settings-column-rule-type { 57 | width: 10em; 58 | } 59 | .cnb-rule-settings-column-rule-value { 60 | width: 10em; 61 | } 62 | .cnb-rule-settings-column-rule-color { 63 | width: 6em; 64 | } 65 | .cnb-rule-settings-column-rule-button { 66 | width: 6em; 67 | } */ 68 | -------------------------------------------------------------------------------- /tests/__mocks__/obsidian.ts: -------------------------------------------------------------------------------- 1 | export { }; 2 | -------------------------------------------------------------------------------- /tests/global-setup.js: -------------------------------------------------------------------------------- 1 | module.exports = async () => { 2 | process.env.TZ = 'UTC'; 3 | }; 4 | -------------------------------------------------------------------------------- /tests/main.test.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @jest-environment jsdom 4 | */ 5 | import { describe, expect, test } from '@jest/globals'; 6 | import { checkPath } from '../src/main'; 7 | 8 | 9 | jest.mock('obsidian', () => { 10 | class MockPlugin { 11 | app: any; 12 | constructor(app: any) { 13 | this.app = app; 14 | } 15 | // Add any mock methods or properties as needed 16 | } 17 | 18 | return { 19 | Plugin: MockPlugin, 20 | PluginSettingTab: class { 21 | constructor(app: any, plugin: any) { 22 | // Mock properties and methods as needed 23 | } 24 | }, 25 | // ... other Obsidian exports 26 | }; 27 | }); 28 | 29 | 30 | describe('utility functions', () => { 31 | it('ensure checkPath matches full folders', () => { 32 | expect(checkPath("Other/Two Rules.md", "Other")).toBe(true); 33 | }); 34 | it('should not match filenames', () => { 35 | expect(checkPath("Obsidian/readme.md", "Obsidian")).toBe(true); 36 | // Issue #4 - https://github.com/rusi/obsidian-colorful-note-borders/issues/4 37 | expect(checkPath("Index/300-Obsidian-index.md", "Obsidian")).toBe(false); 38 | }); 39 | it('should match Windows style paths', () => { 40 | expect(checkPath("Obsidian\\readme.md", "Obsidian")).toBe(true); 41 | }); 42 | it('should match paths with spaces', () => { 43 | expect(checkPath("Inbox/Test Note/test note with spaces.md", "Test Note")).toBe(true); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES6", 8 | // "module": "CommonJS", 9 | // "target": "ES2019", 10 | // "strict": true, 11 | // "allowJs": true, 12 | // "skipLibCheck": true, 13 | // "forceConsistentCasingInFileNames": true, 14 | "esModuleInterop": true, 15 | "noImplicitAny": true, 16 | "moduleResolution": "node", 17 | "importHelpers": true, 18 | "isolatedModules": true, 19 | "strictNullChecks": true, 20 | // "outDir": "./", 21 | "lib": [ 22 | "DOM", 23 | "ES5", 24 | "ES6", 25 | "ES7" 26 | ] 27 | }, 28 | "include": [ 29 | "**/*.ts" 30 | ], 31 | "exclude": ["node_modules"] 32 | } 33 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 15 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "0.1.1": "0.15.0", 3 | "0.2.0": "0.15.0", 4 | "0.2.1": "0.15.0", 5 | "0.2.2": "0.15.0", 6 | "0.2.3": "0.15.0", 7 | "0.2.4": "0.15.0" 8 | } --------------------------------------------------------------------------------