mirror of
https://github.com/sbuffkin/hexmaker.git
synced 2026-07-22 06:14:01 +00:00
init commit
This commit is contained in:
commit
9a4b5d4254
13 changed files with 1765 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
node_modules
|
||||
main.js
|
||||
main.js.map
|
||||
.DS_Store
|
||||
*.log
|
||||
35
README.md
Normal file
35
README.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Duckmage Obsidian Plugin
|
||||
|
||||
A template Obsidian plugin. Customize `manifest.json`, `main.ts`, and `styles.css` for your vault.
|
||||
|
||||
## Development
|
||||
|
||||
1. **Install dependencies**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. **Build**
|
||||
- `npm run dev` — watch mode (rebuilds on save)
|
||||
- `npm run build` — production build
|
||||
|
||||
3. **Install in Obsidian**
|
||||
- Copy the entire `plugin` folder into your vault’s `.obsidian/plugins/duckmage-plugin/` (create the folder if needed), **or** symlink this folder there.
|
||||
- Ensure these files are present in that folder: `main.js`, `manifest.json`, `styles.css`.
|
||||
- In Obsidian: **Settings → Community plugins → Turn on “Duckmage Plugin”** (enable Developer mode if the plugin doesn’t appear).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **"Failed to load plugin duckmage-plugin"** — Usually means `main.js` is missing. Run `npm run build` in this folder so that `main.js` is generated from `main.ts`. Obsidian loads the compiled JS, not the TypeScript source.
|
||||
- **Viewing logs** — In Obsidian, press **Ctrl+Shift+I** (Windows/Linux) or **Cmd+Option+I** (Mac) to open Developer tools. Check the **Console** tab for red error messages when you enable the plugin or use its commands.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|--------|
|
||||
| `main.ts` | Plugin logic (ribbon, commands, settings) |
|
||||
| `manifest.json` | Plugin id, name, version, min Obsidian version |
|
||||
| `styles.css` | Plugin styles |
|
||||
| `versions.json` | Version history for community plugin releases |
|
||||
|
||||
After editing `main.ts`, run `npm run dev` or `npm run build` to produce `main.js`.
|
||||
11
_Index_of_plugin.md
Normal file
11
_Index_of_plugin.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
%% Zoottelkeeper: Beginning of the autogenerated index file list %%
|
||||
[[RPG/duckmage/plugin/esbuild.config.mjs|esbuild.config.mjs]]
|
||||
[[RPG/duckmage/plugin/main.ts|main.ts]]
|
||||
[[RPG/duckmage/plugin/manifest.json|manifest.json]]
|
||||
[[RPG/duckmage/plugin/package.json|package.json]]
|
||||
[[RPG/duckmage/plugin/README|README]]
|
||||
[[RPG/duckmage/plugin/styles.css|styles.css]]
|
||||
[[RPG/duckmage/plugin/tsconfig.json|tsconfig.json]]
|
||||
[[RPG/duckmage/plugin/version-bump.mjs|version-bump.mjs]]
|
||||
[[RPG/duckmage/plugin/versions.json|versions.json]]
|
||||
%% Zoottelkeeper: End of the autogenerated index file list %%
|
||||
5
data.json
Normal file
5
data.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"mySetting": "default",
|
||||
"hexFolder": "RPG/duckmage/world/hexes",
|
||||
"templatePath": "RPG/duckmage/world/hextemplate.md"
|
||||
}
|
||||
41
esbuild.config.mjs
Normal file
41
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import { createRequire } from "module";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const pkg = require("./package.json");
|
||||
const fs = require("fs");
|
||||
|
||||
const production = process.argv[2] === "production";
|
||||
|
||||
const banner = `/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
`;
|
||||
|
||||
const prod = production;
|
||||
const outDir = ".";
|
||||
const outFile = "main.js";
|
||||
|
||||
const context = await esbuild.context({
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["main.ts"],
|
||||
bundle: true,
|
||||
external: ["obsidian"],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: `${outDir}/${outFile}`,
|
||||
});
|
||||
|
||||
if (prod) {
|
||||
await context.rebuild();
|
||||
process.exit(0);
|
||||
} else {
|
||||
await context.watch();
|
||||
}
|
||||
395
main.ts
Normal file
395
main.ts
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
import { App, ItemView, Menu, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, WorkspaceLeaf } from "obsidian";
|
||||
|
||||
const VIEW_TYPE_HEX_MAP = "duckmage-hex-map";
|
||||
|
||||
/** Normalize folder path: no leading/trailing slashes. */
|
||||
function normalizeFolder(path: string): string {
|
||||
return path.replace(/^\/+|\/+$/g, "") || "";
|
||||
}
|
||||
|
||||
/** Get terrain key from a note's frontmatter (from cache or by reading). */
|
||||
function getTerrainFromFile(app: App, path: string): string | null {
|
||||
const file = app.vault.getAbstractFileByPath(path);
|
||||
if (!(file instanceof TFile)) return null;
|
||||
const cache = app.metadataCache.getFileCache(file);
|
||||
const terrain = cache?.frontmatter?.terrain;
|
||||
return typeof terrain === "string" ? terrain : null;
|
||||
}
|
||||
|
||||
/** Set terrain in a note's frontmatter. File must already exist. */
|
||||
async function setTerrainInFile(app: App, path: string, terrainKey: string | null): Promise<boolean> {
|
||||
const file = app.vault.getAbstractFileByPath(path);
|
||||
if (!(file instanceof TFile)) return false;
|
||||
const content = await app.vault.read(file);
|
||||
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
||||
let newContent: string;
|
||||
if (fmMatch) {
|
||||
const fmBlock = fmMatch[1];
|
||||
const rest = content.slice(fmMatch[0].length);
|
||||
let newFm: string;
|
||||
if (terrainKey === null) {
|
||||
// Remove any terrain line (including optional newline); support quoted values
|
||||
newFm = fmBlock.replace(/^\s*terrain:\s*[^\r\n]*(?:\r?\n)?/gm, "").trimEnd();
|
||||
} else {
|
||||
const terrainLine = /^\s*terrain:\s*.*$/m;
|
||||
newFm = terrainLine.test(fmBlock)
|
||||
? fmBlock.replace(terrainLine, `terrain: ${terrainKey}`)
|
||||
: fmBlock.trimEnd() + (fmBlock.endsWith("\n") ? "" : "\n") + `terrain: ${terrainKey}\n`;
|
||||
}
|
||||
newContent = `---\n${newFm}\n---\n${rest}`;
|
||||
} else {
|
||||
if (terrainKey === null) return true;
|
||||
newContent = `---\nterrain: ${terrainKey}\n---\n\n${content}`;
|
||||
}
|
||||
await app.vault.modify(file, newContent);
|
||||
return true;
|
||||
}
|
||||
|
||||
export interface TerrainColor {
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface DuckmagePluginSettings {
|
||||
mySetting: string;
|
||||
hexFolder: string;
|
||||
templatePath: string;
|
||||
hexGap: string;
|
||||
terrainPalette: TerrainColor[];
|
||||
}
|
||||
|
||||
const DEFAULT_TERRAIN_PALETTE: TerrainColor[] = [
|
||||
{ name: "mountain", color: "#9ca3af" },
|
||||
{ name: "water", color: "#60a5fa" },
|
||||
{ name: "grass", color: "#84cc16" },
|
||||
{ name: "forest", color: "#15803d" },
|
||||
{ name: "cliffs", color: "#a16207" },
|
||||
{ name: "desert", color: "#eab308" },
|
||||
{ name: "snow", color: "#e0f2fe" },
|
||||
];
|
||||
|
||||
const DEFAULT_SETTINGS: DuckmagePluginSettings = {
|
||||
mySetting: "default",
|
||||
hexFolder: "world/hexes",
|
||||
templatePath: "",
|
||||
hexGap: "0.15",
|
||||
terrainPalette: DEFAULT_TERRAIN_PALETTE,
|
||||
};
|
||||
|
||||
export default class DuckmagePlugin extends Plugin {
|
||||
settings: DuckmagePluginSettings;
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
||||
this.registerView(VIEW_TYPE_HEX_MAP, (leaf) => new HexMapView(leaf, this));
|
||||
|
||||
this.addRibbonIcon("map", "Duckmage: Open hex map", () => this.openHexMap());
|
||||
|
||||
this.addCommand({
|
||||
id: "open-hex-map",
|
||||
name: "Open Duckmage hex map",
|
||||
callback: () => this.openHexMap(),
|
||||
});
|
||||
|
||||
// Settings tab
|
||||
this.addSettingTab(new DuckmageSettingTab(this.app, this));
|
||||
}
|
||||
|
||||
private openHexMap(): void {
|
||||
const leaf = this.app.workspace.getLeaf(false);
|
||||
leaf.setViewState({ type: VIEW_TYPE_HEX_MAP });
|
||||
}
|
||||
|
||||
onunload() {}
|
||||
|
||||
async loadSettings() {
|
||||
const data = await this.loadData();
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, data);
|
||||
if (!Array.isArray(this.settings.terrainPalette) || this.settings.terrainPalette.length === 0) {
|
||||
this.settings.terrainPalette = [...DEFAULT_TERRAIN_PALETTE];
|
||||
}
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
/** Path for hex note at (x, y). */
|
||||
hexPath(x: number, y: number): string {
|
||||
const folder = normalizeFolder(this.settings.hexFolder);
|
||||
return folder ? `${folder}/${x}_${y}.md` : `${x}_${y}.md`;
|
||||
}
|
||||
}
|
||||
|
||||
class HexMapView extends ItemView {
|
||||
static readonly GRID_COLS = 20;
|
||||
static readonly GRID_ROWS = 16;
|
||||
plugin: DuckmagePlugin;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: DuckmagePlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return VIEW_TYPE_HEX_MAP;
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return "Hex map";
|
||||
}
|
||||
|
||||
async onOpen(): Promise<void> {
|
||||
this.renderGrid();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param terrainOverrides - If set, use these terrain keys for the given paths instead of cache (for immediate UI update after set/clear).
|
||||
*/
|
||||
private renderGrid(terrainOverrides?: Map<string, string | null>): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.addClasses(["duckmage-hex-map-container"]);
|
||||
const gap = this.plugin.settings.hexGap?.trim() || "0.15";
|
||||
contentEl.style.setProperty("--duckmage-hex-gap", /^\d*\.?\d+$/.test(gap) ? `${gap}em` : gap);
|
||||
|
||||
const folder = normalizeFolder(this.plugin.settings.hexFolder);
|
||||
const palette = this.plugin.settings.terrainPalette ?? [];
|
||||
for (let y = 0; y < HexMapView.GRID_ROWS; y++) {
|
||||
const rowEl = contentEl.createDiv({ cls: "duckmage-hex-row" });
|
||||
for (let x = 0; x < HexMapView.GRID_COLS; x++) {
|
||||
const path = folder ? `${folder}/${x}_${y}.md` : `${x}_${y}.md`;
|
||||
const file = this.app.vault.getAbstractFileByPath(path);
|
||||
const exists = file instanceof TFile;
|
||||
const terrainKey =
|
||||
terrainOverrides?.has(path) ? terrainOverrides.get(path)! : getTerrainFromFile(this.app, path);
|
||||
const terrainColor =
|
||||
terrainKey != null ? palette.find((p) => p.name === terrainKey)?.color : undefined;
|
||||
|
||||
const hexEl = rowEl.createDiv({
|
||||
cls: `duckmage-hex ${exists ? "duckmage-hex-exists" : ""}`,
|
||||
attr: { "data-x": String(x), "data-y": String(y) },
|
||||
});
|
||||
hexEl.tabIndex = -1;
|
||||
if (terrainColor) hexEl.style.backgroundColor = terrainColor;
|
||||
hexEl.createSpan({ cls: "duckmage-hex-label", text: `${x},${y}` });
|
||||
if (exists) hexEl.createSpan({ cls: "duckmage-hex-dot" });
|
||||
|
||||
hexEl.addEventListener("click", () => this.onHexClick(x, y));
|
||||
hexEl.addEventListener("contextmenu", (e) => this.onHexContextMenu(e, x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onHexContextMenu(evt: MouseEvent, x: number, y: number): void {
|
||||
evt.preventDefault();
|
||||
const path = this.plugin.hexPath(x, y);
|
||||
const palette = this.plugin.settings.terrainPalette ?? [];
|
||||
const hexEl = evt.currentTarget as HTMLElement;
|
||||
|
||||
const clearHighlight = () => {
|
||||
hexEl.blur();
|
||||
window.getSelection()?.removeAllRanges();
|
||||
};
|
||||
|
||||
const menu = new Menu();
|
||||
for (const entry of palette) {
|
||||
menu.addItem((item) =>
|
||||
item
|
||||
.setTitle(entry.name)
|
||||
.setIcon("palette")
|
||||
.onClick(async () => {
|
||||
clearHighlight();
|
||||
let file = this.app.vault.getAbstractFileByPath(path);
|
||||
if (!(file instanceof TFile)) {
|
||||
file = await this.createHexNote(x, y, path);
|
||||
if (!file) return;
|
||||
}
|
||||
await setTerrainInFile(this.app, path, entry.name);
|
||||
this.renderGrid(new Map([[path, entry.name]]));
|
||||
})
|
||||
);
|
||||
}
|
||||
const fileExists = this.app.vault.getAbstractFileByPath(path) instanceof TFile;
|
||||
if (fileExists) {
|
||||
menu.addSeparator();
|
||||
menu.addItem((item) =>
|
||||
item
|
||||
.setTitle("Clear color")
|
||||
.onClick(async () => {
|
||||
clearHighlight();
|
||||
await setTerrainInFile(this.app, path, null);
|
||||
this.renderGrid(new Map([[path, null]]));
|
||||
})
|
||||
);
|
||||
}
|
||||
menu.showAtMouseEvent(evt);
|
||||
}
|
||||
|
||||
private async onHexClick(x: number, y: number): Promise<void> {
|
||||
const path = this.plugin.hexPath(x, y);
|
||||
const abstract = this.app.vault.getAbstractFileByPath(path);
|
||||
let fileToOpen: TFile | null = abstract instanceof TFile ? abstract : null;
|
||||
|
||||
if (!fileToOpen) {
|
||||
const created = await this.createHexNote(x, y, path);
|
||||
if (created) {
|
||||
fileToOpen = created;
|
||||
this.renderGrid();
|
||||
} else return;
|
||||
}
|
||||
|
||||
const leaf = this.app.workspace.getLeaf(false);
|
||||
await leaf.openFile(fileToOpen);
|
||||
}
|
||||
|
||||
private async createHexNote(x: number, y: number, path: string): Promise<TFile | null> {
|
||||
const templatePath = (this.plugin.settings.templatePath ?? "").replace(/^\/+|\/+$/g, "");
|
||||
const DEFAULT_TEMPLATE = `# Hex {{x}}, {{y}}\n\n`;
|
||||
|
||||
let content: string;
|
||||
if (templatePath) {
|
||||
try {
|
||||
content = await this.app.vault.adapter.read(templatePath);
|
||||
} catch {
|
||||
new Notice("Template not found: " + templatePath);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
content = DEFAULT_TEMPLATE;
|
||||
}
|
||||
|
||||
content = content
|
||||
.replace(/\{\{x\}\}/g, String(x))
|
||||
.replace(/\{\{y\}\}/g, String(y))
|
||||
.replace(/\{\{title\}\}/g, `Hex ${x}, ${y}`);
|
||||
|
||||
const folder = normalizeFolder(this.plugin.settings.hexFolder);
|
||||
if (folder) {
|
||||
const folderObj = this.app.vault.getAbstractFileByPath(folder);
|
||||
if (!folderObj) await this.app.vault.createFolder(folder);
|
||||
}
|
||||
|
||||
try {
|
||||
const created = await this.app.vault.create(path, content);
|
||||
return created;
|
||||
} catch (e) {
|
||||
new Notice("Could not create note: " + (e instanceof Error ? e.message : String(e)));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SampleModal extends Modal {
|
||||
constructor(app: App) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.setText("Sample modal — edit main.ts to customize.");
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
class DuckmageSettingTab extends PluginSettingTab {
|
||||
plugin: DuckmagePlugin;
|
||||
|
||||
constructor(app: App, plugin: DuckmagePlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Hex notes folder")
|
||||
.setDesc("Vault-relative path where hex notes (x_y.md) are stored.")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("world/hexes")
|
||||
.setValue(this.plugin.settings.hexFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.hexFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Template path")
|
||||
.setDesc("Vault-relative path to the template file for new hex notes. Use {{x}}, {{y}}, {{title}} as placeholders.")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("templates/hex.md")
|
||||
.setValue(this.plugin.settings.templatePath)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.templatePath = (value ?? "").replace(/^\/+|\/+$/g, "");
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Hex cell spacing")
|
||||
.setDesc("Gap between hex cells (e.g. 0.15 for a small gap, 0 for none). Use a number for em units, or add 'em'/'px' (e.g. 0.2em).")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("0.15")
|
||||
.setValue(this.plugin.settings.hexGap ?? "0.15")
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.hexGap = (value ?? "0.15").trim() || "0";
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
containerEl.createEl("h3", { text: "Terrain palette" });
|
||||
const paletteDesc = containerEl.createDiv({ cls: "setting-item-description" });
|
||||
paletteDesc.setText("Right-click a hex to set its color. Add or edit terrain types and colors below.");
|
||||
paletteDesc.style.marginBottom = "0.75em";
|
||||
|
||||
const listEl = containerEl.createDiv({ cls: "duckmage-palette-list" });
|
||||
const palette = this.plugin.settings.terrainPalette ?? [];
|
||||
for (let i = 0; i < palette.length; i++) {
|
||||
const entry = palette[i];
|
||||
const itemEl = listEl.createDiv({ cls: "duckmage-palette-item" });
|
||||
new Setting(itemEl)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("Name (e.g. mountain)")
|
||||
.setValue(entry.name)
|
||||
.onChange(async (value) => {
|
||||
entry.name = (value ?? "").trim() || entry.name;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
)
|
||||
.addColorPicker((color) => {
|
||||
color.setValue(entry.color).onChange(async (value) => {
|
||||
entry.color = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
})
|
||||
.addExtraButton((btn) =>
|
||||
btn.setIcon("trash-2").onClick(async () => {
|
||||
this.plugin.settings.terrainPalette.splice(i, 1);
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
new Setting(containerEl).addButton((btn) =>
|
||||
btn.setButtonText("Add terrain color").onClick(async () => {
|
||||
this.plugin.settings.terrainPalette.push({ name: "New", color: "#888888" });
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
9
manifest.json
Normal file
9
manifest.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "duckmage-plugin",
|
||||
"name": "Duckmage Plugin",
|
||||
"version": "0.1.0",
|
||||
"minAppVersion": "1.0.0",
|
||||
"description": "A template Obsidian plugin for the Duckmage vault.",
|
||||
"author": "Your Name",
|
||||
"authorUrl": ""
|
||||
}
|
||||
1117
package-lock.json
generated
Normal file
1117
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
26
package.json
Normal file
26
package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "duckmage-plugin",
|
||||
"version": "0.1.0",
|
||||
"description": "A template Obsidian plugin for the Duckmage vault.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
},
|
||||
"keywords": ["obsidian", "plugin"],
|
||||
"author": "Your Name",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@codemirror/lang-markdown": "^6.2.2",
|
||||
"@codemirror/language-data": "^6.5.1",
|
||||
"@codemirror/state": "^6.4.1",
|
||||
"@codemirror/view": "^6.25.1",
|
||||
"@types/node": "^22.10.1",
|
||||
"builtin-modules": "^3.3.0",
|
||||
"esbuild": "0.24.0",
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.8.1",
|
||||
"typescript": "5.6.3"
|
||||
}
|
||||
}
|
||||
85
styles.css
Normal file
85
styles.css
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
/* Duckmage plugin styles */
|
||||
|
||||
/* Hex map view */
|
||||
.duckmage-hex-map-container {
|
||||
padding: 1em;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.duckmage-hex-row {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
/* Flat-top hex: vertical step is 3/4 of hex height, so pull next row up by 1/4 of full height = half of --hex-size */
|
||||
margin-bottom: calc(var(--hex-size, 2.2em) * -0.5);
|
||||
}
|
||||
|
||||
.duckmage-hex-row:nth-child(even) {
|
||||
/* Offset even rows by exactly half hex width so they tessellate */
|
||||
margin-left: calc(var(--hex-size, 2.2em) * 1.732 / 2);
|
||||
}
|
||||
|
||||
.duckmage-hex:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.duckmage-hex {
|
||||
--hex-size: 2.2em;
|
||||
/* Flat-top regular hex: width = sqrt(3)*size, height = 2*size */
|
||||
width: calc(var(--hex-size) * 1.732);
|
||||
height: calc(var(--hex-size) * 2);
|
||||
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
|
||||
background: var(--background-secondary);
|
||||
border: 2px solid var(--background-modifier-border);
|
||||
margin: var(--duckmage-hex-gap, 0.15em);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: background-color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.duckmage-hex:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
border-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
/* Has-note state: accent border and dot only (no purple fill so terrain/clear stays clear) */
|
||||
.duckmage-hex-exists {
|
||||
border-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.duckmage-hex-exists:hover {
|
||||
border-color: var(--interactive-accent);
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.duckmage-hex-label {
|
||||
font-size: 0.65em;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.duckmage-hex-dot {
|
||||
position: absolute;
|
||||
bottom: 0.35em;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: var(--interactive-accent);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Terrain-colored hex: keep label readable (text shadow if needed) */
|
||||
.duckmage-hex[style*="background-color"] .duckmage-hex-label {
|
||||
text-shadow: 0 0 2px var(--background-primary), 0 0 4px var(--background-primary);
|
||||
}
|
||||
|
||||
/* Settings: terrain palette list */
|
||||
.duckmage-palette-list .duckmage-palette-item {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": ["DOM", "ES5", "ES6", "ES7"]
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
||||
16
version-bump.mjs
Normal file
16
version-bump.mjs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { readFileSync, writeFileSync } from "fs";
|
||||
|
||||
const targetVersion = process.env.npm_package_version;
|
||||
|
||||
const manifestPath = "manifest.json";
|
||||
const versionsPath = "versions.json";
|
||||
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
||||
manifest.version = targetVersion;
|
||||
writeFileSync(manifestPath, JSON.stringify(manifest, null, "\t"));
|
||||
|
||||
const versions = JSON.parse(readFileSync(versionsPath, "utf8"));
|
||||
versions[targetVersion] = manifest.minAppVersion;
|
||||
writeFileSync(versionsPath, JSON.stringify(versions, null, "\t"));
|
||||
|
||||
console.log("Bumped version to", targetVersion);
|
||||
3
versions.json
Normal file
3
versions.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"0.1.0": "1.0.0"
|
||||
}
|
||||
Loading…
Reference in a new issue