mirror of
https://github.com/nejimakibird/model-weave.git
synced 2026-07-22 06:51:24 +00:00
Merge branch 'feature/color-scheme-picker-assist' into develop
This commit is contained in:
commit
e85556bed9
7 changed files with 736 additions and 26 deletions
238
main.js
238
main.js
|
|
@ -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,
|
||||
|
|
|
|||
188
src/core/color-scheme-table-editor.ts
Normal file
188
src/core/color-scheme-table-editor.ts
Normal 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
|
||||
};
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "塗り",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
47
styles.css
47
styles.css
|
|
@ -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;
|
||||
|
|
|
|||
144
test/color-scheme-picker-assist.test.mjs
Normal file
144
test/color-scheme-picker-assist.test.mjs
Normal 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
|
||||
);
|
||||
});
|
||||
Loading…
Reference in a new issue