Merge branch 'feature/color-scheme-picker-assist' into develop

This commit is contained in:
nejimakibird 2026-07-04 10:44:17 +09:00
commit e85556bed9
7 changed files with 736 additions and 26 deletions

238
main.js
View file

@ -20122,6 +20122,15 @@ var EN_MESSAGES = {
"colorScheme.preview.swatch": "Color preview",
"colorScheme.preview.targets": "Targets",
"colorScheme.preview.empty": "No colors defined.",
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
"colorScheme.preview.blankColor": "(blank)",
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
"colorScheme.preview.unsupportedColor": "Unsupported color value; picking a color will replace it with #RRGGBB.",
"colorScheme.preview.pick": "Pick",
"colorScheme.preview.pickAria": "Pick {column} color",
"colorScheme.preview.pickApplied": "Color updated.",
"colorScheme.preview.pickFailed": "Could not update the color cell.",
"colorScheme.preview.pickUnchanged": "Color is already set.",
"colorScheme.field.target": "Target",
"colorScheme.field.kind": "Kind",
"colorScheme.field.fill": "Fill",
@ -20514,6 +20523,13 @@ var JA_MESSAGES = {
"colorScheme.preview.swatch": "\u30AB\u30E9\u30FC\u78BA\u8A8D",
"colorScheme.preview.targets": "\u5BFE\u8C61",
"colorScheme.preview.empty": "Color \u306F\u5B9A\u7FA9\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
"colorScheme.preview.blankColor": "\uFF08\u7A7A\uFF09",
"colorScheme.preview.unsupportedColor": "\u672A\u5BFE\u5FDC\u306E\u8272\u5024\u3067\u3059\u3002\u8272\u3092\u9078\u3076\u3068 #RRGGBB \u3067\u7F6E\u304D\u63DB\u3048\u307E\u3059\u3002",
"colorScheme.preview.pick": "\u9078\u629E",
"colorScheme.preview.pickAria": "{column} \u306E\u8272\u3092\u9078\u629E",
"colorScheme.preview.pickApplied": "\u8272\u3092\u66F4\u65B0\u3057\u307E\u3057\u305F\u3002",
"colorScheme.preview.pickFailed": "\u8272\u30BB\u30EB\u3092\u66F4\u65B0\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002",
"colorScheme.preview.pickUnchanged": "\u8272\u306F\u3059\u3067\u306B\u8A2D\u5B9A\u3055\u308C\u3066\u3044\u307E\u3059\u3002",
"colorScheme.field.target": "\u5BFE\u8C61",
"colorScheme.field.kind": "kind",
"colorScheme.field.fill": "\u5857\u308A",
@ -21260,6 +21276,132 @@ function escapeDomainMindmapLabel(value) {
return value.replace(/\r\n?/g, "\n").replace(/\n/g, " ").replace(/\(/g, "\uFF08").replace(/\)/g, "\uFF09").replace(/\s+/g, " ").trim();
}
// src/core/color-scheme-table-editor.ts
var COLORS_SECTION_NAME = "Colors";
var COLOR_HEADERS2 = ["target", "kind", "fill", "stroke", "text", "notes"];
var HEX_RGB_PATTERN = /^#([0-9a-fA-F]{3})$/;
var HEX_RRGGBB_PATTERN = /^#[0-9a-fA-F]{6}$/;
function updateColorSchemeColorCell(markdown, request) {
const normalizedColor = normalizeHexColorForPicker(request.value);
if (!normalizedColor || !HEX_RRGGBB_PATTERN.test(normalizedColor)) {
return unchanged2(markdown, "invalid-color");
}
const columnIndex = COLOR_HEADERS2.indexOf(request.columnName);
if (columnIndex < 0) {
return unchanged2(markdown, "column-missing");
}
const lineEnding = markdown.includes("\r\n") ? "\r\n" : "\n";
const lines = markdown.split(/\r?\n/);
const sectionRange = findMarkdownSectionRange(lines, COLORS_SECTION_NAME);
if (!sectionRange) {
return unchanged2(markdown, "section-missing");
}
const tableStart = findTableStart(lines, sectionRange.start + 1, sectionRange.end);
if (tableStart === null || tableStart + 1 >= sectionRange.end) {
return unchanged2(markdown, "table-missing");
}
const headers = splitMarkdownTableRow(lines[tableStart]) ?? [];
if (!sameHeaders7(headers, [...COLOR_HEADERS2])) {
return unchanged2(markdown, "header-mismatch");
}
const separatorCells = splitMarkdownTableRow(lines[tableStart + 1]);
if (!separatorCells || separatorCells.length !== COLOR_HEADERS2.length) {
return unchanged2(markdown, "table-missing");
}
const targetLineIndex = findDataRowLineIndex(lines, tableStart + 2, sectionRange.end, request.rowIndex);
if (targetLineIndex === null) {
return unchanged2(markdown, "row-missing");
}
const ranges = getMarkdownTableCellRanges(lines[targetLineIndex]);
if (!ranges || !ranges[columnIndex]) {
return unchanged2(markdown, "column-missing");
}
const range = ranges[columnIndex];
const line = lines[targetLineIndex];
const rawCell = line.slice(range.rawStart, range.rawEnd);
const updatedLine = rawCell.trim().length === 0 ? `${line.slice(0, range.rawStart)} ${normalizedColor} ${line.slice(range.rawEnd)}` : `${line.slice(0, range.contentStart)}${normalizedColor}${line.slice(range.contentEnd)}`;
if (updatedLine === line) {
return {
changed: false,
updatedMarkdown: markdown,
status: "unchanged"
};
}
const updatedLines = [...lines];
updatedLines[targetLineIndex] = updatedLine;
return {
changed: true,
updatedMarkdown: updatedLines.join(lineEnding),
status: "updated"
};
}
function normalizeHexColorForPicker(value) {
const trimmed = value?.trim();
if (!trimmed) {
return null;
}
const rgb = trimmed.match(HEX_RGB_PATTERN);
if (rgb) {
return `#${rgb[1].split("").map((char) => `${char}${char}`).join("")}`.toLowerCase();
}
return HEX_RRGGBB_PATTERN.test(trimmed) ? trimmed.toLowerCase() : null;
}
function findMarkdownSectionRange(lines, sectionName) {
const headingPattern = /^##\s+(.+?)\s*$/;
let start = null;
for (let index = 0; index < lines.length; index += 1) {
const match = lines[index].match(headingPattern);
if (!match) {
continue;
}
if (start !== null) {
return { start, end: index };
}
if (normalizeHeadingName(match[1]) === normalizeHeadingName(sectionName)) {
start = index;
}
}
return start === null ? null : { start, end: lines.length };
}
function findTableStart(lines, start, end) {
for (let index = start; index < end; index += 1) {
if (lines[index].trim().startsWith("|")) {
return index;
}
}
return null;
}
function findDataRowLineIndex(lines, start, end, targetRowIndex) {
let currentDataRowIndex = 0;
for (let index = start; index < end; index += 1) {
if (!lines[index].trim().startsWith("|")) {
continue;
}
const values = splitMarkdownTableRow(lines[index]);
if (!values || isEmptyMarkdownTableDataRow(values)) {
continue;
}
if (currentDataRowIndex === targetRowIndex) {
return index;
}
currentDataRowIndex += 1;
}
return null;
}
function sameHeaders7(actual, expected) {
return actual.length === expected.length && actual.every((header, index) => header === expected[index]);
}
function normalizeHeadingName(value) {
return value.trim().replace(/\s+#+$/, "").trim().toLowerCase();
}
function unchanged2(markdown, status) {
return {
changed: false,
updatedMarkdown: markdown,
status
};
}
// src/views/usage-view-renderer.ts
function renderUsageViewSections(container, sections, options) {
for (const section of sections) {
@ -22834,9 +22976,12 @@ var ModelingPreviewView = class extends import_obsidian7.ItemView {
this.t("colorScheme.preview.colors"),
true
);
this.renderColorSchemeTable(section, state.model.colors);
const canPickColors = !state.warnings.some(
(warning) => warning.code === "invalid-table-column" && (warning.field === "Colors" || warning.context?.section === "Colors" || /section "Colors"/.test(warning.message))
);
this.renderColorSchemeTable(section, state.model.colors, canPickColors);
}
renderColorSchemeTable(container, colors) {
renderColorSchemeTable(container, colors, canPickColors) {
const tableWrap = container.createDiv({ cls: "model-weave-table-wrap" });
const table = tableWrap.createEl("table", {
cls: "model-weave-summary-table model-weave-data-table"
@ -22867,7 +23012,7 @@ var ModelingPreviewView = class extends import_obsidian7.ItemView {
return;
}
for (const color of colors) {
this.renderColorSchemeTableRow(tbody, color);
this.renderColorSchemeTableRow(tbody, color, canPickColors);
}
}
renderAppliedColorScheme(container, colorScheme, targets) {
@ -22892,17 +23037,13 @@ var ModelingPreviewView = class extends import_obsidian7.ItemView {
this.t
);
}
renderColorSchemeTableRow(tbody, color) {
renderColorSchemeTableRow(tbody, color, canPickColors) {
const row = tbody.createEl("tr");
for (const value of [
color.target ?? this.t("domains.value.none"),
color.kind,
color.fill ?? this.t("domains.value.none"),
color.stroke ?? this.t("domains.value.none"),
color.text ?? this.t("domains.value.none")
]) {
row.createEl("td", { text: value });
}
row.createEl("td", { text: color.target ?? this.t("domains.value.none") });
row.createEl("td", { text: color.kind });
this.renderColorSchemeColorCell(row, color, "fill", canPickColors);
this.renderColorSchemeColorCell(row, color, "stroke", canPickColors);
this.renderColorSchemeColorCell(row, color, "text", canPickColors);
const swatchCell = row.createEl("td");
const swatch = swatchCell.createSpan({
cls: "model-weave-color-swatch"
@ -22919,6 +23060,77 @@ var ModelingPreviewView = class extends import_obsidian7.ItemView {
swatch.textContent = "Aa";
row.createEl("td", { text: color.notes ?? "" });
}
renderColorSchemeColorCell(row, color, columnName, canPickColors) {
const value = color[columnName] ?? "";
const normalized = normalizeHexColorForPicker(value);
const cell = row.createEl("td");
const control = cell.createDiv({ cls: "model-weave-color-cell-control" });
const swatch = control.createSpan({
cls: normalized ? "model-weave-color-cell-swatch" : "model-weave-color-cell-swatch model-weave-color-cell-swatch-empty",
attr: { "aria-hidden": "true" }
});
if (normalized) {
swatch.style.backgroundColor = normalized;
}
const text = control.createSpan({
text: value || this.t("colorScheme.preview.blankColor"),
cls: value ? "model-weave-color-cell-value" : "model-weave-color-cell-value model-weave-summary-empty-cell"
});
if (normalized) {
text.title = normalized;
} else if (value) {
text.title = this.t("colorScheme.preview.unsupportedColor");
}
if (!canPickColors) {
return;
}
const input = control.createEl("input", {
cls: "model-weave-color-picker-input",
attr: {
type: "color",
value: normalized ?? "#ffffff",
"aria-label": this.t("colorScheme.preview.pickAria", {
column: this.t(`colorScheme.field.${columnName}`)
})
}
});
const button = control.createEl("button", {
text: this.t("colorScheme.preview.pick"),
cls: "model-weave-color-picker-button",
attr: { type: "button" }
});
button.addEventListener("click", () => {
input.click();
});
input.addEventListener("change", () => {
void this.applyColorSchemeColorPick(color.rowIndex, columnName, input.value);
});
}
async applyColorSchemeColorPick(rowIndex, columnName, value) {
try {
const file = this.getCurrentTFileForQuickFix();
if (!file) {
new import_obsidian7.Notice(this.t("colorScheme.preview.pickFailed"));
return;
}
const markdown = await this.app.vault.read(file);
const result = updateColorSchemeColorCell(markdown, {
rowIndex,
columnName,
value
});
if (!result.changed) {
const noticeKey = result.status === "unchanged" ? "colorScheme.preview.pickUnchanged" : "colorScheme.preview.pickFailed";
new import_obsidian7.Notice(this.t(noticeKey));
return;
}
await this.app.vault.modify(file, result.updatedMarkdown);
new import_obsidian7.Notice(this.t("colorScheme.preview.pickApplied"));
this.renderCurrentState();
} catch {
new import_obsidian7.Notice(this.t("colorScheme.preview.pickFailed"));
}
}
renderDomainTable(container, domains) {
const section = this.createCollapsibleSection(
container,

View file

@ -0,0 +1,188 @@
import {
getMarkdownTableCellRanges,
isEmptyMarkdownTableDataRow,
splitMarkdownTableRow
} from "../parsers/markdown-table";
const COLORS_SECTION_NAME = "Colors";
const COLOR_HEADERS = ["target", "kind", "fill", "stroke", "text", "notes"] as const;
const HEX_RGB_PATTERN = /^#([0-9a-fA-F]{3})$/;
const HEX_RRGGBB_PATTERN = /^#[0-9a-fA-F]{6}$/;
export type ColorSchemeEditableColumn = "fill" | "stroke" | "text";
export interface UpdateColorSchemeCellRequest {
rowIndex: number;
columnName: ColorSchemeEditableColumn;
value: string;
}
export interface UpdateColorSchemeCellResult {
changed: boolean;
updatedMarkdown: string;
status: "updated" | "unchanged" | "invalid-color" | "section-missing" | "table-missing" | "header-mismatch" | "row-missing" | "column-missing";
}
export function updateColorSchemeColorCell(
markdown: string,
request: UpdateColorSchemeCellRequest
): UpdateColorSchemeCellResult {
const normalizedColor = normalizeHexColorForPicker(request.value);
if (!normalizedColor || !HEX_RRGGBB_PATTERN.test(normalizedColor)) {
return unchanged(markdown, "invalid-color");
}
const columnIndex = COLOR_HEADERS.indexOf(request.columnName);
if (columnIndex < 0) {
return unchanged(markdown, "column-missing");
}
const lineEnding = markdown.includes("\r\n") ? "\r\n" : "\n";
const lines = markdown.split(/\r?\n/);
const sectionRange = findMarkdownSectionRange(lines, COLORS_SECTION_NAME);
if (!sectionRange) {
return unchanged(markdown, "section-missing");
}
const tableStart = findTableStart(lines, sectionRange.start + 1, sectionRange.end);
if (tableStart === null || tableStart + 1 >= sectionRange.end) {
return unchanged(markdown, "table-missing");
}
const headers = splitMarkdownTableRow(lines[tableStart]) ?? [];
if (!sameHeaders(headers, [...COLOR_HEADERS])) {
return unchanged(markdown, "header-mismatch");
}
const separatorCells = splitMarkdownTableRow(lines[tableStart + 1]);
if (!separatorCells || separatorCells.length !== COLOR_HEADERS.length) {
return unchanged(markdown, "table-missing");
}
const targetLineIndex = findDataRowLineIndex(lines, tableStart + 2, sectionRange.end, request.rowIndex);
if (targetLineIndex === null) {
return unchanged(markdown, "row-missing");
}
const ranges = getMarkdownTableCellRanges(lines[targetLineIndex]);
if (!ranges || !ranges[columnIndex]) {
return unchanged(markdown, "column-missing");
}
const range = ranges[columnIndex];
const line = lines[targetLineIndex];
const rawCell = line.slice(range.rawStart, range.rawEnd);
const updatedLine = rawCell.trim().length === 0
? `${line.slice(0, range.rawStart)} ${normalizedColor} ${line.slice(range.rawEnd)}`
: `${line.slice(0, range.contentStart)}${normalizedColor}${line.slice(range.contentEnd)}`;
if (updatedLine === line) {
return {
changed: false,
updatedMarkdown: markdown,
status: "unchanged"
};
}
const updatedLines = [...lines];
updatedLines[targetLineIndex] = updatedLine;
return {
changed: true,
updatedMarkdown: updatedLines.join(lineEnding),
status: "updated"
};
}
export function normalizeHexColorForPicker(value: string | null | undefined): string | null {
const trimmed = value?.trim();
if (!trimmed) {
return null;
}
const rgb = trimmed.match(HEX_RGB_PATTERN);
if (rgb) {
return `#${rgb[1].split("").map((char) => `${char}${char}`).join("")}`.toLowerCase();
}
return HEX_RRGGBB_PATTERN.test(trimmed)
? trimmed.toLowerCase()
: null;
}
function findMarkdownSectionRange(
lines: string[],
sectionName: string
): { start: number; end: number } | null {
const headingPattern = /^##\s+(.+?)\s*$/;
let start: number | null = null;
for (let index = 0; index < lines.length; index += 1) {
const match = lines[index].match(headingPattern);
if (!match) {
continue;
}
if (start !== null) {
return { start, end: index };
}
if (normalizeHeadingName(match[1]) === normalizeHeadingName(sectionName)) {
start = index;
}
}
return start === null ? null : { start, end: lines.length };
}
function findTableStart(lines: string[], start: number, end: number): number | null {
for (let index = start; index < end; index += 1) {
if (lines[index].trim().startsWith("|")) {
return index;
}
}
return null;
}
function findDataRowLineIndex(
lines: string[],
start: number,
end: number,
targetRowIndex: number
): number | null {
let currentDataRowIndex = 0;
for (let index = start; index < end; index += 1) {
if (!lines[index].trim().startsWith("|")) {
continue;
}
const values = splitMarkdownTableRow(lines[index]);
if (!values || isEmptyMarkdownTableDataRow(values)) {
continue;
}
if (currentDataRowIndex === targetRowIndex) {
return index;
}
currentDataRowIndex += 1;
}
return null;
}
function sameHeaders(actual: string[], expected: string[]): boolean {
return actual.length === expected.length &&
actual.every((header, index) => header === expected[index]);
}
function normalizeHeadingName(value: string): string {
return value.trim().replace(/\s+#+$/, "").trim().toLowerCase();
}
function unchanged(
markdown: string,
status: Exclude<UpdateColorSchemeCellResult["status"], "updated" | "unchanged">
): UpdateColorSchemeCellResult {
return {
changed: false,
updatedMarkdown: markdown,
status
};
}

View file

@ -368,6 +368,15 @@ export const EN_MESSAGES: ModelWeaveMessageDictionary = {
"colorScheme.preview.swatch": "Color preview",
"colorScheme.preview.targets": "Targets",
"colorScheme.preview.empty": "No colors defined.",
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
"colorScheme.preview.blankColor": "(blank)",
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
"colorScheme.preview.unsupportedColor": "Unsupported color value; picking a color will replace it with #RRGGBB.",
"colorScheme.preview.pick": "Pick",
"colorScheme.preview.pickAria": "Pick {column} color",
"colorScheme.preview.pickApplied": "Color updated.",
"colorScheme.preview.pickFailed": "Could not update the color cell.",
"colorScheme.preview.pickUnchanged": "Color is already set.",
"colorScheme.field.target": "Target",
"colorScheme.field.kind": "Kind",
"colorScheme.field.fill": "Fill",

View file

@ -325,6 +325,13 @@ export const JA_MESSAGES: ModelWeaveMessageDictionary = {
"colorScheme.preview.swatch": "カラー確認",
"colorScheme.preview.targets": "対象",
"colorScheme.preview.empty": "Color は定義されていません。",
"colorScheme.preview.blankColor": "(空)",
"colorScheme.preview.unsupportedColor": "未対応の色値です。色を選ぶと #RRGGBB で置き換えます。",
"colorScheme.preview.pick": "選択",
"colorScheme.preview.pickAria": "{column} の色を選択",
"colorScheme.preview.pickApplied": "色を更新しました。",
"colorScheme.preview.pickFailed": "色セルを更新できませんでした。",
"colorScheme.preview.pickUnchanged": "色はすでに設定されています。",
"colorScheme.field.target": "対象",
"colorScheme.field.kind": "kind",
"colorScheme.field.fill": "塗り",

View file

@ -69,6 +69,11 @@ import {
getExpectedHeaderForDiagnostic as getSchemaExpectedHeaderForDiagnostic,
resolveDiagnosticSectionGuidance
} from "../core/diagnostic-section-guidance";
import {
normalizeHexColorForPicker,
updateColorSchemeColorCell,
type ColorSchemeEditableColumn
} from "../core/color-scheme-table-editor";
import type { ModelWeaveViewerPreferences } from "../settings/model-weave-settings";
import type { ModelingVaultIndex } from "../core/vault-index";
import type {
@ -1974,12 +1979,21 @@ export class ModelingPreviewView extends ItemView {
this.t("colorScheme.preview.colors"),
true
);
this.renderColorSchemeTable(section, state.model.colors);
const canPickColors = !state.warnings.some((warning) =>
warning.code === "invalid-table-column" &&
(
warning.field === "Colors" ||
warning.context?.section === "Colors" ||
/section "Colors"/.test(warning.message)
)
);
this.renderColorSchemeTable(section, state.model.colors, canPickColors);
}
private renderColorSchemeTable(
container: HTMLElement,
colors: ColorSchemeEntry[]
colors: ColorSchemeEntry[],
canPickColors: boolean
): void {
const tableWrap = container.createDiv({ cls: "model-weave-table-wrap" });
const table = tableWrap.createEl("table", {
@ -2013,7 +2027,7 @@ export class ModelingPreviewView extends ItemView {
}
for (const color of colors) {
this.renderColorSchemeTableRow(tbody, color);
this.renderColorSchemeTableRow(tbody, color, canPickColors);
}
}
@ -2051,18 +2065,15 @@ export class ModelingPreviewView extends ItemView {
private renderColorSchemeTableRow(
tbody: HTMLElement,
color: ColorSchemeEntry
color: ColorSchemeEntry,
canPickColors: boolean
): void {
const row = tbody.createEl("tr");
for (const value of [
color.target ?? this.t("domains.value.none"),
color.kind,
color.fill ?? this.t("domains.value.none"),
color.stroke ?? this.t("domains.value.none"),
color.text ?? this.t("domains.value.none")
]) {
row.createEl("td", { text: value });
}
row.createEl("td", { text: color.target ?? this.t("domains.value.none") });
row.createEl("td", { text: color.kind });
this.renderColorSchemeColorCell(row, color, "fill", canPickColors);
this.renderColorSchemeColorCell(row, color, "stroke", canPickColors);
this.renderColorSchemeColorCell(row, color, "text", canPickColors);
const swatchCell = row.createEl("td");
const swatch = swatchCell.createSpan({
@ -2082,6 +2093,98 @@ export class ModelingPreviewView extends ItemView {
row.createEl("td", { text: color.notes ?? "" });
}
private renderColorSchemeColorCell(
row: HTMLElement,
color: ColorSchemeEntry,
columnName: ColorSchemeEditableColumn,
canPickColors: boolean
): void {
const value = color[columnName] ?? "";
const normalized = normalizeHexColorForPicker(value);
const cell = row.createEl("td");
const control = cell.createDiv({ cls: "model-weave-color-cell-control" });
const swatch = control.createSpan({
cls: normalized
? "model-weave-color-cell-swatch"
: "model-weave-color-cell-swatch model-weave-color-cell-swatch-empty",
attr: { "aria-hidden": "true" }
});
if (normalized) {
swatch.style.backgroundColor = normalized;
}
const text = control.createSpan({
text: value || this.t("colorScheme.preview.blankColor"),
cls: value
? "model-weave-color-cell-value"
: "model-weave-color-cell-value model-weave-summary-empty-cell"
});
if (normalized) {
text.title = normalized;
} else if (value) {
text.title = this.t("colorScheme.preview.unsupportedColor");
}
if (!canPickColors) {
return;
}
const input = control.createEl("input", {
cls: "model-weave-color-picker-input",
attr: {
type: "color",
value: normalized ?? "#ffffff",
"aria-label": this.t("colorScheme.preview.pickAria", {
column: this.t(`colorScheme.field.${columnName}`)
})
}
});
const button = control.createEl("button", {
text: this.t("colorScheme.preview.pick"),
cls: "model-weave-color-picker-button",
attr: { type: "button" }
});
button.addEventListener("click", () => {
input.click();
});
input.addEventListener("change", () => {
void this.applyColorSchemeColorPick(color.rowIndex, columnName, input.value);
});
}
private async applyColorSchemeColorPick(
rowIndex: number,
columnName: ColorSchemeEditableColumn,
value: string
): Promise<void> {
try {
const file = this.getCurrentTFileForQuickFix();
if (!file) {
new Notice(this.t("colorScheme.preview.pickFailed"));
return;
}
const markdown = await this.app.vault.read(file);
const result = updateColorSchemeColorCell(markdown, {
rowIndex,
columnName,
value
});
if (!result.changed) {
const noticeKey = result.status === "unchanged"
? "colorScheme.preview.pickUnchanged"
: "colorScheme.preview.pickFailed";
new Notice(this.t(noticeKey));
return;
}
await this.app.vault.modify(file, result.updatedMarkdown);
new Notice(this.t("colorScheme.preview.pickApplied"));
this.renderCurrentState();
} catch {
new Notice(this.t("colorScheme.preview.pickFailed"));
}
}
private renderDomainTable(container: HTMLElement, domains: DomainEntry[]): void {
const section = this.createCollapsibleSection(
container,

View file

@ -931,6 +931,53 @@ body.model-weave-focus-mode-active .popover.hover-popover {
font-size: 0.75rem;
}
.model-weave-color-cell-control {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
min-width: 9rem;
}
.model-weave-color-cell-swatch {
width: 1.25rem;
height: 1.25rem;
flex: 0 0 auto;
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
background: var(--background-primary-alt);
}
.model-weave-color-cell-swatch-empty {
background: repeating-linear-gradient(
45deg,
var(--background-primary-alt),
var(--background-primary-alt) 4px,
var(--background-modifier-border) 4px,
var(--background-modifier-border) 5px
);
}
.model-weave-color-cell-value {
font-family: var(--font-monospace);
font-size: 0.85em;
white-space: nowrap;
}
.model-weave-color-picker-input {
width: 1px;
height: 1px;
opacity: 0;
position: absolute;
pointer-events: none;
}
.model-weave-color-picker-button {
padding: 2px 8px;
font-size: 0.8em;
line-height: 1.4;
}
.model-weave-summary-details .model-weave-summary-empty-cell {
color: var(--text-muted);
font-style: italic;

View file

@ -0,0 +1,144 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { build } from "esbuild";
const outputFile = "dist/test-color-scheme-picker-assist.mjs";
await build({
stdin: {
contents: [
'export { parseColorSchemeFile } from "./src/parsers/color-scheme-parser";',
'export { buildCurrentObjectDiagnostics } from "./src/core/current-file-diagnostics";',
'export { getExpectedHeaderForDiagnostic } from "./src/core/diagnostic-section-guidance";',
'export { normalizeHexColorForPicker, updateColorSchemeColorCell } from "./src/core/color-scheme-table-editor";'
].join("\n"),
resolveDir: ".",
sourcefile: "test-color-scheme-picker-assist-entry.ts",
loader: "ts"
},
bundle: true,
format: "esm",
platform: "browser",
outfile: outputFile,
plugins: [
{
name: "stub-obsidian",
setup(buildApi) {
buildApi.onResolve({ filter: /^obsidian$/ }, () => ({
path: "obsidian",
namespace: "stub"
}));
buildApi.onLoad({ filter: /^obsidian$/, namespace: "stub" }, () => ({
contents: "export const getLanguage = () => 'en';",
loader: "js"
}));
}
}
],
logLevel: "silent"
});
const {
buildCurrentObjectDiagnostics,
getExpectedHeaderForDiagnostic,
normalizeHexColorForPicker,
parseColorSchemeFile,
updateColorSchemeColorCell
} = await import("../" + outputFile + "?t=" + Date.now());
function colorSchemeMarkdown({ header = "| target | kind | fill | stroke | text | notes |", row = "| domain | system | #112233 | #445566 | #abcdef | Base |" } = {}) {
return `---
type: color_scheme
id: COLOR-TEST
name: Color Test
---
# Color Test
## Colors
${header}
|---|---|---|---|---|---|
${row}
`;
}
function update(markdown, rowIndex, columnName, value) {
return updateColorSchemeColorCell(markdown, { rowIndex, columnName, value });
}
test("updates a fill cell in a valid color_scheme Colors table", () => {
const result = update(colorSchemeMarkdown(), 0, "fill", "#aabbcc");
assert.equal(result.status, "updated");
assert.equal(result.changed, true);
assert.match(result.updatedMarkdown, /\| domain \| system \| #aabbcc \| #445566 \| #abcdef \| Base \|/);
});
test("updates a stroke cell in a valid color_scheme Colors table", () => {
const result = update(colorSchemeMarkdown(), 0, "stroke", "#0a1b2c");
assert.equal(result.status, "updated");
assert.match(result.updatedMarkdown, /\| domain \| system \| #112233 \| #0a1b2c \| #abcdef \| Base \|/);
});
test("updates a text cell in a valid color_scheme Colors table", () => {
const result = update(colorSchemeMarkdown(), 0, "text", "#ffffff");
assert.equal(result.status, "updated");
assert.match(result.updatedMarkdown, /\| domain \| system \| #112233 \| #445566 \| #ffffff \| Base \|/);
});
test("updates a blank color cell with a selected RRGGBB value", () => {
const markdown = colorSchemeMarkdown({ row: "| domain | system | | #445566 | #abcdef | Base |" });
const result = update(markdown, 0, "fill", "#123abc");
assert.equal(result.status, "updated");
assert.match(result.updatedMarkdown, /\| domain \| system \| #123abc \| #445566 \| #abcdef \| Base \|/);
});
test("accepts an existing RGB value and can replace it safely", () => {
const markdown = colorSchemeMarkdown({ row: "| domain | system | #abc | #445566 | #abcdef | Base |" });
assert.equal(normalizeHexColorForPicker("#abc"), "#aabbcc");
const result = update(markdown, 0, "fill", "#ddeeff");
assert.equal(result.status, "updated");
assert.match(result.updatedMarkdown, /\| domain \| system \| #ddeeff \| #445566 \| #abcdef \| Base \|/);
});
test("replaces unsupported existing color values without rewriting other cells", () => {
const markdown = colorSchemeMarkdown({ row: "| domain | system | blue | #445566 | #abcdef | Base |" });
const result = update(markdown, 0, "fill", "#010203");
assert.equal(result.status, "updated");
assert.match(result.updatedMarkdown, /\| domain \| system \| #010203 \| #445566 \| #abcdef \| Base \|/);
});
test("refuses to update a malformed Colors header", () => {
const markdown = colorSchemeMarkdown({ header: "| targeta | kind | fill | stroke | text | notes |" });
const result = update(markdown, 0, "fill", "#010203");
assert.equal(result.status, "header-mismatch");
assert.equal(result.changed, false);
assert.equal(result.updatedMarkdown, markdown);
});
test("malformed Colors header keeps schema-driven diagnostics behavior", () => {
const markdown = colorSchemeMarkdown({
header: "| targeta | kind | fill | stroke | text | notes |",
row: "| domain | system | #112233 | #445566 | #abcdef | Base |\n| domain | system | #112233 | #445566 | #abcdef | Duplicate |"
});
const parsed = parseColorSchemeFile(markdown, "COLOR-TEST.md");
assert.ok(parsed.file);
const diagnostics = buildCurrentObjectDiagnostics(parsed.file, { warningsByFilePath: {} }, null, parsed.warnings);
const headerDiagnostic = diagnostics.find((diagnostic) => diagnostic.code === "invalid-table-column");
assert.ok(headerDiagnostic);
assert.equal(headerDiagnostic.severity, "error");
assert.equal(getExpectedHeaderForDiagnostic(headerDiagnostic), "| target | kind | fill | stroke | text | notes |");
assert.equal(
diagnostics.some((diagnostic) => /duplicate Color Scheme entry/.test(diagnostic.message)),
false
);
});