UI API
dmn.ui를 통해 앱의 UI를 확장할 수 있습니다.
UI API는 메인 윈도우에서만 사용 가능합니다. 오버레이에서 호출하면 경고만 표시되고 동작하지 않습니다.
컨텍스트 메뉴
키 메뉴 (addKeyMenuItem)
키를 우클릭할 때 표시되는 메뉴에 항목을 추가합니다.
const menuId = dmn.ui.contextMenu.addKeyMenuItem({
id: 'copy-keycode',
label: '키 코드 복사',
onClick: (context) => {
navigator.clipboard.writeText(context.keyCode);
},
});Context 객체:
| 속성 | 타입 | 설명 |
|---|---|---|
keyCode | string | canonical 슬롯 식별자 (예: “D”, “LEFT CTRL+Z”, “F|NUMPAD 4”) |
id | string | 요소 안정 ID (UUID). 재정렬·모드 전환에도 같은 키를 가리킵니다 |
index | number | 키 인덱스. deprecated: 현재 스냅샷에서만 유효하며 신원은 id를 사용하세요 |
position | KeyPosition | 키 위치 정보 |
mode | string | 현재 키 모드 |
그리드 메뉴 (addGridMenuItem)
그리드 빈 공간을 우클릭할 때 표시되는 메뉴에 항목을 추가합니다.
설치된 플러그인이 늘어도 메뉴가 길어지지 않도록, 여기 추가된 항목은 플러그인 서브메뉴 하나로 묶여 표시됩니다.
자리는 등록 순서를 따르며 position은 적용되지 않습니다.
dmn.ui.contextMenu.addGridMenuItem({
id: 'add-timer',
label: '타이머 추가',
onClick: (context) => {
createTimer(context.position);
},
});Context 객체:
| 속성 | 타입 | 설명 |
|---|---|---|
position | { dx, dy } | 클릭 위치 (그리드 좌표) |
mode | string | 현재 키 모드 |
메뉴 항목 옵션
{
id: "my-menu", // 플러그인 내 고유 ID
label: "메뉴 항목", // 표시 텍스트
position: "bottom", // "top" | "bottom" (기본: bottom, 키 메뉴 전용)
// 조건부 표시
visible: (context) => context.mode === "4key",
// 조건부 비활성화
disabled: (context) => context.position.count === 0,
// 클릭 핸들러
onClick: async (context) => {
// ...
},
}visible과 disabled는 boolean 또는 context를 받는 함수입니다. 함수가 예외를 던지면 숨김/비활성으로 처리되고(나머지 메뉴는 그대로 표시), 함수가 아닌 값은 truthiness로 판정합니다. visible에 0, "", null을 주면 항목이 숨겨지고, 같은 값들을 disabled에 주면 항목이 활성 상태로 남습니다.
메뉴 관리
// 메뉴 업데이트
dmn.ui.contextMenu.updateMenuItem(menuId, {
label: '새로운 라벨',
disabled: true,
});
// 특정 메뉴 제거
dmn.ui.contextMenu.removeMenuItem(menuId);
// 이 플러그인의 모든 메뉴 제거
dmn.ui.contextMenu.clearMyMenuItems();Display Element
displayElement는 저수준(Primitive) API로, 직접적인 사용을 권장하지 않습니다.
그리드와 오버레이에 커스텀 UI 요소를 추가하려면 선언형 방식인 defineElement
사용을 강력히 권장합니다.
그리드와 오버레이에 커스텀 UI 요소를 추가합니다.
기본 사용법
const panel = dmn.ui.displayElement.add({
html: `<div style="background: #000; color: #fff; padding: 16px;">
Hello!
</div>`,
position: { x: 100, y: 100 },
draggable: true,
});
// 제거
panel.remove();상태 기반 템플릿
const panel = dmn.ui.displayElement.add({
position: { x: 100, y: 100 },
state: { value: 0, history: [] },
template: (state, { html }) => html`
<div style="background: #000; color: #fff; padding: 16px;">
<strong>${state.value}</strong>
<div style="display: flex; gap: 2px;">
${state.history.map(
(v) => html`
<span
style="width: 4px; height: ${v}px; background: #86EFAC;"
></span>
`,
)}
</div>
</div>
`,
});
// 상태 업데이트 → 자동 리렌더링
panel.setState({ value: 42, history: [10, 20, 30] });이벤트 핸들러
dmn.ui.displayElement.add({
html: `<div>클릭하세요</div>`,
position: { x: 100, y: 100 },
// 함수 직접 전달 (권장)
onClick: async () => {
console.log('클릭됨!');
},
onPositionChange: async (pos) => {
await dmn.plugin.storage.set('position', pos);
},
onDelete: async () => {
console.log('삭제됨!');
},
});인스턴스 메서드
| 메서드 | 설명 |
|---|---|
setState(updates) | 상태 업데이트 (템플릿 리렌더링) |
setData(updates) | 상태 업데이트 (setState 별칭) |
getState() | 현재 상태 조회 |
setText(selector, text) | 요소 텍스트 설정 |
setHTML(selector, html) | 요소 HTML 설정 |
setStyle(selector, styles) | 스타일 적용 |
addClass(selector, ...classes) | 클래스 추가 |
removeClass(selector, ...classes) | 클래스 제거 |
toggleClass(selector, className) | 클래스 토글 |
query(selector) | 요소 검색 (Shadow DOM 포함) |
update(config) | 메타데이터 업데이트 |
remove() | 요소 제거 |
앵커
특정 키에 요소를 고정합니다:
dmn.ui.displayElement.add({
html: `<div>→</div>`,
position: { x: 0, y: 0 },
anchor: {
keyCode: 'D',
offset: { x: 70, y: 0 },
},
});anchor.keyCode는 canonical 슬롯 식별자입니다. 멀티 키 슬롯에는 canonical
문자열을 사용하세요 (match: 'all'은 "LEFT CTRL+Z", match: 'any'는
"F|NUMPAD 4" 형태).
canonical 슬롯 식별자를
참고하세요.
Shadow DOM
스타일을 격리하려면 scoped: true를 사용합니다:
dmn.ui.displayElement.add({
html: `
<style>
:host { display: block; }
.widget { background: red; }
</style>
<div class="widget">격리된 스타일</div>
`,
position: { x: 100, y: 100 },
scoped: true,
});클린업
dmn.plugin.registerCleanup(() => {
// 모든 Display Element 제거
dmn.ui.displayElement.clearMyElements();
});Dialog
사용자와 상호작용할 수 있는 모달 다이얼로그입니다.
alert
간단한 알림을 표시합니다. 반환된 Promise는 사용자가 확인 버튼을 누르거나 다이얼로그가 닫힐 때 resolve됩니다.
await dmn.ui.dialog.alert('저장되었습니다!');
// 사용자가 확인을 누른 뒤에 이어서 실행됨
await dmn.ui.dialog.alert('작업 완료', { confirmText: 'OK' });confirm
확인/취소 다이얼로그를 표시합니다.
옵션: confirmText(확인 버튼 문구), cancelText(취소 버튼 문구), danger(파괴적 작업용, 확인 버튼이 위험 색상으로 표시)
const ok = await dmn.ui.dialog.confirm('진행하시겠습니까?');
if (ok) {
// 확인 클릭
}
// 위험한 작업
const confirmed = await dmn.ui.dialog.confirm('모든 데이터가 삭제됩니다.', {
danger: true,
confirmText: '삭제',
cancelText: '유지',
});custom
커스텀 HTML 다이얼로그를 표시합니다. Components API와 함께 사용합니다.
const formHtml = `
<div class="flex flex-col gap-[12px]">
${dmn.ui.components.formRow(
'이름',
dmn.ui.components.input({ id: 'name' }),
)}
${dmn.ui.components.formRow(
'테마',
dmn.ui.components.dropdown({
id: 'theme',
options: [
{ label: '다크', value: 'dark' },
{ label: '라이트', value: 'light' },
],
}),
)}
</div>
`;
const confirmed = await dmn.ui.dialog.custom(formHtml, {
confirmText: '저장',
showCancel: true,
});
if (confirmed) {
const name = document.getElementById('name').value;
const theme = document.getElementById('theme').value;
}Components
Dialog 내부에서 사용할 수 있는 UI 컴포넌트입니다. 모든 컴포넌트는 HTML 문자열을 반환합니다.
button
dmn.ui.components.button('저장', {
variant: 'primary', // "primary" | "danger" | "secondary"
onClick: () => console.log('클릭'),
});checkbox
dmn.ui.components.checkbox({
checked: true,
id: 'enabled',
onChange: (checked) => console.log('상태:', checked),
});input
dmn.ui.components.input({
type: 'number',
value: 50,
min: 0,
max: 100,
width: 100,
id: 'volume',
onChange: (value) => console.log('값:', value),
});dropdown
dmn.ui.components.dropdown({
options: [
{ label: '다크', value: 'dark' },
{ label: '라이트', value: 'light' },
],
selected: 'dark',
id: 'theme',
onChange: (value) => console.log('선택:', value),
});formRow
라벨과 컴포넌트를 수평으로 배치합니다.
dmn.ui.components.formRow('볼륨', volumeInput);panel
컨텐츠를 감싸는 패널 컨테이너입니다.
panel은 Display Element 전용입니다. Dialog 내부에서는 사용하지 마세요.
const panelHtml = dmn.ui.components.panel(formHtml, {
title: '설정',
width: 400,
});
dmn.ui.displayElement.add({
html: panelHtml,
position: { x: 100, y: 100 },
});Color Picker
색상 선택기를 표시합니다.
dmn.ui.pickColor({
initialColor: '#ff0000',
onColorChange: (color) => {
console.log('색상 변경 중:', color);
},
onColorChangeComplete: (color) => {
console.log('색상 선택 완료:', color);
},
// 선택적 옵션
position: { x: 100, y: 100 }, // 직접 위치 지정
referenceElement: document.getElementById('my-button'), // 요소 기준 위치
});옵션
| 속성 | 타입 | 설명 |
|---|---|---|
initialColor | string | 초기 색상 (HEX) |
onColorChange | (color) => void | 색상이 변경될 때마다 호출 |
onColorChangeComplete | (color) => void | 색상 선택이 완료되었을 때 호출 |
onClose | () => void | 색상 선택기가 닫힐 때 호출 |
position | { x, y } | 표시 위치 (referenceElement가 없을 때 사용) |
referenceElement | HTMLElement | 이 요소를 기준으로 위치 설정 |
id | string | 고유 ID (중복 방지용) |
실전 예제: 설정 패널
// @id settings-panel
if (dmn.window.type !== 'main') return;
let settings = {
enabled: true,
theme: 'dark',
volume: 50,
};
async function showSettings() {
const tempSettings = { ...settings };
const formHtml = `
<div class="flex flex-col gap-[12px]">
${dmn.ui.components.formRow(
'활성화',
dmn.ui.components.checkbox({
checked: settings.enabled,
onChange: (v) => (tempSettings.enabled = v),
}),
)}
${dmn.ui.components.formRow(
'테마',
dmn.ui.components.dropdown({
options: [
{ label: '다크', value: 'dark' },
{ label: '라이트', value: 'light' },
],
selected: settings.theme,
onChange: (v) => (tempSettings.theme = v),
}),
)}
${dmn.ui.components.formRow(
'볼륨',
dmn.ui.components.input({
type: 'number',
value: settings.volume,
min: 0,
max: 100,
width: 60,
onChange: (v) => (tempSettings.volume = parseInt(v)),
}),
)}
</div>
`;
const confirmed = await dmn.ui.dialog.custom(formHtml, {
confirmText: '저장',
showCancel: true,
});
if (confirmed) {
settings = tempSettings;
await dmn.plugin.storage.set('settings', settings);
await dmn.ui.dialog.alert('저장되었습니다!');
}
}
// 그리드 메뉴에 설정 추가
dmn.ui.contextMenu.addGridMenuItem({
id: 'open-settings',
label: '설정',
onClick: showSettings,
});
// 초기화
(async () => {
const saved = await dmn.plugin.storage.get('settings');
if (saved) settings = saved;
})();
dmn.plugin.registerCleanup(() => {
dmn.ui.contextMenu.clearMyMenuItems();
});