fix: overlay pattern editor — hex preview, palette refresh, thin outline

Three refinements to the 1.3.0 overlay pattern editor based on user
feedback:

- Preview: was a 120×36 rect at the pattern's own scale. Now a hex
  polygon at the live on-map hex size (read from .duckmage-hex,
  falling back to 96 px), in the configured orientation — so the
  preview shows exactly what the overlay will look like on the map.

- Palette tiles: were never updating on Save because the picker
  re-read style from the metadata cache, which lags Obsidian's
  frontmatter write by a tick. The editor's onSaved callback now
  passes the new OverlayStyle directly, so the picker renders from
  the editor's in-memory state and updates immediately.

- Outline width: was hardcoded to gapPx*2+2 (often 14-22 px), which
  made overlay borders read as solid blobs. Now per-overlay,
  defaulting to 1.5 px, with a slider (0-20, step 0.5) in the editor.
  Persisted as faction-outline-width / region-outline-width
  frontmatter keys. Crank it up to recover the old thick-blob look.
This commit is contained in:
isaprettycoolguy@protonmail.com 2026-06-17 12:22:28 -04:00
parent f9e292c5f0
commit 06bb93c91d
7 changed files with 220 additions and 80 deletions

View file

@ -5,21 +5,25 @@ import {
DEFAULT_OVERLAY_PATTERN_KEY,
DEFAULT_OVERLAY_PATTERN_SCALE,
DEFAULT_OVERLAY_PATTERN_OPACITY,
DEFAULT_OVERLAY_OUTLINE_WIDTH,
normalizeOverlayPatternKey,
normalizeOverlayPatternScale,
normalizeOverlayPatternOpacity,
normalizeOverlayOutlineWidth,
} from "./overlayPatterns";
export interface OverlayStyle {
pattern: OverlayPatternKey;
scale: number;
opacity: number;
outlineWidth: number;
}
export const DEFAULT_OVERLAY_STYLE: OverlayStyle = {
pattern: DEFAULT_OVERLAY_PATTERN_KEY,
scale: DEFAULT_OVERLAY_PATTERN_SCALE,
opacity: DEFAULT_OVERLAY_PATTERN_OPACITY,
outlineWidth: DEFAULT_OVERLAY_OUTLINE_WIDTH,
};
export interface Frontmatter {
@ -38,10 +42,12 @@ export interface Frontmatter {
"faction-pattern"?: string;
"faction-pattern-scale"?: string;
"faction-pattern-opacity"?: string;
"faction-outline-width"?: string;
"region-color"?: string;
"region-pattern"?: string;
"region-pattern-scale"?: string;
"region-pattern-opacity"?: string;
"region-outline-width"?: string;
}
export function getFrontMatter(app: App, path: string) {
@ -102,6 +108,7 @@ export function getFactionStyleFromFile(app: App, path: string): OverlayStyle {
pattern: normalizeOverlayPatternKey(fm?.["faction-pattern"]),
scale: normalizeOverlayPatternScale(fm?.["faction-pattern-scale"]),
opacity: normalizeOverlayPatternOpacity(fm?.["faction-pattern-opacity"]),
outlineWidth: normalizeOverlayOutlineWidth(fm?.["faction-outline-width"]),
};
}
@ -125,6 +132,10 @@ export async function setFactionStyleInFile(
if (style.opacity === DEFAULT_OVERLAY_PATTERN_OPACITY) delete fm["faction-pattern-opacity"];
else fm["faction-pattern-opacity"] = String(style.opacity);
}
if (style.outlineWidth !== undefined) {
if (style.outlineWidth === DEFAULT_OVERLAY_OUTLINE_WIDTH) delete fm["faction-outline-width"];
else fm["faction-outline-width"] = String(style.outlineWidth);
}
});
return true;
}
@ -179,6 +190,7 @@ export function getRegionStyleFromFile(app: App, path: string): OverlayStyle {
pattern: normalizeOverlayPatternKey(fm?.["region-pattern"]),
scale: normalizeOverlayPatternScale(fm?.["region-pattern-scale"]),
opacity: normalizeOverlayPatternOpacity(fm?.["region-pattern-opacity"]),
outlineWidth: normalizeOverlayOutlineWidth(fm?.["region-outline-width"]),
};
}
@ -202,6 +214,10 @@ export async function setRegionStyleInFile(
if (style.opacity === DEFAULT_OVERLAY_PATTERN_OPACITY) delete fm["region-pattern-opacity"];
else fm["region-pattern-opacity"] = String(style.opacity);
}
if (style.outlineWidth !== undefined) {
if (style.outlineWidth === DEFAULT_OVERLAY_OUTLINE_WIDTH) delete fm["region-outline-width"];
else fm["region-outline-width"] = String(style.outlineWidth);
}
});
return true;
}

View file

@ -7,18 +7,28 @@ import {
setFactionColorInFile,
getFactionStyleFromFile,
setFactionStyleInFile,
type OverlayStyle,
} from "../frontmatter";
import { addOverlayPatternControls } from "./overlayPatternControls";
import { addOverlayPatternControls, renderHexPreview } from "./overlayPatternControls";
// ── Faction editor sub-modal ─────────────────────────────────────────────────
class FactionEditorModal extends HexmakerModal {
constructor(
app: App,
private plugin: HexmakerPlugin,
private file: TFile,
private initialColor: string | null,
/** Called on save with the (possibly updated) color and basename. */
private onSaved: (color: string | null, newBasename: string) => void,
/**
* Called on save with the (possibly updated) color, basename, and full
* overlay style. We pass the style directly because the metadata cache
* may not yet reflect the just-written frontmatter.
*/
private onSaved: (
color: string | null,
newBasename: string,
style: OverlayStyle,
) => void,
) {
super(app);
}
@ -83,6 +93,7 @@ class FactionEditorModal extends HexmakerModal {
contentEl,
initialStyle,
() => pendingColor,
this.plugin.settings.hexOrientation,
);
// ── Buttons ───────────────────────────────────────────────────────────────
@ -107,7 +118,7 @@ class FactionEditorModal extends HexmakerModal {
// this.file.path is updated in-place by Obsidian after rename
await setFactionColorInFile(this.app, this.file.path, pendingColor);
await setFactionStyleInFile(this.app, this.file.path, styleState);
this.onSaved(pendingColor, newBasename);
this.onSaved(pendingColor, newBasename, styleState);
this.close();
})();
});
@ -120,7 +131,7 @@ class FactionEditorModal extends HexmakerModal {
clearBtn.addEventListener("click", () => {
void (async () => {
await setFactionColorInFile(this.app, this.file.path, null);
this.onSaved(null, this.file.basename);
this.onSaved(null, this.file.basename, styleState);
this.close();
})();
});
@ -190,14 +201,18 @@ export class FactionPickerModal extends HexmakerModal {
});
}
const orientation = this.plugin.settings.hexOrientation;
const TILE_HEX_PX = 48;
for (const file of files) {
let color = this.pendingColorOverrides.get(file.path) ?? getFactionColorFromFile(this.app, file.path);
let style = getFactionStyleFromFile(this.app, file.path);
const tile = grid.createDiv({ cls: "duckmage-faction-tile" });
const preview = tile.createDiv({ cls: "duckmage-faction-tile-preview" });
if (color) {
preview.setCssProps({ "--duckmage-tile-color": color });
renderHexPreview(preview, { color, style, orientation, hexSizePx: TILE_HEX_PX });
} else {
preview.addClass("duckmage-faction-tile-preview-empty");
}
@ -219,15 +234,22 @@ export class FactionPickerModal extends HexmakerModal {
e.stopPropagation();
new FactionEditorModal(
this.app,
this.plugin,
file, // TFile mutated in-place by Obsidian on rename
color,
(newColor, newBasename) => {
(newColor, newBasename, newStyle) => {
color = newColor;
style = newStyle;
preview.replaceChildren();
if (newColor) {
preview.setCssProps({ "--duckmage-tile-color": newColor });
preview.removeClass("duckmage-faction-tile-preview-empty");
renderHexPreview(preview, {
color: newColor,
style: newStyle,
orientation,
hexSizePx: TILE_HEX_PX,
});
} else {
preview.setCssProps({ "--duckmage-tile-color": "" });
preview.addClass("duckmage-faction-tile-preview-empty");
}
nameEl.setText(newBasename);
@ -280,6 +302,7 @@ export class FactionPickerModal extends HexmakerModal {
new FactionEditorModal(
this.app,
this.plugin,
file,
null,
(newColor) => {
@ -289,6 +312,8 @@ export class FactionPickerModal extends HexmakerModal {
).open();
}
onClose(): void {
this.contentEl.empty();
}

View file

@ -7,17 +7,23 @@ import {
setRegionColorInFile,
getRegionStyleFromFile,
setRegionStyleInFile,
type OverlayStyle,
} from "../frontmatter";
import { addOverlayPatternControls } from "./overlayPatternControls";
import { addOverlayPatternControls, renderHexPreview } from "./overlayPatternControls";
// ── Region editor sub-modal ───────────────────────────────────────────────────
class GeoRegionEditorModal extends HexmakerModal {
constructor(
app: App,
private plugin: HexmakerPlugin,
private file: TFile,
private initialColor: string | null,
private onSaved: (color: string | null, newBasename: string) => void,
private onSaved: (
color: string | null,
newBasename: string,
style: OverlayStyle,
) => void,
) {
super(app);
}
@ -80,6 +86,7 @@ class GeoRegionEditorModal extends HexmakerModal {
contentEl,
initialStyle,
() => pendingColor,
this.plugin.settings.hexOrientation,
);
// ── Buttons ───────────────────────────────────────────────────────────────
@ -100,7 +107,7 @@ class GeoRegionEditorModal extends HexmakerModal {
}
await setRegionColorInFile(this.app, this.file.path, pendingColor);
await setRegionStyleInFile(this.app, this.file.path, styleState);
this.onSaved(pendingColor, newBasename);
this.onSaved(pendingColor, newBasename, styleState);
this.close();
})();
});
@ -113,7 +120,7 @@ class GeoRegionEditorModal extends HexmakerModal {
clearBtn.addEventListener("click", () => {
void (async () => {
await setRegionColorInFile(this.app, this.file.path, null);
this.onSaved(null, this.file.basename);
this.onSaved(null, this.file.basename, styleState);
this.close();
})();
});
@ -183,14 +190,18 @@ export class GeoRegionPickerModal extends HexmakerModal {
});
}
const orientation = this.plugin.settings.hexOrientation;
const TILE_HEX_PX = 48;
for (const file of files) {
let color = this.pendingColorOverrides.get(file.path) ?? getRegionColorFromFile(this.app, file.path);
let style = getRegionStyleFromFile(this.app, file.path);
const tile = grid.createDiv({ cls: "duckmage-faction-tile" });
const preview = tile.createDiv({ cls: "duckmage-faction-tile-preview" });
if (color) {
preview.setCssProps({ "--duckmage-tile-color": color });
renderHexPreview(preview, { color, style, orientation, hexSizePx: TILE_HEX_PX });
} else {
preview.addClass("duckmage-faction-tile-preview-empty");
}
@ -210,15 +221,22 @@ export class GeoRegionPickerModal extends HexmakerModal {
e.stopPropagation();
new GeoRegionEditorModal(
this.app,
this.plugin,
file,
color,
(newColor, newBasename) => {
(newColor, newBasename, newStyle) => {
color = newColor;
style = newStyle;
preview.replaceChildren();
if (newColor) {
preview.setCssProps({ "--duckmage-tile-color": newColor });
preview.removeClass("duckmage-faction-tile-preview-empty");
renderHexPreview(preview, {
color: newColor,
style: newStyle,
orientation,
hexSizePx: TILE_HEX_PX,
});
} else {
preview.setCssProps({ "--duckmage-tile-color": "" });
preview.addClass("duckmage-faction-tile-preview-empty");
}
nameEl.setText(newBasename);
@ -268,7 +286,7 @@ export class GeoRegionPickerModal extends HexmakerModal {
const file = await this.app.vault.create(filePath, "");
void this.plugin.ensureRegionTable(name);
new GeoRegionEditorModal(this.app, file, null, (newColor) => {
new GeoRegionEditorModal(this.app, this.plugin, file, null, (newColor) => {
if (newColor) this.pendingColorOverrides.set(file.path, newColor);
this.buildPalette();
}).open();

View file

@ -14,7 +14,7 @@ import { HexEditorModal } from "./HexEditorModal";
import { TerrainPickerModal } from "./TerrainPickerModal";
import { IconPickerModal } from "./IconPickerModal";
import { addLinkToSection, getLinksInSection, removeLinkFromSection } from "../sections";
import { getFactionColorFromFile, getRegionColorFromFile, getFactionStyleFromFile, getRegionStyleFromFile, setHexRegionInFile, getSubmapFromFile, setSubmapInFile } from "../frontmatter";
import { getFactionColorFromFile, getRegionColorFromFile, getFactionStyleFromFile, getRegionStyleFromFile, setHexRegionInFile, getSubmapFromFile, setSubmapInFile, type OverlayStyle } from "../frontmatter";
import { buildSvgPattern, colorToIdToken, type OverlayPatternKey } from "../overlayPatterns";
import {
VIEW_TYPE_HEX_MAP,
@ -3344,7 +3344,7 @@ export class HexMapView extends ItemView {
// ── Build faction color + style maps ──────────────────────────────────────
const folder = normalizeFolder(this.plugin.settings.factionsFolder);
const factionColorMap = new Map<string, string>();
const factionStyleMap = new Map<string, { pattern: OverlayPatternKey; scale: number; opacity: number }>();
const factionStyleMap = new Map<string, OverlayStyle>();
for (const f of this.app.vault.getMarkdownFiles()) {
if (folder && !f.path.startsWith(folder + "/")) continue;
const color = getFactionColorFromFile(this.app, f.path);
@ -3423,7 +3423,7 @@ export class HexMapView extends ItemView {
for (const [factionName, hexKeys] of factionHexKeys) {
const color = factionColorMap.get(factionName);
if (!color) continue;
const style = factionStyleMap.get(factionName) ?? { pattern: "solid" as OverlayPatternKey, scale: 16, opacity: 0.45 };
const style = factionStyleMap.get(factionName) ?? { pattern: "solid" as OverlayPatternKey, scale: 16, opacity: 0.45, outlineWidth: 1.5 };
const patternId = ensurePattern(style.pattern, color, style.scale);
const fillVal = patternId ? `url(#${patternId})` : color;
@ -3509,7 +3509,7 @@ export class HexMapView extends ItemView {
path.setAttribute("d", d);
path.setAttribute("fill", fillVal);
path.setAttribute("stroke", color);
path.setAttribute("stroke-width", String(gapPx * 2 + 2));
path.setAttribute("stroke-width", String(style.outlineWidth));
path.setAttribute("stroke-linejoin", "round");
path.setAttribute("stroke-linecap", "round");
path.setAttribute("paint-order", "stroke fill");
@ -3717,7 +3717,7 @@ export class HexMapView extends ItemView {
// ── Region color + style maps ───────────────────────────────────────────
const folder = normalizeFolder(this.plugin.settings.regionsFolder);
const regionColorMap = new Map<string, string>();
const regionStyleMap = new Map<string, { pattern: OverlayPatternKey; scale: number; opacity: number }>();
const regionStyleMap = new Map<string, OverlayStyle>();
for (const f of this.app.vault.getMarkdownFiles()) {
if (f.basename.startsWith("_")) continue;
if (folder && !f.path.startsWith(folder + "/")) continue;
@ -3782,7 +3782,7 @@ export class HexMapView extends ItemView {
for (const [regionName, hexKeys] of regionHexKeys) {
const color = regionColorMap.get(regionName);
if (!color) continue;
const style = regionStyleMap.get(regionName) ?? { pattern: "solid" as OverlayPatternKey, scale: 16, opacity: 0.45 };
const style = regionStyleMap.get(regionName) ?? { pattern: "solid" as OverlayPatternKey, scale: 16, opacity: 0.45, outlineWidth: 1.5 };
const patternId = ensureRegionPattern(style.pattern, color, style.scale);
const fillVal = patternId ? `url(#${patternId})` : color;
@ -3884,9 +3884,8 @@ export class HexMapView extends ItemView {
const path = activeDocument.createElementNS(svgNS, "path");
path.setAttribute("d", d);
path.setAttribute("fill", fillVal);
// Stroke bridges the inter-hex gap; round joins/caps smooth blob edges
path.setAttribute("stroke", color);
path.setAttribute("stroke-width", String(gapPx * 2 + 2));
path.setAttribute("stroke-width", String(style.outlineWidth));
path.setAttribute("stroke-linejoin", "round");
path.setAttribute("stroke-linecap", "round");
path.setAttribute("paint-order", "stroke fill");

View file

@ -7,74 +7,124 @@ import {
MAX_PATTERN_SCALE,
MIN_PATTERN_OPACITY,
MAX_PATTERN_OPACITY,
MIN_OUTLINE_WIDTH,
MAX_OUTLINE_WIDTH,
buildSvgPattern,
} from "../overlayPatterns";
import type { OverlayStyle } from "../frontmatter";
import { hexPolygonPoints } from "./hexGeometry";
const SVG_NS = "http://www.w3.org/2000/svg";
const PREVIEW_W = 120;
const PREVIEW_H = 36;
/** Returns the live on-map hex width in CSS pixels; falls back to 96 if no map view is open. */
export function getOnMapHexSizePx(): number {
const hex = activeDocument.querySelector(".duckmage-hex");
if (hex) {
const r = hex.getBoundingClientRect();
if (r.width > 0) return r.width;
}
return 96;
}
/**
* Add a pattern dropdown + scale + opacity sliders + live preview to `containerEl`.
* Returns the live state object; callers read this on Save and write to frontmatter.
* Render a hex-shaped preview into `container` showing what the overlay will
* look like on the map. Replaces any existing children.
*/
export function renderHexPreview(
container: HTMLElement,
opts: {
color: string;
style: OverlayStyle;
orientation: "flat" | "pointy";
hexSizePx: number;
},
): void {
container.replaceChildren();
const { color, style, orientation, hexSizePx } = opts;
const W = hexSizePx;
let radius: number;
let H: number;
if (orientation === "flat") {
radius = W / 2;
H = W * Math.sqrt(3) / 2;
} else {
radius = W / Math.sqrt(3);
H = 2 * radius;
}
const PAD = 3;
const svgW = W + 2 * PAD;
const svgH = H + 2 * PAD;
const cx = svgW / 2;
const cy = svgH / 2;
const svg = document.createElementNS(SVG_NS, "svg");
svg.setAttribute("width", String(svgW));
svg.setAttribute("height", String(svgH));
svg.classList.add("duckmage-hex-preview-svg");
// Pattern def (only if non-solid)
let fillVal: string = color;
if (style.pattern !== "solid") {
const defs = document.createElementNS(SVG_NS, "defs");
const id = `dm-prev-${Math.random().toString(36).slice(2, 9)}`;
const pat = buildSvgPattern(document, {
id,
pattern: style.pattern,
color,
scale: style.scale,
});
if (pat) {
defs.appendChild(pat);
svg.appendChild(defs);
fillVal = `url(#${id})`;
}
}
const pts = hexPolygonPoints(cx, cy, orientation, radius - 1);
const poly = document.createElementNS(SVG_NS, "polygon");
poly.setAttribute("points", pts.map((p) => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" "));
poly.setAttribute("fill", fillVal);
poly.setAttribute("stroke", color);
poly.setAttribute("stroke-width", String(style.outlineWidth));
poly.setAttribute("opacity", String(style.opacity));
svg.appendChild(poly);
container.appendChild(svg);
}
/**
* Add a pattern dropdown + scale + opacity sliders + hex-shaped live preview
* (rendered at the actual on-map hex size) to `containerEl`. Returns the live
* state object; callers read it on Save and write to frontmatter.
*/
export function addOverlayPatternControls(
containerEl: HTMLElement,
initial: OverlayStyle,
getColor: () => string,
orientation: "flat" | "pointy",
): { state: OverlayStyle; refreshPreview: () => void } {
const state: OverlayStyle = { ...initial };
// ── Live preview ──────────────────────────────────────────────────────────
const previewWrap = containerEl.createDiv({ cls: "duckmage-pattern-preview-wrap" });
previewWrap.createSpan({
text: "Preview",
cls: "duckmage-pattern-preview-label",
});
const previewSvg = document.createElementNS(SVG_NS, "svg");
previewSvg.setAttribute("width", String(PREVIEW_W));
previewSvg.setAttribute("height", String(PREVIEW_H));
previewSvg.classList.add("duckmage-pattern-preview-svg");
previewWrap.appendChild(previewSvg);
const previewHost = previewWrap.createDiv({ cls: "duckmage-pattern-preview-host" });
const hexSizePx = getOnMapHexSizePx();
const refreshPreview = () => {
previewSvg.replaceChildren();
const color = getColor();
if (state.pattern === "solid") {
const rect = document.createElementNS(SVG_NS, "rect");
rect.setAttribute("x", "0");
rect.setAttribute("y", "0");
rect.setAttribute("width", String(PREVIEW_W));
rect.setAttribute("height", String(PREVIEW_H));
rect.setAttribute("fill", color);
rect.setAttribute("opacity", String(state.opacity));
previewSvg.appendChild(rect);
return;
}
const defs = document.createElementNS(SVG_NS, "defs");
const id = `dm-preview-${Math.random().toString(36).slice(2, 9)}`;
const pat = buildSvgPattern(document, {
id,
pattern: state.pattern,
color,
scale: state.scale,
renderHexPreview(previewHost, {
color: getColor(),
style: state,
orientation,
hexSizePx,
});
if (pat) defs.appendChild(pat);
previewSvg.appendChild(defs);
const bg = document.createElementNS(SVG_NS, "rect");
bg.setAttribute("x", "0");
bg.setAttribute("y", "0");
bg.setAttribute("width", String(PREVIEW_W));
bg.setAttribute("height", String(PREVIEW_H));
bg.setAttribute("fill", `url(#${id})`);
bg.setAttribute("opacity", String(state.opacity));
previewSvg.appendChild(bg);
};
refreshPreview();
// ── Pattern dropdown ──────────────────────────────────────────────────────
new Setting(containerEl).setName("Pattern").addDropdown((dd) => {
for (const key of OVERLAY_PATTERN_KEYS) {
dd.addOption(key, OVERLAY_PATTERN_LABELS[key]);
@ -85,7 +135,6 @@ export function addOverlayPatternControls(
});
});
// ── Scale slider ──────────────────────────────────────────────────────────
new Setting(containerEl).setName("Pattern scale").addSlider((sl) =>
sl
.setLimits(MIN_PATTERN_SCALE, MAX_PATTERN_SCALE, 1)
@ -97,7 +146,6 @@ export function addOverlayPatternControls(
}),
);
// ── Opacity slider ────────────────────────────────────────────────────────
new Setting(containerEl).setName("Opacity").addSlider((sl) =>
sl
.setLimits(MIN_PATTERN_OPACITY, MAX_PATTERN_OPACITY, 0.05)
@ -109,5 +157,16 @@ export function addOverlayPatternControls(
}),
);
new Setting(containerEl).setName("Outline width").addSlider((sl) =>
sl
.setLimits(MIN_OUTLINE_WIDTH, MAX_OUTLINE_WIDTH, 0.5)
.setValue(state.outlineWidth)
.setDynamicTooltip()
.onChange((v) => {
state.outlineWidth = v;
refreshPreview();
}),
);
return { state, refreshPreview };
}

View file

@ -40,11 +40,14 @@ export const OVERLAY_PATTERN_LABELS: Record<OverlayPatternKey, string> = {
export const DEFAULT_OVERLAY_PATTERN_KEY: OverlayPatternKey = "solid";
export const DEFAULT_OVERLAY_PATTERN_SCALE = 16;
export const DEFAULT_OVERLAY_PATTERN_OPACITY = 0.45;
export const DEFAULT_OVERLAY_OUTLINE_WIDTH = 1.5;
export const MIN_PATTERN_SCALE = 8;
export const MAX_PATTERN_SCALE = 48;
export const MIN_PATTERN_OPACITY = 0.05;
export const MAX_PATTERN_OPACITY = 1.0;
export const MIN_OUTLINE_WIDTH = 0;
export const MAX_OUTLINE_WIDTH = 20;
export function isOverlayPatternKey(v: unknown): v is OverlayPatternKey {
return (
@ -79,6 +82,17 @@ export function normalizeOverlayPatternOpacity(v: unknown): number {
return Math.min(MAX_PATTERN_OPACITY, Math.max(MIN_PATTERN_OPACITY, n));
}
export function normalizeOverlayOutlineWidth(v: unknown): number {
const n =
typeof v === "number"
? v
: typeof v === "string"
? parseFloat(v)
: NaN;
if (!Number.isFinite(n)) return DEFAULT_OVERLAY_OUTLINE_WIDTH;
return Math.min(MAX_OUTLINE_WIDTH, Math.max(MIN_OUTLINE_WIDTH, n));
}
const SVG_NS = "http://www.w3.org/2000/svg";
/**

View file

@ -3718,11 +3718,16 @@ tr:hover .duckmage-rt-entry-copy-btn:hover {
}
.duckmage-faction-tile-preview {
width: 48px;
height: 48px;
width: 56px;
height: 56px;
border-radius: 4px;
border: 1px solid var(--background-modifier-border);
background-color: var(--duckmage-tile-color);
display: flex;
align-items: center;
justify-content: center;
}
.duckmage-faction-tile-preview svg {
display: block;
}
/* Empty / no-color state: diagonal stripe */
@ -3833,9 +3838,9 @@ tr:hover .duckmage-rt-entry-copy-btn:hover {
.duckmage-pattern-preview-wrap {
display: flex;
align-items: center;
gap: 10px;
margin: 10px 0 4px;
padding: 6px 10px;
gap: 14px;
margin: 10px 0 6px;
padding: 8px 12px;
border-radius: 6px;
background: var(--background-secondary);
}
@ -3846,10 +3851,14 @@ tr:hover .duckmage-rt-entry-copy-btn:hover {
font-weight: 500;
}
.duckmage-pattern-preview-svg {
.duckmage-pattern-preview-host {
display: flex;
align-items: center;
justify-content: center;
}
.duckmage-hex-preview-svg {
display: block;
border-radius: 4px;
background: var(--background-primary);
}
/* Region overlay SVG */