@@ -232,6 +252,12 @@ Sets the anchor position when element size changes. Default is `"top-left"`.
| `bottom-center` | Bottom center |
| `bottom-right` | Bottom-right |
+### Anchor Behavior
+
+- `top-left`: the top-left corner stays fixed as the size changes (default)
+- `center`: the center position stays fixed as the size changes
+- `bottom-right`: the bottom-right corner stays fixed as the size changes
+
### Definition Level Setting
Define default anchor for all instances:
@@ -252,7 +278,7 @@ Use `setAnchor()` and `getAnchor()` in `onMount` to dynamically change anchor:
dmn.plugin.defineElement({
name: "Dynamic Anchor Panel",
- onMount: ({ setAnchor, getAnchor }) => {
+ onMount: ({ setAnchor, getAnchor, getSettings }) => {
// Check current anchor
console.log("Current anchor:", getAnchor()); // "top-left"
@@ -270,6 +296,12 @@ dmn.plugin.defineElement({
});
```
+### Anchor Priority
+
+1. Per-instance `resizeAnchor` (set via `setAnchor`)
+2. `resizeAnchor` on the definition
+3. Default `"top-left"`
+
## Resize Settings (resizable, preserveAxis)
Use `resizable` option to let users resize elements directly in the grid.
@@ -277,6 +309,7 @@ Use `resizable` option to let users resize elements directly in the grid.
### resizable
Set `resizable: true` to show 8-direction resize handles:
+The initial size is 200×150. A saved or estimated size takes precedence when available.
```javascript
dmn.plugin.defineElement({
@@ -312,3 +345,386 @@ dmn.plugin.defineElement({
// ...
});
```
+
+### Usage Example
+
+For a panel with a graph toggle, like the KPS panel:
+
+- With `preserveAxis: "width"`, the width is preserved when the graph is toggled on and off
+- The height adjusts automatically depending on whether the graph is shown
+
+```javascript
+dmn.plugin.defineElement({
+ name: "KPS Panel",
+ resizable: true,
+ preserveAxis: "width",
+ resizeAnchor: "bottom-left", // Bottom fixed (graph expands upward)
+
+ settings: {
+ showGraph: { type: "boolean", default: true, label: "Show Graph" },
+ },
+
+ template: (state, settings, { html }) => html`
+
+
${state.kps ?? 0}
+ ${settings.showGraph ? html`
...
` : ""}
+
+ `,
+});
+```
+
+
+ Apply `width: 100%; height: 100%` to the root element of a `resizable`
+ element's template so the content fills the size the user chose.
+
+
+## Context Menu (contextMenu)
+
+### Basic Setup
+
+```javascript fragment=object-members
+contextMenu: {
+ create: "Create Panel", // Right-click on empty grid space
+ delete: "Delete Panel", // Right-click on the panel
+},
+```
+
+### Custom Menu Items
+
+```javascript fragment=object-members
+contextMenu: {
+ create: "Create Panel",
+ delete: "Delete Panel",
+ items: [
+ {
+ label: "Reset Stats",
+ onClick: ({ actions }) => actions.reset(),
+ },
+ {
+ label: "Export Data",
+ onClick: async ({ element, actions }) => {
+ await actions.exportData();
+ },
+ // Conditional visibility
+ visible: ({ element }) => !!element.settings.enableExport,
+ // Conditional disable
+ disabled: ({ element }) => element.settings.exportFormat === "none",
+ // Position (top or bottom, default: bottom)
+ position: "bottom",
+ },
+ ],
+},
+
+// Register actions in onMount
+onMount: ({ expose, setState }) => {
+ expose({
+ reset: () => setState({ count: 0 }),
+ exportData: async () => { /* export logic */ },
+ });
+},
+```
+
+Items with `position: "top"` appear above the built-in menu entries; the rest
+appear below them.
+
+
+ Menu predicates run in the main window with `{ element, actions }`. Overlay
+ runtime state set via `setState` is not synced into this context by default —
+ `element.state` here reflects the main-window state (initialized from
+ `previewState`). To base conditions on overlay state, declare the keys in
+ `contextMenuStateKeys`.
+
+
+### Overlay State in Predicates (contextMenuStateKeys)
+
+Declare overlay state keys that menu predicates need. Only the declared keys are
+mirrored to the main window (on `setState` changes) and merged into
+`element.state` during predicate evaluation. High-frequency state is never sent
+unless declared, and the main-window preview state stays untouched.
+
+```javascript fragment=object-members
+// Mirror only `active` for menu predicates
+contextMenuStateKeys: ["active"],
+
+contextMenu: {
+ items: [
+ {
+ label: "Stop Capture",
+ action: "stopCapture",
+ visible: ({ element }) => !!element.state?.active,
+ },
+ ],
+},
+```
+
+## Template (template)
+
+The template function receives `state` and `settings` and returns the UI.
+See [Template Syntax](/docs/template-syntax) for the full syntax.
+
+```javascript fragment=object-members
+template: (state, settings, { html, t, locale }) => html`
+
+
${state.value}
+ ${settings.showDetails ? html`
+
Details
+ ` : ""}
+
+`,
+```
+
+### Template Helpers
+
+| Helper | Description |
+| -------- | ---------------------------------------- |
+| `html` | htm tag function (creates React Elements) |
+| `t(key)` | i18n translation function |
+| `locale` | Current locale code |
+
+## Preview State (previewState)
+
+Initial state shown as a preview in the main window.
+The actual logic runs only in the overlay, so the main window displays this state.
+
+```javascript fragment=object-members
+previewState: {
+ kps: 12,
+ history: [5, 8, 12, 10, 15],
+},
+```
+
+## Mount Logic (onMount)
+
+`onMount` runs only in the overlay and implements the actual behavior.
+
+### Context Object
+
+```javascript fragment=object-members
+onMount: (context) => {
+ const {
+ setState, // Update state
+ getSettings, // Get current settings
+ setAnchor, // Set resize anchor
+ getAnchor, // Get current anchor
+ onHook, // Register event hooks
+ expose, // Expose functions for the context menu
+ locale, // Current locale code
+ t, // Translation function
+ onLocaleChange, // Subscribe to locale changes
+ onSettingsChange, // Subscribe to settings changes
+ } = context;
+
+ // Return cleanup function
+ return () => { /* cleanup */ };
+},
+```
+
+### Event Hooks (onHook)
+
+```javascript fragment=object-members
+onMount: ({ onHook, setState }) => {
+ // Mapped key events
+ onHook("key", ({ key, state, mode }) => {
+ if (state === "DOWN") {
+ console.log(`${key} pressed (${mode})`);
+ }
+ });
+
+ // All raw input events (keyboard, mouse)
+ onHook("rawKey", ({ device, label, state }) => {
+ console.log(`[${device}] ${label} ${state}`);
+ });
+},
+```
+
+### Settings Change Detection (onSettingsChange)
+
+Use this when you need to react to settings changes immediately.
+
+
+ In most cases, reading the latest settings with `getSettings()` is enough.
+ Use `onSettingsChange` only when you need external API calls or resource
+ re-initialization.
+
+
+```javascript fragment=object-members
+onMount: ({ setState, getSettings, onSettingsChange }) => {
+ const fetchData = async (nickname) => {
+ const response = await fetch(`/api/user/${nickname}`);
+ const data = await response.json();
+ setState({ data });
+ };
+
+ // Initial load
+ fetchData(getSettings().nickname);
+
+ // Refetch when the nickname changes
+ onSettingsChange((newSettings, oldSettings) => {
+ if (newSettings.nickname !== oldSettings.nickname) {
+ fetchData(newSettings.nickname);
+ }
+ });
+},
+```
+
+## i18n Support (messages)
+
+```javascript
+dmn.plugin.defineElement({
+ name: "Localized Panel",
+
+ messages: {
+ ko: {
+ "menu.create": "패널 생성",
+ "menu.delete": "패널 삭제",
+ "label.count": "카운트",
+ },
+ en: {
+ "menu.create": "Create Panel",
+ "menu.delete": "Delete Panel",
+ "label.count": "Count",
+ },
+ },
+
+ contextMenu: {
+ create: "menu.create", // Use message keys
+ delete: "menu.delete",
+ },
+
+ settings: {
+ count: {
+ type: "number",
+ default: 0,
+ label: "label.count", // Use message keys
+ },
+ },
+
+ template: (state, settings, { html, t, locale }) => html`
+
${t("label.count")}: ${state.value ?? 0}
+ `,
+});
+```
+
+## Practical Example: KPS Panel
+
+```javascript
+// @id kps-panel
+
+dmn.plugin.defineElement({
+ name: "KPS Panel",
+ maxInstances: 1,
+
+ contextMenu: {
+ create: "Create KPS Panel",
+ delete: "Delete KPS Panel",
+ items: [
+ {
+ label: "Reset Stats",
+ onClick: ({ actions }) => actions.reset(),
+ },
+ ],
+ },
+
+ settings: {
+ showGraph: { type: "boolean", default: true, label: "Show Graph" },
+ textColor: { type: "color", default: "#FFFFFF", label: "Text Color" },
+ graphColor: {
+ type: "color",
+ default: "#86EFAC",
+ label: "Graph Color",
+ visible: (s) => s.showGraph,
+ },
+ },
+
+ previewState: {
+ kps: 12,
+ max: 20,
+ history: [5, 8, 12, 15, 10, 12],
+ },
+
+ template: (state, settings, { html }) => html`
+
+
+ ${state.kps ?? 0}
+ KPS
+
+ ${settings.showGraph
+ ? html`
+
+ ${(state.history ?? []).map((v) => {
+ const height = state.max ? (v / state.max) * 100 : 0;
+ return html`
+
+ `;
+ })}
+
+ `
+ : ""}
+
+ `,
+
+ onMount: ({ setState, expose, onHook }) => {
+ const timestamps = [];
+ let max = 0;
+ const historySize = 20;
+ const history = [];
+
+ onHook("key", ({ state }) => {
+ if (state === "DOWN") {
+ timestamps.push(Date.now());
+ }
+ });
+
+ const interval = setInterval(() => {
+ const now = Date.now();
+ // Keep only timestamps within the last second
+ while (timestamps.length && timestamps[0] < now - 1000) {
+ timestamps.shift();
+ }
+
+ const kps = timestamps.length;
+ max = Math.max(max, kps);
+
+ history.push(kps);
+ if (history.length > historySize) history.shift();
+
+ setState({ kps, max, history: [...history] });
+ }, 50);
+
+ expose({
+ reset: () => {
+ timestamps.length = 0;
+ history.length = 0;
+ max = 0;
+ setState({ kps: 0, max: 0, history: [] });
+ },
+ });
+
+ return () => clearInterval(interval);
+ },
+});
+```
diff --git a/docs/content/en/guide/installation/page.mdx b/docs/content/en/guide/installation/page.mdx
index 2643e750..49651d73 100644
--- a/docs/content/en/guide/installation/page.mdx
+++ b/docs/content/en/guide/installation/page.mdx
@@ -9,7 +9,7 @@ description: How to download, install, and run DM Note
DM Note can be downloaded from GitHub Releases.
-1. Download the latest version from the [GitHub Releases page](https://github.com/lee-sihun/DmNote/releases).
+1. Download the latest version from the [GitHub Releases page](https://github.com/DmNote-App/DmNote/releases).
2. Extract the downloaded ZIP file to your desired location.
3. Run the `DM Note.exe` file.
@@ -39,7 +39,7 @@ When you first run the program, two windows will appear.
Program settings are automatically saved to the following path.
-```
+```text
%appdata%/com.dmnote.desktop/store.json
```
diff --git a/docs/content/en/guide/settings/page.mdx b/docs/content/en/guide/settings/page.mdx
index 7caab6c0..3331b11e 100644
--- a/docs/content/en/guide/settings/page.mdx
+++ b/docs/content/en/guide/settings/page.mdx
@@ -39,6 +39,13 @@ When enabled, the overlay window ignores mouse events (click-through) allowing y
When locked, you cannot directly interact with the overlay.
+### Detaching the Properties Panel
+
+Use the detach button at the top of the properties panel to move it into a separate window at any time. Key, note, and counter editing as well as layer management work the same in the detached window.
+
+- Use the X button on the detached window or the reattach button at the top of the panel to return it inline.
+- While detached, picking a gradient color anchor on the canvas is limited.
+
## Graphics Settings
### Rendering Option
@@ -70,9 +77,16 @@ Select the reference point when resizing the overlay window:
Load a custom CSS file to customize key and counter styles.
-1. Click the **Select CSS File** button.
-2. Select your CSS file.
-3. Styles are applied immediately.
+1. Click the **Manage CSS** button to open the custom CSS panel.
+2. Turn on the **Enable** toggle at the top of the panel.
+3. Click **Import CSS File** and select your CSS file (`.css`, up to 1 MiB).
+4. Styles are applied immediately.
+
+The list in the panel keeps up to 10 previously imported CSS files.
+
+- Click the **Apply** button on an entry to switch to that file instantly, without a file dialog.
+- Right-click an entry and choose **Remove from list** (the file itself is not deleted).
+- Entries that can no longer be applied show a badge: **Missing** (moved or deleted), **Unusable** (not a regular `.css` file anymore), or **Too large** (over 1 MiB).
For detailed CSS styling information, see the [Custom CSS
@@ -83,9 +97,9 @@ Load a custom CSS file to customize key and counter styles.
Load JavaScript plugins to extend program functionality.
-1. Turn on the **Enable JS Plugins** toggle.
-2. Click the **Manage Plugins** button.
-3. Click **Add JS Plugin** to select a file.
+1. Click the **Manage Plugins** button to open the panel.
+2. Turn on the **Enable** toggle at the top of the panel.
+3. Click **Add Plugins** to select a file.
For detailed plugin development information, see [Getting
diff --git a/docs/content/en/guide/tips/page.mdx b/docs/content/en/guide/tips/page.mdx
index 597723eb..85c44767 100644
--- a/docs/content/en/guide/tips/page.mdx
+++ b/docs/content/en/guide/tips/page.mdx
@@ -49,6 +49,14 @@ To capture both game and overlay at once:
- Add the overlay with **Window Capture**.
- Place the overlay source above the game source.
+### Using a Browser Source with OBS Mode
+
+Turning on **OBS Mode** in settings lets you show the overlay as a browser source on the same network. Use **Copy URL** to get the address and paste it into an OBS browser source.
+
+
+ This share URL is not just a view link. Anyone on your network who has it can subscribe to your key input stream and modify plugin storage, so only share it on trusted networks. If you suspect it leaked, regenerate the session token to invalidate the old URL.
+
+
## Performance Optimization
### Graphics Troubleshooting
@@ -103,7 +111,7 @@ Open Developer Tools with `Ctrl+Shift+I` to inspect elements and test CSS.
Regularly backup important settings:
-```
+```text
%appdata%/com.dmnote.desktop/
```
diff --git a/docs/content/en/settings/page.mdx b/docs/content/en/settings/page.mdx
index add01094..1ee54ca8 100644
--- a/docs/content/en/settings/page.mdx
+++ b/docs/content/en/settings/page.mdx
@@ -22,6 +22,10 @@ description: Plugin settings management with defineSettings
const pluginSettings = dmn.plugin.defineSettings({
settings: {
+ connectionSection: {
+ type: "section",
+ label: "Connection",
+ },
apiKey: {
type: "string",
default: "",
@@ -36,27 +40,26 @@ const pluginSettings = dmn.plugin.defineSettings({
default: "dark",
label: "Theme",
},
- sectionDivider: {
- type: "divider",
+ behaviorSection: {
+ type: "section",
+ label: "Behavior",
},
enabled: {
type: "boolean",
default: true,
label: "Enabled",
},
+ // Conditional visibility — static boolean or function, shown only while enabled is true
+ advancedOption: {
+ type: "number",
+ default: 5,
+ label: "Advanced Option",
+ visible: (settings) => settings.enabled,
+ },
},
});
-// `type: "divider"` adds only a divider in settings panel/modal.
-
-// Conditional visibility — use visible property to dynamically show/hide items
-// Supports both static boolean and function
-advancedOption: {
- type: "number",
- default: 5,
- label: "Advanced Option",
- visible: (settings) => settings.enabled, // Only shown when enabled is true
-},
+// section starts a new card. (The legacy divider type was removed — existing divider entries are ignored.)
// Get settings values
const current = pluginSettings.get();
@@ -72,6 +75,14 @@ if (confirmed) {
}
```
+`defineSettings` uses the same section contract as `defineElement`: a section starts
+a new card, its optional label appears above the card, and its key is excluded from
+stored/default values and callbacks. `section.visible` controls the entire group.
+Sections with no renderable value settings are hidden entirely; only when no value
+setting is renderable anywhere does the settings UI show its standard empty state.
+Panel and modal modes have identical semantics, including fail-closed visibility
+evaluation.
+
## API Reference
### defineSettings(definition)
@@ -115,7 +126,7 @@ Both detect settings changes but have different purposes:
```javascript
const settings = dmn.plugin.defineSettings({
- settings: { apiKey: { type: "string", default: "" } },
+ settings: { apiKey: { type: "string", default: "", label: "API Key" } },
// onChange: Always executes (cannot unsubscribe)
onChange: (newSettings, oldSettings) => {
@@ -205,7 +216,144 @@ dmn.plugin.defineElement({
// Instance-specific settings
settings: {
showGraph: { type: "boolean", default: true, label: "Show Graph" },
+ graphColor: {
+ type: "color",
+ default: "#86EFAC",
+ label: "Graph Color",
+ visible: (s) => s.showGraph,
+ },
+ },
+
+ template: (state, instanceSettings, { html }) => {
+ const global = globalSettings.get();
+ return html`
+ KPS: ${state.kps ?? 0}
+ `;
+ },
+
+ onMount: ({ setState, onHook }) => {
+ const global = globalSettings.get();
+ let count = 0;
+
+ onHook("key", ({ state }) => {
+ if (state === "DOWN") count++;
+ });
+
+ const interval = setInterval(() => {
+ setState({ kps: count });
+ count = 0;
+ }, global.refreshRate);
+
+ return () => clearInterval(interval);
},
- // ...
});
```
+
+### Adding Settings to the Grid Menu
+
+You can also add a settings menu without any panel:
+
+```javascript
+// @id settings-only-plugin
+
+const pluginSettings = dmn.plugin.defineSettings({
+ settings: {
+ volume: { type: "number", default: 50, min: 0, max: 100, label: "Volume" },
+ notifications: { type: "boolean", default: true, label: "Notifications" },
+ },
+});
+
+// Add to the right-click menu on empty grid space
+dmn.ui.contextMenu.addGridMenuItem({
+ id: "my-plugin-settings",
+ label: "Plugin Settings",
+ onClick: () => pluginSettings.open(),
+});
+
+dmn.plugin.registerCleanup(() => {
+ dmn.ui.contextMenu.clearMyMenuItems();
+});
+```
+
+### Reacting to Settings Changes
+
+```javascript
+// @id data-fetcher
+
+const fetcherSettings = dmn.plugin.defineSettings({
+ settings: {
+ apiEndpoint: {
+ type: "string",
+ default: "https://api.example.com",
+ label: "API Endpoint",
+ },
+ refreshInterval: {
+ type: "number",
+ default: 5000,
+ min: 1000,
+ max: 60000,
+ label: "Refresh Interval (ms)",
+ },
+ autoRefresh: {
+ type: "boolean",
+ default: true,
+ label: "Auto Refresh",
+ },
+ },
+});
+
+let fetchInterval = null;
+
+function startFetching() {
+ const { refreshInterval, autoRefresh, apiEndpoint } = fetcherSettings.get();
+
+ if (fetchInterval) {
+ clearInterval(fetchInterval);
+ fetchInterval = null;
+ }
+
+ if (!autoRefresh) return;
+
+ fetchInterval = setInterval(async () => {
+ const response = await fetch(apiEndpoint);
+ const data = await response.json();
+ console.log("Data:", data);
+ }, refreshInterval);
+}
+
+// Restart the interval when settings change
+fetcherSettings.subscribe((newSettings, oldSettings) => {
+ if (
+ newSettings.apiEndpoint !== oldSettings.apiEndpoint ||
+ newSettings.refreshInterval !== oldSettings.refreshInterval ||
+ newSettings.autoRefresh !== oldSettings.autoRefresh
+ ) {
+ startFetching();
+ }
+});
+
+startFetching();
+
+dmn.plugin.registerCleanup(() => {
+ if (fetchInterval) clearInterval(fetchInterval);
+});
+```
+
+## Comparison with defineElement's onSettingsChange
+
+| Feature | `defineElement` | `defineSettings` |
+| ------------------------ | ---------------------------- | --------------------------------------- |
+| Settings change handling | `onSettingsChange(callback)` | `onChange` + `subscribe()` |
+| Unsubscribing | Automatic (on unmount) | Manual, via `subscribe()` return value |
+| Where to use | Inside `onMount` only | Anywhere |
+| Target | Instance-specific settings | Global/standalone settings |
+
+## Automatic Behavior
+
+| Feature | Description |
+| ------------------------------ | -------------------------------------------------------------- |
+| **Automatic UI generation** | Property panel/modal generated from the settings schema |
+| **Automatic storage handling** | Saved to and restored from `plugin.storage` |
+| **i18n support** | Integrates with messages |
+| **Per-type components** | boolean→checkbox, color→color picker, and so on |
+| **Automatic panel sync** | All panels of the same plugin re-render when settings change |
diff --git a/docs/content/en/template-syntax/page.mdx b/docs/content/en/template-syntax/page.mdx
index d5aea175..508e92df 100644
--- a/docs/content/en/template-syntax/page.mdx
+++ b/docs/content/en/template-syntax/page.mdx
@@ -12,11 +12,11 @@ It allows intuitive writing close to standard HTML syntax.
### Value Interpolation
-```javascript
+```javascript fragment=object-members
template: (state, settings, { html }) => html`
Current value: ${state.value}
Colored text
-`;
+`,
```
@@ -112,7 +112,7 @@ html`
### Inline Styles
-```javascript
+```javascript fragment=object-members
template: (state, settings, { html }) => html`
${state.value}
KPS
-`;
+`,
```
## Practical Example
### Stats Panel
-```javascript
+```javascript fragment=object-members
template: (state, settings, { html }) => html`