mirror of
https://github.com/sirwanafifi/inkplane.git
synced 2026-07-22 08:32:24 +00:00
fix: address Obsidian plugin review recommendations
This commit is contained in:
parent
9bcfe75160
commit
e769ebc8bd
9 changed files with 338 additions and 230 deletions
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
|
|
@ -6,7 +6,10 @@ on:
|
|||
- "[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
permissions:
|
||||
artifact-metadata: write
|
||||
attestations: write
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
|
|
@ -25,6 +28,13 @@ jobs:
|
|||
run: npm run check
|
||||
- name: Validate tag
|
||||
run: node scripts/check-release.mjs "$GITHUB_REF_NAME"
|
||||
- name: Attest release assets
|
||||
uses: actions/attest@v4
|
||||
with:
|
||||
subject-path: |
|
||||
main.js
|
||||
manifest.json
|
||||
styles.css
|
||||
- name: Publish release assets
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import esbuild from "esbuild";
|
||||
import { builtinModules } from "node:module";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
|
||||
const production = process.argv[2] === "production";
|
||||
const builtins = [...builtinModules, ...builtinModules.map((name) => `node:${name}`)];
|
||||
|
||||
const context = await esbuild.context({
|
||||
banner: { js: "/* Inkplane for Obsidian */" },
|
||||
|
|
|
|||
14
package-lock.json
generated
14
package-lock.json
generated
|
|
@ -13,7 +13,6 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.0",
|
||||
"builtin-modules": "^5.0.0",
|
||||
"esbuild": "^0.28.1",
|
||||
"obsidian": "^1.13.1",
|
||||
"tslib": "^2.8.1",
|
||||
|
|
@ -1366,19 +1365,6 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/builtin-modules": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.3.0.tgz",
|
||||
"integrity": "sha512-hMQUl2bUFG339QygPM97E+mc8OY1IAchORZxm4a/frcYwKzozMzRVDBwHW0NjOqGElLm2O37AVQE8ikxlZHrMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.0",
|
||||
"builtin-modules": "^5.0.0",
|
||||
"esbuild": "^0.28.1",
|
||||
"obsidian": "^1.13.1",
|
||||
"tslib": "^2.8.1",
|
||||
|
|
|
|||
|
|
@ -114,36 +114,28 @@ export class DrawingSurface {
|
|||
private readonly callbacks: DrawingSurfaceCallbacks,
|
||||
drawing?: InkDrawing
|
||||
) {
|
||||
const doc = host.ownerDocument;
|
||||
this.drawing = cloneDrawing(drawing ?? emptyDrawing(
|
||||
store.settings.defaultCanvasWidth,
|
||||
store.settings.defaultCanvasHeight
|
||||
));
|
||||
|
||||
this.root = doc.createElement("div");
|
||||
this.root.className = "ink-canvas-view";
|
||||
this.root.tabIndex = 0;
|
||||
|
||||
this.viewport = doc.createElement("div");
|
||||
this.viewport.className = "ink-canvas-viewport";
|
||||
this.viewport.setAttribute("aria-label", "Ink drawing canvas");
|
||||
this.root.appendChild(this.viewport);
|
||||
|
||||
this.dryCanvas = doc.createElement("canvas");
|
||||
this.dryCanvas.className = "ink-canvas-layer ink-canvas-layer-dry";
|
||||
this.dryCanvas.setAttribute("aria-hidden", "true");
|
||||
this.viewport.appendChild(this.dryCanvas);
|
||||
|
||||
this.wetCanvas = doc.createElement("canvas");
|
||||
this.wetCanvas.className = "ink-canvas-layer";
|
||||
this.wetCanvas.setAttribute("aria-hidden", "true");
|
||||
this.viewport.appendChild(this.wetCanvas);
|
||||
|
||||
this.toolbar = doc.createElement("div");
|
||||
this.toolbar.className = "ink-layer-toolbar";
|
||||
this.toolbar.setAttribute("role", "toolbar");
|
||||
this.toolbar.setAttribute("aria-label", "Ink tools");
|
||||
this.root.appendChild(this.toolbar);
|
||||
this.root = host.createDiv({ cls: "ink-canvas-view", attr: { tabindex: "0" } });
|
||||
this.viewport = this.root.createDiv({
|
||||
cls: "ink-canvas-viewport",
|
||||
attr: { "aria-label": "Ink drawing canvas" }
|
||||
});
|
||||
this.dryCanvas = this.viewport.createEl("canvas", {
|
||||
cls: "ink-canvas-layer ink-canvas-layer-dry",
|
||||
attr: { "aria-hidden": "true" }
|
||||
});
|
||||
this.wetCanvas = this.viewport.createEl("canvas", {
|
||||
cls: "ink-canvas-layer",
|
||||
attr: { "aria-hidden": "true" }
|
||||
});
|
||||
this.toolbar = this.root.createDiv({
|
||||
cls: "ink-layer-toolbar",
|
||||
attr: { role: "toolbar", "aria-label": "Ink tools" }
|
||||
});
|
||||
|
||||
this.startToolbarGroup("History", "ink-toolbar-history");
|
||||
this.undoButton = this.addActionButton("undo-2", "Undo ink", () => this.undo());
|
||||
|
|
@ -174,27 +166,18 @@ export class DrawingSurface {
|
|||
this.zoomButton = this.addTextButton("100%", "Fit drawing", () => this.fitToView());
|
||||
this.addActionButton("plus", "Zoom in", () => this.zoomBy(1.25));
|
||||
|
||||
this.emptyState = doc.createElement("div");
|
||||
this.emptyState.className = "ink-canvas-empty-state";
|
||||
this.emptyState.setAttribute("aria-hidden", "true");
|
||||
const emptyIcon = doc.createElement("span");
|
||||
emptyIcon.className = "ink-empty-icon";
|
||||
this.emptyState = this.viewport.createDiv({
|
||||
cls: "ink-canvas-empty-state",
|
||||
attr: { "aria-hidden": "true" }
|
||||
});
|
||||
const emptyIcon = this.emptyState.createSpan({ cls: "ink-empty-icon" });
|
||||
setIcon(emptyIcon, "pen-tool");
|
||||
const emptyTitle = doc.createElement("strong");
|
||||
emptyTitle.textContent = "Draw with Apple Pencil";
|
||||
const emptyHint = doc.createElement("span");
|
||||
emptyHint.textContent = "One finger pans · two fingers zoom";
|
||||
this.emptyState.append(emptyIcon, emptyTitle, emptyHint);
|
||||
this.viewport.appendChild(this.emptyState);
|
||||
this.emptyState.createEl("strong", { text: "Draw with Apple Pencil" });
|
||||
this.emptyState.createSpan({ text: "One finger pans · two fingers zoom" });
|
||||
|
||||
const status = doc.createElement("div");
|
||||
status.className = "ink-canvas-status";
|
||||
this.statusLabel = doc.createElement("span");
|
||||
this.statusLabel.className = "ink-canvas-status-tool";
|
||||
this.statusMeta = doc.createElement("span");
|
||||
this.statusMeta.className = "ink-canvas-status-meta";
|
||||
status.append(this.statusLabel, this.statusMeta);
|
||||
this.root.appendChild(status);
|
||||
const status = this.root.createDiv({ cls: "ink-canvas-status" });
|
||||
this.statusLabel = status.createSpan({ cls: "ink-canvas-status-tool" });
|
||||
this.statusMeta = status.createSpan({ cls: "ink-canvas-status-meta" });
|
||||
|
||||
this.toolInspector = new ToolInspector(
|
||||
this.root,
|
||||
|
|
@ -366,17 +349,13 @@ export class DrawingSurface {
|
|||
const button = this.addActionButton(icon, label, () => this.setTool(tool));
|
||||
button.dataset.tool = tool;
|
||||
button.setAttribute("aria-pressed", "false");
|
||||
const indicator = this.toolbar.ownerDocument.createElement("span");
|
||||
indicator.className = "ink-tool-selection-indicator";
|
||||
indicator.setAttribute("aria-hidden", "true");
|
||||
button.appendChild(indicator);
|
||||
button.createSpan({ cls: "ink-tool-selection-indicator", attr: { "aria-hidden": "true" } });
|
||||
this.toolButtons.set(tool, button);
|
||||
}
|
||||
|
||||
private addQuickColorButton(index: number): HTMLButtonElement {
|
||||
const button = this.toolbar.ownerDocument.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "ink-quick-color";
|
||||
const parent = this.toolbarGroup ?? this.toolbar;
|
||||
const button = parent.createEl("button", { cls: "ink-quick-color", attr: { type: "button" } });
|
||||
button.dataset.colorIndex = String(index);
|
||||
button.setAttribute("aria-pressed", "false");
|
||||
button.addEventListener("click", (event) => {
|
||||
|
|
@ -391,7 +370,6 @@ export class DrawingSurface {
|
|||
this.updateToolbar();
|
||||
this.root.focus({ preventScroll: true });
|
||||
});
|
||||
(this.toolbarGroup ?? this.toolbar).appendChild(button);
|
||||
return button;
|
||||
}
|
||||
|
||||
|
|
@ -400,11 +378,11 @@ export class DrawingSurface {
|
|||
label: string,
|
||||
action: (event: MouseEvent) => void
|
||||
): HTMLButtonElement {
|
||||
const button = this.toolbar.ownerDocument.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "ink-layer-tool clickable-icon";
|
||||
button.setAttribute("aria-label", label);
|
||||
button.setAttribute("title", label);
|
||||
const parent = this.toolbarGroup ?? this.toolbar;
|
||||
const button = parent.createEl("button", {
|
||||
cls: "ink-layer-tool clickable-icon",
|
||||
attr: { type: "button", "aria-label": label, title: label }
|
||||
});
|
||||
button.dataset.inkTooltip = label;
|
||||
try {
|
||||
setIcon(button, icon);
|
||||
|
|
@ -412,43 +390,40 @@ export class DrawingSurface {
|
|||
// Older mobile Obsidian builds can lack newer Lucide icon identifiers.
|
||||
}
|
||||
if (!button.querySelector("svg")) {
|
||||
const fallback = this.toolbar.ownerDocument.createElement("span");
|
||||
fallback.className = "ink-tool-glyph";
|
||||
fallback.textContent = fallbackIcon(icon);
|
||||
fallback.setAttribute("aria-hidden", "true");
|
||||
button.appendChild(fallback);
|
||||
button.createSpan({
|
||||
cls: "ink-tool-glyph",
|
||||
text: fallbackIcon(icon),
|
||||
attr: { "aria-hidden": "true" }
|
||||
});
|
||||
}
|
||||
button.addEventListener("click", (event) => {
|
||||
consumeEvent(event);
|
||||
action(event);
|
||||
this.root.focus({ preventScroll: true });
|
||||
});
|
||||
(this.toolbarGroup ?? this.toolbar).appendChild(button);
|
||||
return button;
|
||||
}
|
||||
|
||||
private addTextButton(text: string, label: string, action: () => void): HTMLButtonElement {
|
||||
const button = this.toolbar.ownerDocument.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "ink-layer-tool ink-layer-zoom";
|
||||
button.textContent = text;
|
||||
button.setAttribute("aria-label", label);
|
||||
button.setAttribute("title", label);
|
||||
const parent = this.toolbarGroup ?? this.toolbar;
|
||||
const button = parent.createEl("button", {
|
||||
cls: "ink-layer-tool ink-layer-zoom",
|
||||
text,
|
||||
attr: { type: "button", "aria-label": label, title: label }
|
||||
});
|
||||
button.addEventListener("click", (event) => {
|
||||
consumeEvent(event);
|
||||
action();
|
||||
this.root.focus({ preventScroll: true });
|
||||
});
|
||||
(this.toolbarGroup ?? this.toolbar).appendChild(button);
|
||||
return button;
|
||||
}
|
||||
|
||||
private startToolbarGroup(label: string, className = ""): void {
|
||||
const group = this.toolbar.ownerDocument.createElement("div");
|
||||
group.className = `ink-toolbar-group ${className}`.trim();
|
||||
group.setAttribute("role", "group");
|
||||
group.setAttribute("aria-label", label);
|
||||
this.toolbar.appendChild(group);
|
||||
const group = this.toolbar.createDiv({
|
||||
cls: `ink-toolbar-group ${className}`.trim(),
|
||||
attr: { role: "group", "aria-label": label }
|
||||
});
|
||||
this.toolbarGroup = group;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,11 +19,11 @@ export class InkEmbedManager {
|
|||
this.observer = new MutationObserver((mutations) => {
|
||||
this.removeDetachedPreviews();
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.target instanceof HTMLElement) {
|
||||
if (mutation.target.instanceOf(HTMLElement)) {
|
||||
this.scan(mutation.target, this.sourcePathFor(mutation.target));
|
||||
}
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (node instanceof HTMLElement) this.scan(node, this.sourcePathFor(node));
|
||||
if (node.instanceOf(HTMLElement)) this.scan(node, this.sourcePathFor(node));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -112,33 +112,26 @@ class InkEmbedPreview {
|
|||
readonly file: TFile,
|
||||
private readonly getSettings: () => InkSettings
|
||||
) {
|
||||
const doc = container.ownerDocument;
|
||||
this.shell = doc.createElement("div");
|
||||
this.shell.className = "ink-embed-preview";
|
||||
this.shell.tabIndex = 0;
|
||||
this.shell = container.createDiv({ cls: "ink-embed-preview", attr: { tabindex: "0" } });
|
||||
this.dryCanvas = this.shell.createEl("canvas", {
|
||||
cls: "ink-embed-layer",
|
||||
attr: { "aria-hidden": "true" }
|
||||
});
|
||||
this.wetCanvas = this.shell.createEl("canvas", {
|
||||
cls: "ink-embed-layer",
|
||||
attr: { "aria-hidden": "true" }
|
||||
});
|
||||
|
||||
this.dryCanvas = doc.createElement("canvas");
|
||||
this.dryCanvas.className = "ink-embed-layer";
|
||||
this.dryCanvas.setAttribute("aria-hidden", "true");
|
||||
this.shell.appendChild(this.dryCanvas);
|
||||
|
||||
this.wetCanvas = doc.createElement("canvas");
|
||||
this.wetCanvas.className = "ink-embed-layer";
|
||||
this.wetCanvas.setAttribute("aria-hidden", "true");
|
||||
this.shell.appendChild(this.wetCanvas);
|
||||
|
||||
const openButton = doc.createElement("button");
|
||||
openButton.type = "button";
|
||||
openButton.className = "ink-embed-open clickable-icon";
|
||||
openButton.setAttribute("aria-label", `Open ${file.basename}`);
|
||||
openButton.setAttribute("title", "Open drawing");
|
||||
const openButton = this.shell.createEl("button", {
|
||||
cls: "ink-embed-open clickable-icon",
|
||||
attr: { type: "button", "aria-label": `Open ${file.basename}`, title: "Open drawing" }
|
||||
});
|
||||
setIcon(openButton, "maximize-2");
|
||||
openButton.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void this.repository.open(this.file);
|
||||
});
|
||||
this.shell.appendChild(openButton);
|
||||
this.shell.addEventListener("dblclick", (event) => {
|
||||
event.preventDefault();
|
||||
void this.repository.open(this.file);
|
||||
|
|
|
|||
30
src/main.ts
30
src/main.ts
|
|
@ -75,9 +75,13 @@ export default class InkLayerPlugin
|
|||
this.registerDomEvent(window, "pagehide", () => void this.flushOpenDrawings());
|
||||
}
|
||||
|
||||
async onunload(): Promise<void> {
|
||||
await this.flushOpenDrawings();
|
||||
onunload(): void {
|
||||
this.embedManager.destroy();
|
||||
void this.persistBeforeUnload();
|
||||
}
|
||||
|
||||
private async persistBeforeUnload(): Promise<void> {
|
||||
await this.flushOpenDrawings();
|
||||
await this.store.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -317,18 +321,24 @@ class ClearInkModal extends Modal {
|
|||
|
||||
onOpen(): void {
|
||||
this.titleEl.textContent = "Clear this drawing?";
|
||||
const description = this.contentEl.ownerDocument.createElement("p");
|
||||
description.textContent = "This removes every stroke. You can undo it while the drawing remains open.";
|
||||
this.contentEl.appendChild(description);
|
||||
this.contentEl.createEl("p", {
|
||||
text: "This removes every stroke. You can undo it while the drawing remains open."
|
||||
});
|
||||
new Setting(this.contentEl)
|
||||
.addButton((button) => button.setButtonText("Cancel").onClick(() => this.close()))
|
||||
.addButton((button) => button
|
||||
.setButtonText("Clear drawing")
|
||||
.setWarning()
|
||||
.onClick(() => {
|
||||
.addButton((button) => {
|
||||
button.setButtonText("Clear drawing");
|
||||
if (typeof button.setDestructive === "function") {
|
||||
button.setDestructive().setCta();
|
||||
} else {
|
||||
// Preserve destructive styling on supported Obsidian versions before 1.13.0.
|
||||
button.buttonEl.addClasses(["mod-warning", "mod-cta"]);
|
||||
}
|
||||
button.onClick(() => {
|
||||
this.onConfirm();
|
||||
this.close();
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
|
|
|
|||
161
src/settings.ts
161
src/settings.ts
|
|
@ -1,4 +1,10 @@
|
|||
import { Plugin, PluginSettingTab, Setting, type App } from "obsidian";
|
||||
import {
|
||||
Plugin,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
type App,
|
||||
type SettingDefinitionItem
|
||||
} from "obsidian";
|
||||
import type { InkStore } from "./storage";
|
||||
import type { InkSettings } from "./types";
|
||||
|
||||
|
|
@ -7,12 +13,155 @@ export interface InkSettingsHost {
|
|||
refreshInkUI(): void;
|
||||
}
|
||||
|
||||
type InkSettingKey = keyof InkSettings | "matchPenToTheme";
|
||||
|
||||
export class InkSettingTab extends PluginSettingTab {
|
||||
constructor(app: App, private readonly pluginHost: Plugin & InkSettingsHost) {
|
||||
super(app, pluginHost);
|
||||
}
|
||||
|
||||
getSettingDefinitions(): SettingDefinitionItem<InkSettingKey>[] {
|
||||
return [
|
||||
{
|
||||
type: "group",
|
||||
heading: "Drawing files",
|
||||
items: [
|
||||
{
|
||||
name: "Drawing folder",
|
||||
desc: "New .inklayer canvases are stored here inside the vault. Leave blank for the vault root.",
|
||||
control: { type: "folder", key: "drawingFolder", placeholder: "Inkplane" }
|
||||
},
|
||||
{
|
||||
name: "Embed width",
|
||||
desc: "Default width for previews inserted into notes.",
|
||||
control: { type: "number", key: "defaultEmbedWidth", min: 240, max: 2400, step: 1 }
|
||||
},
|
||||
{
|
||||
name: "Embed height",
|
||||
desc: "Default height for previews inserted into notes.",
|
||||
control: { type: "number", key: "defaultEmbedHeight", min: 180, max: 1800, step: 1 }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "group",
|
||||
heading: "Ink appearance",
|
||||
items: [
|
||||
{
|
||||
name: "Match pen to the theme",
|
||||
desc: "Use Obsidian’s current text color for new pen strokes.",
|
||||
control: { type: "toggle", key: "matchPenToTheme" }
|
||||
},
|
||||
{
|
||||
name: "Pen color",
|
||||
desc: "Color for new pen strokes when theme matching is disabled.",
|
||||
control: {
|
||||
type: "color",
|
||||
key: "penColor",
|
||||
disabled: () => this.pluginHost.store.settings.penColor === "adaptive"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Pen width",
|
||||
desc: "Base width. Pencil pressure varies the final width.",
|
||||
control: { type: "slider", key: "penWidth", min: 1, max: 12, step: 0.2 }
|
||||
},
|
||||
{
|
||||
name: "Highlighter color",
|
||||
control: { type: "color", key: "highlighterColor" }
|
||||
},
|
||||
{
|
||||
name: "Highlighter width",
|
||||
control: { type: "slider", key: "highlighterWidth", min: 6, max: 40, step: 1 }
|
||||
},
|
||||
{
|
||||
name: "Eraser size",
|
||||
desc: "The eraser removes only the area of a stroke that it touches.",
|
||||
control: { type: "slider", key: "eraserWidth", min: 8, max: 56, step: 2 }
|
||||
},
|
||||
{
|
||||
name: "Pressure response",
|
||||
desc: "Zero gives a uniform line; one follows the full pressure range reported by the pen.",
|
||||
control: { type: "slider", key: "pressureSensitivity", min: 0, max: 1, step: 0.05 }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "group",
|
||||
heading: "Input and navigation",
|
||||
items: [
|
||||
{
|
||||
name: "Palm rejection",
|
||||
desc: "Ignore touch contacts while a pen is active or was just detected nearby.",
|
||||
control: { type: "toggle", key: "palmRejection" }
|
||||
},
|
||||
{
|
||||
name: "Draw with a finger",
|
||||
desc: "Off by default: one finger pans and two fingers pinch-zoom while Apple Pencil draws.",
|
||||
control: { type: "toggle", key: "allowFingerDrawing" }
|
||||
},
|
||||
{
|
||||
name: "Draw with a mouse",
|
||||
desc: "Useful for desktop editing and testing without a pen.",
|
||||
control: { type: "toggle", key: "allowMouseDrawing" }
|
||||
},
|
||||
{
|
||||
name: "Toolbar position",
|
||||
desc: "The bottom position respects the iPad safe area.",
|
||||
control: {
|
||||
type: "dropdown",
|
||||
key: "toolbarPosition",
|
||||
options: { top: "Top", bottom: "Bottom" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
getControlValue(key: string): unknown {
|
||||
const settings = this.pluginHost.store.settings;
|
||||
if (key === "matchPenToTheme") return settings.penColor === "adaptive";
|
||||
if (key === "penColor" && settings.penColor === "adaptive") return "#1f2937";
|
||||
return settings[key as keyof InkSettings];
|
||||
}
|
||||
|
||||
setControlValue(key: string, value: unknown): void {
|
||||
if (key === "matchPenToTheme") {
|
||||
if (typeof value !== "boolean") return;
|
||||
this.applyPatch({ penColor: value ? "adaptive" : "#1f2937" });
|
||||
this.update();
|
||||
return;
|
||||
}
|
||||
if (key === "drawingFolder" || key === "penColor" || key === "highlighterColor") {
|
||||
if (typeof value !== "string") return;
|
||||
this.applyPatch({ [key]: key === "drawingFolder" ? value.trim() : value });
|
||||
return;
|
||||
}
|
||||
if (key === "palmRejection" || key === "allowFingerDrawing" || key === "allowMouseDrawing") {
|
||||
if (typeof value !== "boolean") return;
|
||||
this.applyPatch({ [key]: value });
|
||||
return;
|
||||
}
|
||||
if (key === "toolbarPosition") {
|
||||
this.applyPatch({ toolbarPosition: value === "bottom" ? "bottom" : "top" });
|
||||
return;
|
||||
}
|
||||
const numberValue = typeof value === "number" ? value : Number(value);
|
||||
if (!Number.isFinite(numberValue)) return;
|
||||
if (key === "defaultEmbedWidth") this.applyPatch({ defaultEmbedWidth: clampRound(numberValue, 240, 2400) });
|
||||
else if (key === "defaultEmbedHeight") this.applyPatch({ defaultEmbedHeight: clampRound(numberValue, 180, 1800) });
|
||||
else if (key === "penWidth") this.applyPatch({ penWidth: clamp(numberValue, 1, 12) });
|
||||
else if (key === "highlighterWidth") this.applyPatch({ highlighterWidth: clamp(numberValue, 6, 40) });
|
||||
else if (key === "eraserWidth") this.applyPatch({ eraserWidth: clamp(numberValue, 8, 56) });
|
||||
else if (key === "pressureSensitivity") this.applyPatch({ pressureSensitivity: clamp(numberValue, 0, 1) });
|
||||
}
|
||||
|
||||
display(): void {
|
||||
this.renderLegacy();
|
||||
}
|
||||
|
||||
private renderLegacy(): void {
|
||||
this.containerEl.replaceChildren();
|
||||
const settings = this.pluginHost.store.settings;
|
||||
|
||||
|
|
@ -50,7 +199,7 @@ export class InkSettingTab extends PluginSettingTab {
|
|||
.setDesc("Use Obsidian’s current text color for new pen strokes.")
|
||||
.addToggle((toggle) => toggle.setValue(settings.penColor === "adaptive").onChange((enabled) => {
|
||||
this.applyPatch({ penColor: enabled ? "adaptive" : "#1f2937" });
|
||||
this.display();
|
||||
this.renderLegacy();
|
||||
}));
|
||||
|
||||
new Setting(this.containerEl)
|
||||
|
|
@ -161,3 +310,11 @@ export class InkSettingTab extends PluginSettingTab {
|
|||
this.pluginHost.refreshInkUI();
|
||||
}
|
||||
}
|
||||
|
||||
function clamp(value: number, minimum: number, maximum: number): number {
|
||||
return Math.min(maximum, Math.max(minimum, value));
|
||||
}
|
||||
|
||||
function clampRound(value: number, minimum: number, maximum: number): number {
|
||||
return Math.round(clamp(value, minimum, maximum));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,13 +91,15 @@ export class ToolInspector {
|
|||
private readonly onOpenChange?: (open: boolean) => void,
|
||||
private readonly anchor?: HTMLElement
|
||||
) {
|
||||
this.element = parent.ownerDocument.createElement("div");
|
||||
this.element.className = "ink-tool-inspector";
|
||||
this.element.setAttribute("role", "dialog");
|
||||
this.element.setAttribute("aria-label", "Tool settings");
|
||||
this.element.setAttribute("aria-hidden", "true");
|
||||
this.element.tabIndex = -1;
|
||||
this.parent.appendChild(this.element);
|
||||
this.element = parent.createDiv({
|
||||
cls: "ink-tool-inspector",
|
||||
attr: {
|
||||
role: "dialog",
|
||||
"aria-label": "Tool settings",
|
||||
"aria-hidden": "true",
|
||||
tabindex: "-1"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get isOpen(): boolean {
|
||||
|
|
@ -182,67 +184,52 @@ export class ToolInspector {
|
|||
this.element.replaceChildren();
|
||||
this.element.setAttribute("aria-label", `${design.label} settings`);
|
||||
|
||||
const header = doc.createElement("div");
|
||||
header.className = "ink-inspector-header";
|
||||
const heading = doc.createElement("div");
|
||||
heading.className = "ink-inspector-heading";
|
||||
const toolIcon = doc.createElement("span");
|
||||
toolIcon.className = "ink-inspector-tool-icon";
|
||||
toolIcon.setAttribute("aria-hidden", "true");
|
||||
const header = this.element.createDiv({ cls: "ink-inspector-header" });
|
||||
const heading = header.createDiv({ cls: "ink-inspector-heading" });
|
||||
const toolIcon = heading.createSpan({
|
||||
cls: "ink-inspector-tool-icon",
|
||||
attr: { "aria-hidden": "true" }
|
||||
});
|
||||
setIcon(toolIcon, design.icon);
|
||||
const titleWrap = doc.createElement("div");
|
||||
const title = doc.createElement("div");
|
||||
title.className = "ink-inspector-title";
|
||||
title.textContent = `${design.label} settings`;
|
||||
const description = doc.createElement("div");
|
||||
description.className = "ink-inspector-description";
|
||||
description.textContent = design.description;
|
||||
titleWrap.append(title, description);
|
||||
heading.append(toolIcon, titleWrap);
|
||||
const closeButton = doc.createElement("button");
|
||||
closeButton.type = "button";
|
||||
closeButton.className = "ink-inspector-close clickable-icon";
|
||||
closeButton.setAttribute("aria-label", "Close color and size");
|
||||
const titleWrap = heading.createDiv();
|
||||
titleWrap.createDiv({ cls: "ink-inspector-title", text: `${design.label} settings` });
|
||||
titleWrap.createDiv({ cls: "ink-inspector-description", text: design.description });
|
||||
const closeButton = header.createEl("button", {
|
||||
cls: "ink-inspector-close clickable-icon",
|
||||
attr: { type: "button", "aria-label": "Close color and size" }
|
||||
});
|
||||
closeButton.dataset.inkFocus = "close";
|
||||
setIcon(closeButton, "x");
|
||||
closeButton.addEventListener("click", () => this.close());
|
||||
header.append(heading, closeButton);
|
||||
this.element.appendChild(header);
|
||||
|
||||
const preview = doc.createElement("div");
|
||||
preview.className = `ink-stroke-preview is-${this.tool}`;
|
||||
preview.setAttribute("role", "img");
|
||||
preview.setAttribute("aria-label", `${design.label} preview at ${formatWidth(width)}`);
|
||||
const previewLine = doc.createElement("div");
|
||||
previewLine.className = "ink-stroke-preview-line";
|
||||
const preview = this.element.createDiv({
|
||||
cls: `ink-stroke-preview is-${this.tool}`,
|
||||
attr: { role: "img", "aria-label": `${design.label} preview at ${formatWidth(width)}` }
|
||||
});
|
||||
const previewLine = preview.createDiv({ cls: "ink-stroke-preview-line" });
|
||||
previewLine.style.setProperty("--ink-preview-color", cssColor(color));
|
||||
previewLine.style.setProperty("--ink-preview-width", `${previewWidth(this.tool, width)}px`);
|
||||
preview.appendChild(previewLine);
|
||||
this.element.appendChild(preview);
|
||||
|
||||
if (design.colors) {
|
||||
const selectedChoice = design.colors.find((choice) => colorsMatch(color, choice.value));
|
||||
const isCustomColor = color !== "adaptive" && selectedChoice === undefined;
|
||||
const colorHeader = doc.createElement("div");
|
||||
colorHeader.className = "ink-inspector-section-row is-color";
|
||||
colorHeader.appendChild(this.sectionLabel("Color"));
|
||||
const colorValue = doc.createElement("output");
|
||||
colorValue.className = "ink-color-value";
|
||||
colorValue.textContent = selectedChoice?.label ?? color.toUpperCase();
|
||||
colorHeader.appendChild(colorValue);
|
||||
this.element.appendChild(colorHeader);
|
||||
const colorHeader = this.element.createDiv({ cls: "ink-inspector-section-row is-color" });
|
||||
this.sectionLabel(colorHeader, "Color");
|
||||
const colorValue = colorHeader.createEl("output", {
|
||||
cls: "ink-color-value",
|
||||
text: selectedChoice?.label ?? color.toUpperCase()
|
||||
});
|
||||
|
||||
const swatches = doc.createElement("div");
|
||||
swatches.className = "ink-color-grid";
|
||||
swatches.setAttribute("role", "group");
|
||||
swatches.setAttribute("aria-label", "Color presets");
|
||||
const swatches = this.element.createDiv({
|
||||
cls: "ink-color-grid",
|
||||
attr: { role: "group", "aria-label": "Color presets" }
|
||||
});
|
||||
for (const choice of design.colors) {
|
||||
const button = doc.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "ink-color-swatch";
|
||||
const button = swatches.createEl("button", {
|
||||
cls: "ink-color-swatch",
|
||||
attr: { type: "button", "aria-label": choice.label, title: choice.label }
|
||||
});
|
||||
button.classList.toggle("is-selected", colorsMatch(color, choice.value));
|
||||
button.setAttribute("aria-label", choice.label);
|
||||
button.setAttribute("title", choice.label);
|
||||
button.setAttribute("aria-pressed", button.classList.contains("is-selected") ? "true" : "false");
|
||||
button.dataset.inkFocus = `color-${choice.value}`;
|
||||
if (choice.value === "adaptive") {
|
||||
|
|
@ -251,21 +238,18 @@ export class ToolInspector {
|
|||
} else {
|
||||
button.style.setProperty("--ink-swatch-color", choice.value);
|
||||
}
|
||||
button.appendChild(this.selectionMark());
|
||||
this.selectionMark(button);
|
||||
button.addEventListener("click", () => {
|
||||
this.applyColor(choice.value);
|
||||
this.render();
|
||||
});
|
||||
swatches.appendChild(button);
|
||||
}
|
||||
|
||||
const custom = doc.createElement("label");
|
||||
custom.className = "ink-custom-color";
|
||||
const custom = swatches.createEl("label", { cls: "ink-custom-color", attr: { title: "Custom color" } });
|
||||
custom.classList.toggle("is-selected", isCustomColor);
|
||||
custom.setAttribute("title", "Custom color");
|
||||
const colorInput = doc.createElement("input");
|
||||
colorInput.type = "color";
|
||||
colorInput.setAttribute("aria-label", "Custom color");
|
||||
const colorInput = custom.createEl("input", {
|
||||
attr: { type: "color", "aria-label": "Custom color" }
|
||||
});
|
||||
colorInput.dataset.inkFocus = "color-custom";
|
||||
colorInput.value = color === "adaptive" ? "#111827" : color;
|
||||
colorInput.addEventListener("input", () => {
|
||||
|
|
@ -278,56 +262,52 @@ export class ToolInspector {
|
|||
}
|
||||
custom.classList.add("is-selected");
|
||||
});
|
||||
const customIcon = doc.createElement("span");
|
||||
customIcon.className = "ink-custom-color-glyph";
|
||||
customIcon.setAttribute("aria-hidden", "true");
|
||||
const customIcon = custom.createSpan({
|
||||
cls: "ink-custom-color-glyph",
|
||||
attr: { "aria-hidden": "true" }
|
||||
});
|
||||
setIcon(customIcon, "pipette");
|
||||
custom.append(colorInput, customIcon, this.selectionMark());
|
||||
swatches.appendChild(custom);
|
||||
this.element.appendChild(swatches);
|
||||
this.selectionMark(custom);
|
||||
}
|
||||
|
||||
const widthHeader = doc.createElement("div");
|
||||
widthHeader.className = "ink-inspector-section-row";
|
||||
widthHeader.appendChild(this.sectionLabel(this.tool === "eraser" ? "Eraser size" : "Stroke width"));
|
||||
const widthValue = doc.createElement("output");
|
||||
widthValue.className = "ink-width-value";
|
||||
widthValue.setAttribute("aria-live", "polite");
|
||||
widthValue.textContent = formatWidth(width);
|
||||
widthHeader.appendChild(widthValue);
|
||||
this.element.appendChild(widthHeader);
|
||||
const widthHeader = this.element.createDiv({ cls: "ink-inspector-section-row" });
|
||||
this.sectionLabel(widthHeader, this.tool === "eraser" ? "Eraser size" : "Stroke width");
|
||||
const widthValue = widthHeader.createEl("output", {
|
||||
cls: "ink-width-value",
|
||||
text: formatWidth(width),
|
||||
attr: { "aria-live": "polite" }
|
||||
});
|
||||
|
||||
const presets = doc.createElement("div");
|
||||
presets.className = "ink-width-presets";
|
||||
presets.setAttribute("role", "group");
|
||||
presets.setAttribute("aria-label", "Width presets");
|
||||
const presets = this.element.createDiv({
|
||||
cls: "ink-width-presets",
|
||||
attr: { role: "group", "aria-label": "Width presets" }
|
||||
});
|
||||
for (const choice of design.widths) {
|
||||
const button = doc.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "ink-width-preset";
|
||||
const button = presets.createEl("button", {
|
||||
cls: "ink-width-preset",
|
||||
attr: { type: "button" }
|
||||
});
|
||||
button.classList.toggle("is-selected", Math.abs(width - choice.value) < 0.01);
|
||||
button.setAttribute("aria-pressed", button.classList.contains("is-selected") ? "true" : "false");
|
||||
button.dataset.inkFocus = `width-${choice.value}`;
|
||||
button.dataset.width = String(choice.value);
|
||||
const sample = doc.createElement("span");
|
||||
sample.className = `ink-width-preset-sample is-${this.tool}`;
|
||||
const sample = button.createSpan({
|
||||
cls: `ink-width-preset-sample is-${this.tool}`,
|
||||
attr: { "aria-hidden": "true" }
|
||||
});
|
||||
sample.style.setProperty("--ink-preset-width", `${previewWidth(this.tool, choice.value)}px`);
|
||||
sample.style.setProperty("--ink-preset-color", cssColor(color));
|
||||
sample.setAttribute("aria-hidden", "true");
|
||||
const label = doc.createElement("span");
|
||||
label.textContent = choice.label;
|
||||
button.append(sample, label);
|
||||
button.createSpan({ text: choice.label });
|
||||
button.addEventListener("click", () => {
|
||||
this.applyWidth(choice.value);
|
||||
this.render();
|
||||
});
|
||||
presets.appendChild(button);
|
||||
}
|
||||
this.element.appendChild(presets);
|
||||
|
||||
const range = doc.createElement("input");
|
||||
range.type = "range";
|
||||
range.className = "ink-width-slider";
|
||||
const range = this.element.createEl("input", {
|
||||
cls: "ink-width-slider",
|
||||
attr: { type: "range" }
|
||||
});
|
||||
range.min = String(design.minimum);
|
||||
range.max = String(design.maximum);
|
||||
range.step = String(design.step);
|
||||
|
|
@ -350,7 +330,6 @@ export class ToolInspector {
|
|||
button.setAttribute("aria-pressed", selected ? "true" : "false");
|
||||
}
|
||||
});
|
||||
this.element.appendChild(range);
|
||||
if (shouldRestoreFocus) {
|
||||
const controls = this.element.querySelectorAll<HTMLElement>("[data-ink-focus]");
|
||||
const focusTarget = Array.from(controls).find((control) => control.dataset.inkFocus === focusKey);
|
||||
|
|
@ -358,17 +337,15 @@ export class ToolInspector {
|
|||
}
|
||||
}
|
||||
|
||||
private sectionLabel(text: string): HTMLDivElement {
|
||||
const label = this.element.ownerDocument.createElement("div");
|
||||
label.className = "ink-inspector-section-label";
|
||||
label.textContent = text;
|
||||
return label;
|
||||
private sectionLabel(parent: HTMLElement, text: string): HTMLDivElement {
|
||||
return parent.createDiv({ cls: "ink-inspector-section-label", text });
|
||||
}
|
||||
|
||||
private selectionMark(): HTMLSpanElement {
|
||||
const mark = this.element.ownerDocument.createElement("span");
|
||||
mark.className = "ink-color-swatch-check";
|
||||
mark.setAttribute("aria-hidden", "true");
|
||||
private selectionMark(parent: HTMLElement): HTMLSpanElement {
|
||||
const mark = parent.createSpan({
|
||||
cls: "ink-color-swatch-check",
|
||||
attr: { "aria-hidden": "true" }
|
||||
});
|
||||
setIcon(mark, "check");
|
||||
return mark;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue