feat: add hex color support

Support hex colors (#RGB, #RRGGBB) in addition to RGB values and CSS variables.
Color picker in modals now preserves hex format instead of converting to RGB.

Closes #2

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@gapmiss 2026-05-30 17:20:54 -05:00
parent 663e6b83f0
commit cf52f94f29
4 changed files with 64 additions and 41 deletions

View file

@ -94,11 +94,11 @@ A plugin for displaying inline "callouts" in [Obsidian.md](https://github.com/ob
`[!!ICON|LABEL|COLOR]`
```
| Syntax | Details |
| ----------------- | ------------------------------ |
| `ICON` | Name of the Lucide icon |
| `LABEL`(optional) | Callout label/title text |
| `COLOR`(optional) | RGB values or Obsidian CSS var |
| Syntax | Details |
| ----------------- | ---------------------------------------- |
| `ICON` | Name of the Lucide icon |
| `LABEL`(optional) | Callout label/title text |
| `COLOR`(optional) | Hex, RGB values, or Obsidian CSS var |
> [!IMPORTANT]

View file

@ -3,6 +3,39 @@ import {
setIcon
} from 'obsidian';
function parseColorToRgb(color: string): string | null {
const trimmed = color.trim();
// Hex color (3, 4, 6, or 8 digit)
if (trimmed.startsWith('#')) {
let hex = trimmed.slice(1);
// Expand 3-digit hex to 6-digit
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
// Expand 4-digit hex (with alpha) to 8-digit
if (hex.length === 4) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3];
}
// Parse 6-digit or 8-digit hex
if (hex.length === 6 || hex.length === 8) {
const r = parseInt(hex.slice(0, 2), 16);
const g = parseInt(hex.slice(2, 4), 16);
const b = parseInt(hex.slice(4, 6), 16);
if (!isNaN(r) && !isNaN(g) && !isNaN(b)) {
return `${r}, ${g}, ${b}`;
}
}
return null;
}
// Already RGB format (e.g., "255, 0, 0")
return trimmed;
}
export class InlineCallout {
newEl: HTMLSpanElement;
@ -60,8 +93,11 @@ export class InlineCallout {
// icon and color only
if (!calloutLabel && calloutColor) {
calloutColorStyle = "color: rgba(" + calloutColor + ", 1);"
this.iconEl.setAttr("style", calloutColorStyle);
const rgb = parseColorToRgb(calloutColor);
if (rgb) {
calloutColorStyle = "color: rgba(" + rgb + ", 1);"
this.iconEl.setAttr("style", calloutColorStyle);
}
this.iconEl.setAttr("aria-label", calloutIcon);
setIcon(this.iconEl, calloutIcon);
this.newEl.appendChild(this.iconEl);
@ -79,8 +115,11 @@ export class InlineCallout {
}
// color?
if (calloutLabel && calloutColor) {
calloutColorStyle = "color: rgba(" + calloutColor + ", 1); background-color: rgba(" + calloutColor + ", var(--inline-callout-bg-transparency));"
this.newEl.setAttr("style", calloutColorStyle)
const rgb = parseColorToRgb(calloutColor);
if (rgb) {
calloutColorStyle = "color: rgba(" + rgb + ", 1); background-color: rgba(" + rgb + ", var(--inline-callout-bg-transparency));"
this.newEl.setAttr("style", calloutColorStyle)
}
}
return this.newEl;
}

View file

@ -111,18 +111,18 @@ export class ModifyInlineCalloutModal extends Modal {
});
});
const hexToRgb = (hex: string) => {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `${r}, ${g}, ${b}`;
};
function rgbToHex(r: number, g: number, b: number) {
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b)
.toString(16)
.slice(1)
.toUpperCase();
function colorToHex(color: string | undefined): string {
if (!color) return '#000000';
if (color.startsWith('#')) return color;
// Convert RGB string to hex
const parts = color.split(',').map(s => parseInt(s.trim(), 10));
if (parts.length >= 3 && parts.every(n => !isNaN(n))) {
return '#' + ((1 << 24) + (parts[0] << 16) + (parts[1] << 8) + parts[2])
.toString(16)
.slice(1)
.toUpperCase();
}
return '#000000';
}
new Setting(contentEl)
@ -155,18 +155,9 @@ export class ModifyInlineCalloutModal extends Modal {
})
})
.addColorPicker((cb) => {
let r = 0;
let g = 0;
let b = 0;
if (this.calloutColor) {
const colorArr = this.calloutColor.split(",");
r = Number(colorArr[0] ?? 0);
g = Number(colorArr[1] ?? 0);
b = Number(colorArr[2] ?? 0);
}
cb.setValue(rgbToHex(r, g, b))
cb.setValue(colorToHex(this.calloutColor))
.onChange((value) => {
this.calloutColor = hexToRgb(value);
this.calloutColor = value;
const dropdown = this.activeDoc.querySelector<HTMLSelectElement>(".modify-inline-callout-modal .inline-callouts-color-dropdown .dropdown");
if (dropdown) dropdown.value = '';
this.buildPreview();

View file

@ -82,13 +82,6 @@ export class NewInlineCalloutModal extends Modal {
});
});
const hexToRgb = (hex: string) => {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `${r}, ${g}, ${b}`;
};
new Setting(contentEl)
.setName('Color')
.setDesc('Pick a color using the dropdown or color picker. Default: Obsidian\'s base color')
@ -119,9 +112,9 @@ export class NewInlineCalloutModal extends Modal {
})
})
.addColorPicker((cb) => {
cb.setValue(this.calloutColor ?? '#000000')
cb.setValue(this.calloutColor?.startsWith('#') ? this.calloutColor : '#000000')
.onChange((value) => {
this.calloutColor = hexToRgb(value);
this.calloutColor = value;
const dropdown = this.activeDoc.querySelector<HTMLSelectElement>(".new-inline-callout-modal .inline-callouts-color-dropdown .dropdown");
if (dropdown) dropdown.value = '';
this.buildPreview();