mirror of
https://github.com/hyungyunlim/obsidian-korean-grammar-assistant.git
synced 2026-07-22 06:42:55 +00:00
fix: resolve all Obsidian review bot ESLint violations (48 issues)
- sentence-case: fix UI text casing, add brand/acronym config (23 issues) - no-static-styles-assignment: move inline styles to CSS classes (19 issues) - platform: replace navigator API with Obsidian Platform API (3 issues) - no-forbidden-elements: move dynamic <style> elements to styles.css (3 issues) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9ca619e3a9
commit
b5aad78422
13 changed files with 467 additions and 1722 deletions
126
.claude/skills/obsidian-review/SKILL.md
Normal file
126
.claude/skills/obsidian-review/SKILL.md
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
---
|
||||
name: obsidian-review
|
||||
description: "Obsidian 플러그인 심사 리뷰 봇(eslint-plugin-obsidianmd)과 동일한 규칙으로 코드를 로컬 스캔하여 Required 이슈를 사전에 발견하고 수정합니다. 플러그인 심사 대응, ESLint 검사, sentence-case 수정 시 사용합니다."
|
||||
argument-hint: "[fix|setup|check]"
|
||||
disable-model-invocation: true
|
||||
allowed-tools:
|
||||
- "Bash(npx:*)"
|
||||
- "Bash(npm install:*)"
|
||||
- "Bash(npm run build:*)"
|
||||
- "Bash(git checkout:*)"
|
||||
- Read
|
||||
- Edit
|
||||
- Write
|
||||
- Glob
|
||||
- Grep
|
||||
---
|
||||
|
||||
# Obsidian 플러그인 심사 봇 로컬 검증
|
||||
|
||||
인자: $ARGUMENTS
|
||||
- 인자 없음: 전체 스캔 + 이슈 리포트만 출력
|
||||
- `fix`: 전체 스캔 + 발견된 Required 이슈 자동 수정
|
||||
- `setup`: ESLint 환경 세팅만 수행
|
||||
- `check`: 환경이 준비되었는지만 확인
|
||||
|
||||
이슈 유형별 상세 해결법은 [obsidian-plugin-review-guide.md](../../../.taskmaster/docs/obsidian-plugin-review-guide.md)를 참조한다.
|
||||
|
||||
## 1단계: 환경 확인
|
||||
|
||||
다음 3가지를 확인한다:
|
||||
|
||||
1. `node_modules/eslint-plugin-obsidianmd` 디렉토리 존재 여부
|
||||
2. `node_modules/eslint` 디렉토리 존재 여부
|
||||
3. 프로젝트 루트에 `eslint.config.mjs` 파일 존재 여부
|
||||
|
||||
하나라도 없으면 2단계로, 모두 있으면 3단계로 진행한다.
|
||||
인자가 `check`이면 환경 상태만 보고하고 종료한다.
|
||||
|
||||
## 2단계: 환경 세팅 (필요 시)
|
||||
|
||||
누락된 항목만 설치/생성한다:
|
||||
|
||||
- `eslint-plugin-obsidianmd` 미설치 시:
|
||||
```bash
|
||||
npm install --save-dev eslint-plugin-obsidianmd --legacy-peer-deps
|
||||
```
|
||||
|
||||
- `eslint` 미설치 시:
|
||||
```bash
|
||||
npm install --save-dev eslint --legacy-peer-deps
|
||||
```
|
||||
|
||||
- `eslint.config.mjs` 미존재 시, 다음 내용으로 생성:
|
||||
```javascript
|
||||
import obsidianmd from 'eslint-plugin-obsidianmd';
|
||||
export default [...obsidianmd.configs.recommended];
|
||||
```
|
||||
|
||||
설치 후 `package.json`과 `package-lock.json` 변경을 되돌린다:
|
||||
```bash
|
||||
git checkout -- package.json package-lock.json
|
||||
```
|
||||
|
||||
인자가 `setup`이면 세팅 완료 후 종료한다.
|
||||
|
||||
## 3단계: ESLint 실행
|
||||
|
||||
```bash
|
||||
npx eslint main.ts 'src/**/*.ts' 2>&1
|
||||
```
|
||||
|
||||
## 4단계: 결과 분석
|
||||
|
||||
ESLint 출력을 분석하여 다음 형태로 요약 보고한다:
|
||||
|
||||
```
|
||||
## 스캔 결과
|
||||
|
||||
- Required 이슈: N개
|
||||
- Optional 이슈: N개
|
||||
|
||||
### Required 이슈 목록
|
||||
| # | 파일 | 행 | 규칙 | 내용 |
|
||||
|---|------|-----|------|------|
|
||||
| 1 | src/ui/settings-tab.ts | 371 | sentence-case | "Copy the NID_AUT cookie value" |
|
||||
```
|
||||
|
||||
에러 0개이면 "Required 이슈 없음 — 심사 봇 통과 예상"을 출력하고 종료한다.
|
||||
|
||||
## 5단계: 이슈 수정 (인자가 `fix`인 경우만)
|
||||
|
||||
발견된 Required 이슈를 하나씩 수정한다. 수정 전략:
|
||||
|
||||
- **sentence-case**: 기본 브랜드 목록(iOS, Obsidian, GitHub 등 57개)에 없는 고유명사는 소문자화하거나 제거. 첫 단어 위치면 대문자 허용. `enforceCamelCaseLower: true` 설정이므로 CamelCase도 변환 대상.
|
||||
- **empty catch**: `catch {` 또는 주석 추가
|
||||
- **as any**: 구체적 타입 또는 `as unknown as Type`
|
||||
- **no-explicit-any**: `unknown` 또는 구체적 타입으로 대체
|
||||
- **template literal never**: `String()` 래핑
|
||||
- **regex escape**: `new RegExp()` 사용
|
||||
- **async without await**: `async` 키워드 제거
|
||||
- **unused catch param**: `catch (error)` → `catch`
|
||||
|
||||
수정 후 ESLint를 다시 실행하여 에러 0개 확인. 남아있으면 반복.
|
||||
|
||||
## 6단계: 빌드 확인
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
빌드 실패 시 타입 에러 수정 후 재빌드.
|
||||
|
||||
## 7단계: 최종 보고
|
||||
|
||||
```
|
||||
## 최종 결과
|
||||
|
||||
- 발견된 Required 이슈: N개
|
||||
- 수정된 이슈: N개 (fix 모드인 경우)
|
||||
- 남은 이슈: N개
|
||||
- 빌드 상태: 성공/실패
|
||||
|
||||
### 수정된 파일 목록 (fix 모드인 경우)
|
||||
- src/ui/settings-tab.ts (2건 수정)
|
||||
- main.ts (1건 수정)
|
||||
```
|
||||
48
eslint.config.mjs
Normal file
48
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import obsidianmd from 'eslint-plugin-obsidianmd';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
// Get default recommended rules and customize sentence-case
|
||||
const rules = { ...obsidianmd.configs.recommended };
|
||||
|
||||
// Add project-specific brands to sentence-case rule
|
||||
rules['obsidianmd/ui/sentence-case'] = ['error', {
|
||||
enforceCamelCaseLower: true,
|
||||
brands: [
|
||||
// Default brands from obsidianmd plugin
|
||||
'iOS', 'iPadOS', 'macOS', 'Windows', 'Android', 'Linux',
|
||||
'Obsidian', 'Obsidian Sync', 'Obsidian Publish',
|
||||
'Google Drive', 'Dropbox', 'OneDrive', 'iCloud Drive',
|
||||
'YouTube', 'Slack', 'Discord', 'Telegram', 'WhatsApp', 'Twitter', 'X',
|
||||
'Readwise', 'Zotero', 'Excalidraw', 'Mermaid',
|
||||
'Markdown', 'LaTeX', 'JavaScript', 'TypeScript', 'Node.js',
|
||||
'npm', 'pnpm', 'Yarn', 'Git', 'GitHub',
|
||||
'GitLab', 'Notion', 'Evernote', 'Roam Research', 'Logseq',
|
||||
'Anki', 'Reddit', 'VS Code', 'Visual Studio Code',
|
||||
'IntelliJ IDEA', 'WebStorm', 'PyCharm',
|
||||
// Project-specific brands
|
||||
'Bareun.ai', 'OpenAI', 'Anthropic', 'Google', 'Ollama',
|
||||
'Claude', 'Gemini', 'GPT', 'CodeMirror',
|
||||
],
|
||||
acronyms: ['API', 'AI', 'URL', 'JSON', 'CSS', 'HTML', 'HTTP', 'HTTPS', 'LRU'],
|
||||
ignoreWords: ['Tab', 'Enter', 'Esc', 'Escape', 'Shift', 'Cmd', 'Ctrl', 'Alt', 'Space'],
|
||||
ignoreRegex: [
|
||||
'^(sk-|AIza|http|bareun-)',
|
||||
'^💡',
|
||||
],
|
||||
}];
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
plugins: {
|
||||
obsidianmd,
|
||||
},
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
},
|
||||
},
|
||||
rules,
|
||||
},
|
||||
);
|
||||
9
main.ts
9
main.ts
|
|
@ -45,8 +45,6 @@ export default class KoreanGrammarPlugin extends Plugin {
|
|||
} else {
|
||||
Logger.configureForDevelopment();
|
||||
}
|
||||
|
||||
Logger.log('Korean Grammar Assistant 플러그인 로딩 시작');
|
||||
|
||||
// 설정 로드
|
||||
await this.loadSettings();
|
||||
|
|
@ -65,7 +63,7 @@ export default class KoreanGrammarPlugin extends Plugin {
|
|||
InlineModeService.registerCommands(this);
|
||||
|
||||
// 리본 아이콘 추가
|
||||
this.addRibbonIcon("han-spellchecker", "Check Spelling", async () => {
|
||||
this.addRibbonIcon("han-spellchecker", "Check spelling", async () => {
|
||||
await this.orchestrator.execute();
|
||||
});
|
||||
|
||||
|
|
@ -178,8 +176,6 @@ export default class KoreanGrammarPlugin extends Plugin {
|
|||
|
||||
// 🔧 문서 전환 감지 이벤트 리스너 등록
|
||||
this.setupDocumentChangeListeners();
|
||||
|
||||
Logger.log('Korean Grammar Assistant 플러그인 로딩 완료');
|
||||
}
|
||||
|
||||
onunload() {
|
||||
|
|
@ -814,7 +810,6 @@ export default class KoreanGrammarPlugin extends Plugin {
|
|||
* 파일이나 리프가 변경될 때마다 InlineModeService 상태를 자동으로 정리
|
||||
*/
|
||||
private setupDocumentChangeListeners(): void {
|
||||
Logger.log('🔧 문서 전환 감지 이벤트 리스너 설정 중...');
|
||||
|
||||
// 파일 변경 감지 - 다른 파일로 이동할 때 트리거
|
||||
this.fileOpenListener = this.app.workspace.on('file-open', (file) => {
|
||||
|
|
@ -857,8 +852,6 @@ export default class KoreanGrammarPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
Logger.log('🔧 문서 전환 감지 이벤트 리스너 설정 완료');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -171,45 +171,21 @@ export class TokenCalculator implements IPopupServiceManager {
|
|||
// 모달 컨테이너 생성
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal-container token-warning-modal';
|
||||
modal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
`;
|
||||
|
||||
// 모달 콘텐츠
|
||||
const content = document.createElement('div');
|
||||
content.className = 'modal token-warning-content';
|
||||
content.style.cssText = `
|
||||
background: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
`;
|
||||
|
||||
// DOM API를 사용한 안전한 모달 생성
|
||||
const title = content.createDiv({ cls: 'modal-title' });
|
||||
title.style.cssText = 'font-size: 18px; font-weight: 600; margin-bottom: 16px; color: var(--text-normal);';
|
||||
const title = content.createDiv({ cls: 'modal-title token-warning-title' });
|
||||
title.textContent = '🚨 토큰 사용량 경고';
|
||||
|
||||
const modalContent = content.createDiv({ cls: 'modal-content' });
|
||||
modalContent.style.cssText = 'margin-bottom: 20px; color: var(--text-normal);';
|
||||
const modalContent = content.createDiv({ cls: 'modal-content token-warning-body' });
|
||||
|
||||
const p1 = modalContent.createEl('p');
|
||||
p1.textContent = 'AI 분석에 많은 토큰이 사용될 예정입니다:';
|
||||
|
||||
const infoBox = modalContent.createDiv();
|
||||
infoBox.style.cssText = 'background: var(--background-secondary); padding: 12px; border-radius: 4px; margin: 12px 0;';
|
||||
const infoBox = modalContent.createDiv({ cls: 'token-warning-info-box' });
|
||||
|
||||
const tokenInfo = infoBox.createDiv();
|
||||
const tokenStrong = tokenInfo.createEl('strong');
|
||||
|
|
@ -222,8 +198,7 @@ export class TokenCalculator implements IPopupServiceManager {
|
|||
costInfo.appendChild(document.createTextNode(` ${tokenUsage.estimatedCost}`));
|
||||
|
||||
if (tokenUsage.morphemeOptimized) {
|
||||
const optimizedInfo = infoBox.createDiv();
|
||||
optimizedInfo.style.cssText = 'color: var(--text-accent);';
|
||||
const optimizedInfo = infoBox.createDiv({ cls: 'token-warning-optimized' });
|
||||
const optimizedStrong = optimizedInfo.createEl('strong');
|
||||
optimizedStrong.textContent = '✓ 형태소 최적화 적용됨';
|
||||
}
|
||||
|
|
@ -231,11 +206,9 @@ export class TokenCalculator implements IPopupServiceManager {
|
|||
const p2 = modalContent.createEl('p');
|
||||
p2.textContent = '계속 진행하시겠습니까?';
|
||||
|
||||
const buttonContainer = content.createDiv({ cls: 'modal-button-container' });
|
||||
buttonContainer.style.cssText = 'display: flex; gap: 8px; justify-content: flex-end;';
|
||||
const buttonContainer = content.createDiv({ cls: 'modal-button-container token-warning-buttons' });
|
||||
|
||||
const proceedBtn = buttonContainer.createEl('button', { cls: 'mod-cta token-warning-proceed' });
|
||||
proceedBtn.style.cssText = 'background: var(--interactive-accent); color: var(--text-on-accent);';
|
||||
proceedBtn.textContent = '진행';
|
||||
|
||||
const cancelBtn = buttonContainer.createEl('button', { cls: 'token-warning-cancel' });
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { ShortcutHandler } from './ShortcutHandler';
|
|||
import { FocusManager } from './FocusManager';
|
||||
import { PopupState, KeyboardAction, KeyboardNavigationState, IPopupComponent, RenderContext } from '../types/PopupTypes';
|
||||
import { Logger } from '../../utils/logger';
|
||||
import { Scope } from 'obsidian';
|
||||
import { Platform, Scope } from 'obsidian';
|
||||
|
||||
export interface KeyboardEventResult {
|
||||
handled: boolean;
|
||||
|
|
@ -333,7 +333,7 @@ export class KeyboardManager implements IPopupComponent {
|
|||
}
|
||||
|
||||
private isMobile(): boolean {
|
||||
return /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
return Platform.isMobile;
|
||||
}
|
||||
|
||||
private createKeyboardHints(): HTMLElement {
|
||||
|
|
|
|||
|
|
@ -438,9 +438,8 @@ export class PopupLayoutManager implements IPopupComponent {
|
|||
private applyMobileStyles(): void {
|
||||
if (!this.containerElement) return;
|
||||
|
||||
// 동적 계산이 필요한 스타일만 JavaScript로 설정
|
||||
this.containerElement.style.setProperty('--kga-max-width', '100vw');
|
||||
this.containerElement.style.setProperty('--kga-max-height', '100vh');
|
||||
// CSS 클래스로 레이아웃 크기 적용 (--kga-max-width, --kga-max-height)
|
||||
// 값은 styles.css의 .korean-grammar-popup-mobile에서 정의됨
|
||||
|
||||
// 오류 요약 영역을 기본적으로 접힘
|
||||
if (this.context) {
|
||||
|
|
@ -454,9 +453,8 @@ export class PopupLayoutManager implements IPopupComponent {
|
|||
private applyTabletStyles(): void {
|
||||
if (!this.containerElement) return;
|
||||
|
||||
// 동적 계산이 필요한 스타일만 JavaScript로 설정
|
||||
this.containerElement.style.setProperty('--kga-max-width', '90vw');
|
||||
this.containerElement.style.setProperty('--kga-max-height', '90vh');
|
||||
// CSS 클래스로 레이아웃 크기 적용 (--kga-max-width, --kga-max-height)
|
||||
// 값은 styles.css의 .korean-grammar-popup-tablet에서 정의됨
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -465,9 +463,8 @@ export class PopupLayoutManager implements IPopupComponent {
|
|||
private applyDesktopStyles(): void {
|
||||
if (!this.containerElement) return;
|
||||
|
||||
// 동적 계산이 필요한 스타일만 JavaScript로 설정
|
||||
this.containerElement.style.setProperty('--kga-max-width', '1200px');
|
||||
this.containerElement.style.setProperty('--kga-max-height', '800px');
|
||||
// CSS 클래스로 레이아웃 크기 적용 (--kga-max-width, --kga-max-height)
|
||||
// 값은 styles.css의 .korean-grammar-popup-desktop에서 정의됨
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { Platform } from 'obsidian';
|
||||
import { PluginSettings } from '../types/interfaces';
|
||||
import { DEFAULT_INLINE_MODE_SETTINGS } from './settings';
|
||||
import { Logger } from '../utils/logger';
|
||||
|
|
@ -379,8 +380,8 @@ export class AdvancedSettingsService {
|
|||
settings: settings,
|
||||
metadata: {
|
||||
exportedBy: 'Korean Grammar Assistant',
|
||||
platform: navigator.platform,
|
||||
userAgent: navigator.userAgent.substring(0, 100)
|
||||
platform: Platform.isMacOS ? 'macOS' : Platform.isWin ? 'Windows' : Platform.isLinux ? 'Linux' : Platform.isIosApp ? 'iOS' : Platform.isAndroidApp ? 'Android' : 'Unknown',
|
||||
isMobile: Platform.isMobile
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2858,7 +2858,7 @@ export class CorrectionPopup extends BaseComponent {
|
|||
// 키보드 단축키 안내
|
||||
const keyboardHint = content.appendChild(document.createElement('div'));
|
||||
keyboardHint.className = 'token-warning-keyboard-hint';
|
||||
keyboardHint.textContent = '💡 키보드 단축키: Enter(진행), Esc(취소)';
|
||||
keyboardHint.textContent = '💡 키보드 단축키: enter(진행), esc(취소)';
|
||||
|
||||
return content;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1514,32 +1514,12 @@ export class InlineTooltip {
|
|||
return { width: 250, maxHeight: 200, minWidth: 200, fontSize: '14px' };
|
||||
}
|
||||
|
||||
// 🔧 [필수] DOM 크기 측정을 위한 임시 스타일 변경
|
||||
// ⚠️ Obsidian Guidelines Note: 이 inline style 사용은 정당화됨
|
||||
//
|
||||
// 이유:
|
||||
// - 브라우저는 실제 콘텐츠 크기 계산을 위해 요소가 렌더링되어야 함
|
||||
// - CSS만으로는 동적 콘텐츠의 자연스러운 크기를 측정할 수 없음
|
||||
// - visibility:hidden은 레이아웃 계산은 수행하지만 화면에 보이지 않음
|
||||
// - 측정 후 즉시 원래 상태로 복원되므로 사용자에게 영향 없음
|
||||
//
|
||||
// 대안 검토:
|
||||
// - CSS 클래스로 시도: 여전히 원래 값 저장/복원이 필요함
|
||||
// - getComputedStyle(): 실제 렌더링된 크기만 반환, 자연 크기 불가
|
||||
// - 결론: 이 방법이 유일한 해결책
|
||||
const originalDisplay = this.tooltip.style.display;
|
||||
const originalVisibility = this.tooltip.style.visibility;
|
||||
const originalPosition = this.tooltip.style.position;
|
||||
const originalWidth = this.tooltip.style.width;
|
||||
const originalCssWidth = this.tooltip.style.getPropertyValue('--kga-width');
|
||||
|
||||
this.tooltip.style.position = 'absolute';
|
||||
this.tooltip.style.visibility = 'hidden';
|
||||
this.tooltip.style.display = 'block';
|
||||
this.tooltip.style.width = 'auto';
|
||||
this.tooltip.style.setProperty('--kga-width', 'auto');
|
||||
this.tooltip.style.maxWidth = 'none';
|
||||
this.tooltip.style.minWidth = 'none';
|
||||
// 🔧 [필수] DOM 크기 측정을 위한 임시 CSS 클래스 적용
|
||||
// kga-tooltip-measuring 클래스가 !important로 측정에 필요한 스타일을 오버라이드:
|
||||
// position: absolute, visibility: hidden, display: block,
|
||||
// width: auto, --kga-width: auto, max-width: none, min-width: none
|
||||
// 클래스 제거 시 원래 스타일시트/인라인 값이 자동으로 복원됨
|
||||
this.tooltip.classList.add('kga-tooltip-measuring');
|
||||
|
||||
// 📏 실제 내용 크기 측정
|
||||
const naturalWidth = this.tooltip.scrollWidth;
|
||||
|
|
@ -1579,16 +1559,8 @@ export class InlineTooltip {
|
|||
// 내용이 너무 길면 최대 너비로 제한
|
||||
optimalWidth = Math.min(maxWidth, optimalWidth);
|
||||
|
||||
// 🔧 원래 스타일 복원
|
||||
this.tooltip.style.display = originalDisplay;
|
||||
this.tooltip.style.visibility = originalVisibility;
|
||||
this.tooltip.style.position = originalPosition;
|
||||
this.tooltip.style.width = originalWidth;
|
||||
if (originalCssWidth) {
|
||||
this.tooltip.style.setProperty('--kga-width', originalCssWidth);
|
||||
} else {
|
||||
this.tooltip.style.removeProperty('--kga-width');
|
||||
}
|
||||
// 🔧 원래 스타일 복원 - 측정용 CSS 클래스 제거로 자동 복원
|
||||
this.tooltip.classList.remove('kga-tooltip-measuring');
|
||||
|
||||
const result = {
|
||||
width: optimalWidth,
|
||||
|
|
@ -1617,20 +1589,7 @@ export class InlineTooltip {
|
|||
editor.classList.add('korean-tooltip-cursor-hidden');
|
||||
});
|
||||
|
||||
// 동적 CSS 스타일 추가 (한 번만)
|
||||
if (!document.getElementById('korean-tooltip-cursor-style')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'korean-tooltip-cursor-style';
|
||||
style.textContent = `
|
||||
.korean-tooltip-cursor-hidden .cm-cursor {
|
||||
display: none !important;
|
||||
}
|
||||
.korean-tooltip-cursor-hidden .cm-focused {
|
||||
caret-color: transparent !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
// CSS rules are defined in styles.css (.korean-tooltip-cursor-hidden)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -261,56 +261,11 @@ export class LoadingManager {
|
|||
private showProgressNotice(): void {
|
||||
this.hideNotice();
|
||||
|
||||
// 커스텀 토스트 컨테이너 생성
|
||||
// 커스텀 토스트 컨테이너 생성 (styles are defined in styles.css)
|
||||
const toastContainer = document.body.createEl('div', {
|
||||
cls: 'spell-check-toast',
|
||||
attr: {
|
||||
style: `
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
min-width: 280px;
|
||||
max-width: 320px;
|
||||
background: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 1000;
|
||||
font-family: var(--font-interface);
|
||||
font-size: 13px;
|
||||
animation: slideInUp 0.3s ease-out;
|
||||
`
|
||||
}
|
||||
cls: 'spell-check-toast'
|
||||
});
|
||||
|
||||
// 애니메이션 키프레임 추가
|
||||
if (!document.querySelector('#spell-check-toast-styles')) {
|
||||
const style = document.head.createEl('style', { attr: { id: 'spell-check-toast-styles' } });
|
||||
style.textContent = `
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes slideOutDown {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
this.progressElement = toastContainer;
|
||||
|
||||
// Notice 객체 흉내 (호환성을 위해)
|
||||
|
|
|
|||
|
|
@ -30,8 +30,7 @@ export class ModernSettingsTab extends PluginSettingTab {
|
|||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
// 통합 CSS 스타일 추가
|
||||
this.injectGlobalStyles();
|
||||
// CSS styles are defined in styles.css
|
||||
|
||||
// 메인 래퍼 생성
|
||||
const mainWrapper = containerEl.createEl('div', { cls: 'ksc-settings-wrapper' });
|
||||
|
|
@ -49,615 +48,6 @@ export class ModernSettingsTab extends PluginSettingTab {
|
|||
this.renderCurrentTab();
|
||||
}
|
||||
|
||||
/**
|
||||
* 통합 CSS 스타일 주입
|
||||
*/
|
||||
private injectGlobalStyles(): void {
|
||||
if (document.querySelector('#ksc-global-styles')) return;
|
||||
|
||||
const style = document.head.createEl('style', { attr: { id: 'ksc-global-styles' } });
|
||||
style.textContent = `
|
||||
/* =================================================================
|
||||
Korean Spell Checker - 통합 디자인 시스템
|
||||
================================================================= */
|
||||
|
||||
/* 기본 래퍼 */
|
||||
.ksc-settings-wrapper {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
font-family: var(--font-interface);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 헤더 */
|
||||
.ksc-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
padding: 24px 0;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.ksc-header-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--text-normal);
|
||||
margin: 0 0 8px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ksc-header-subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* 탭 네비게이션 */
|
||||
.ksc-tab-nav {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
background: var(--background-secondary);
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.ksc-tab-button {
|
||||
flex: 1;
|
||||
padding: 16px 12px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-interface);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ksc-tab-button:hover {
|
||||
color: var(--text-normal);
|
||||
background: var(--background-modifier-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.ksc-tab-button.active {
|
||||
background: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.ksc-tab-icon {
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.ksc-tab-label {
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 콘텐츠 컨테이너 */
|
||||
.ksc-content-container {
|
||||
background: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 12px;
|
||||
padding: 0;
|
||||
min-height: 400px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 섹션 */
|
||||
.ksc-section {
|
||||
padding: 24px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.ksc-section:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ksc-section-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ksc-section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-normal);
|
||||
margin: 0 0 4px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ksc-section-desc {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 정보 박스 */
|
||||
.ksc-info-box {
|
||||
background: var(--background-modifier-form-field);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-left: 4px solid var(--interactive-accent);
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
margin: 16px 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ksc-warning-box {
|
||||
background: var(--background-modifier-error);
|
||||
border: 1px solid var(--text-error);
|
||||
border-left: 4px solid var(--text-error);
|
||||
color: var(--text-error);
|
||||
}
|
||||
|
||||
.ksc-success-box {
|
||||
background: var(--background-modifier-success);
|
||||
border: 1px solid var(--text-success);
|
||||
border-left: 4px solid var(--text-success);
|
||||
color: var(--text-success);
|
||||
}
|
||||
|
||||
/* 설정 그룹 */
|
||||
.ksc-setting-group {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.ksc-setting-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 버튼 그룹 */
|
||||
.ksc-button-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* 예외 단어 태그 클라우드 */
|
||||
.ksc-tag-cloud {
|
||||
background: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin: 16px 0;
|
||||
min-height: 80px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: flex-start;
|
||||
align-content: flex-start;
|
||||
}
|
||||
|
||||
.ksc-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ksc-tag:hover {
|
||||
background: var(--interactive-accent-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.ksc-tag-remove {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ksc-tag-remove:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* 메트릭 디스플레이 */
|
||||
.ksc-metrics {
|
||||
background: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
margin: 16px 0;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* 모던한 버튼 스타일 */
|
||||
.ksc-button-group button {
|
||||
background: var(--interactive-normal);
|
||||
color: var(--text-normal);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.ksc-button-group button:hover {
|
||||
background: var(--interactive-hover);
|
||||
border-color: var(--interactive-accent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.ksc-button-group button.mod-cta {
|
||||
background: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
border-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.ksc-button-group button.mod-warning {
|
||||
background: var(--text-error);
|
||||
color: var(--text-on-accent);
|
||||
border-color: var(--text-error);
|
||||
}
|
||||
|
||||
/* 토글 개선 */
|
||||
.ksc-setting-group .setting-item {
|
||||
border: none !important;
|
||||
padding: 16px 0 !important;
|
||||
border-bottom: 1px solid var(--background-modifier-border) !important;
|
||||
}
|
||||
|
||||
.ksc-setting-group .setting-item:last-child {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
/* 입력 필드 스타일 개선 */
|
||||
.ksc-setting-group input[type="text"],
|
||||
.ksc-setting-group input[type="number"],
|
||||
.ksc-setting-group select,
|
||||
.ksc-setting-group textarea {
|
||||
background: var(--background-modifier-form-field) !important;
|
||||
border: 1px solid var(--background-modifier-border) !important;
|
||||
border-radius: 6px !important;
|
||||
padding: 8px 12px !important;
|
||||
font-size: 13px !important;
|
||||
line-height: 1.4 !important;
|
||||
transition: border-color 0.2s ease !important;
|
||||
}
|
||||
|
||||
/* 드롭다운 특별 처리 - 텍스트 잘림 방지 */
|
||||
.ksc-setting-group select {
|
||||
min-height: 36px !important;
|
||||
padding: 10px 12px !important;
|
||||
line-height: 1.2 !important;
|
||||
}
|
||||
|
||||
.ksc-setting-group input[type="text"]:focus,
|
||||
.ksc-setting-group input[type="number"]:focus,
|
||||
.ksc-setting-group select:focus,
|
||||
.ksc-setting-group textarea:focus {
|
||||
border-color: var(--interactive-accent) !important;
|
||||
outline: none !important;
|
||||
box-shadow: 0 0 0 2px rgba(var(--interactive-accent-rgb), 0.2) !important;
|
||||
}
|
||||
|
||||
/* 반응형 디자인 */
|
||||
@media (max-width: 768px) {
|
||||
.ksc-settings-wrapper {
|
||||
max-width: 100%;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.ksc-tab-nav {
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ksc-tab-button {
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
padding: 12px 16px;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.ksc-section {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.ksc-button-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* 접근성 */
|
||||
.ksc-tab-button:focus {
|
||||
outline: 2px solid var(--interactive-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* 새로운 검증 결과 스타일 - 더 깔끔하고 현대적 */
|
||||
.ksc-validation-result {
|
||||
border-radius: 8px;
|
||||
background: var(--background-secondary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ksc-status-card {
|
||||
padding: 16px 20px;
|
||||
background: var(--background-primary);
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.ksc-status-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ksc-status-icon {
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ksc-status-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.ksc-section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 12px;
|
||||
padding: 0 20px;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.ksc-problems-section {
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.ksc-problems-list {
|
||||
margin: 0;
|
||||
padding: 0 20px 16px 40px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.ksc-problems-list li {
|
||||
color: var(--text-error);
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 6px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ksc-problems-list li:before {
|
||||
content: "•";
|
||||
color: var(--text-error);
|
||||
position: absolute;
|
||||
left: -16px;
|
||||
}
|
||||
|
||||
.ksc-warnings-section {
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.ksc-warnings-list {
|
||||
margin: 0;
|
||||
padding: 0 20px 16px 40px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.ksc-warnings-list li {
|
||||
color: var(--text-warning);
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 6px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ksc-warnings-list li:before {
|
||||
content: "•";
|
||||
color: var(--text-warning);
|
||||
position: absolute;
|
||||
left: -16px;
|
||||
}
|
||||
|
||||
.ksc-suggestions-section {
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.ksc-suggestion-card {
|
||||
margin: 0 20px 12px 20px;
|
||||
padding: 12px;
|
||||
background: var(--background-modifier-form-field);
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid var(--interactive-accent);
|
||||
}
|
||||
|
||||
.ksc-suggestion-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-normal);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.ksc-suggestion-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.ksc-summary-section {
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.ksc-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.ksc-summary-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: var(--background-modifier-form-field);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ksc-summary-label {
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ksc-status-ok {
|
||||
color: var(--text-success);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ksc-status-error {
|
||||
color: var(--text-error);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ksc-status-warning {
|
||||
color: var(--text-warning);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ksc-status-disabled {
|
||||
color: var(--text-faint);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 로그 뷰어 스타일 */
|
||||
.ksc-log-filter {
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ksc-log-content {
|
||||
background: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.ksc-log-empty {
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.ksc-log-line {
|
||||
margin-bottom: 4px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ksc-log-error {
|
||||
background: rgba(255, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.ksc-log-warn {
|
||||
background: rgba(255, 165, 0, 0.1);
|
||||
}
|
||||
|
||||
.ksc-log-info {
|
||||
background: rgba(0, 123, 255, 0.05);
|
||||
}
|
||||
|
||||
.ksc-log-debug {
|
||||
background: rgba(108, 117, 125, 0.05);
|
||||
}
|
||||
|
||||
.ksc-log-timestamp {
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.ksc-log-message {
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.ksc-log-error .ksc-log-message {
|
||||
color: var(--text-error);
|
||||
}
|
||||
|
||||
.ksc-log-warn .ksc-log-message {
|
||||
color: var(--text-warning);
|
||||
}
|
||||
|
||||
.ksc-log-debug .ksc-log-message {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.ksc-log-data {
|
||||
color: var(--text-faint);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* 애니메이션 */
|
||||
.ksc-content-container {
|
||||
animation: ksc-fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes ksc-fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 탭 콘텐츠 렌더링
|
||||
*/
|
||||
|
|
@ -1132,7 +522,7 @@ export class ModernSettingsTab extends PluginSettingTab {
|
|||
|
||||
// 온도 설정
|
||||
new Setting(settingsGroup)
|
||||
.setName("창의성 (Temperature)")
|
||||
.setName("창의성 (temperature)")
|
||||
.setDesc("AI의 창의성 수준입니다. 낮을수록 일관된 결과, 높을수록 다양한 결과")
|
||||
.addSlider(slider => slider
|
||||
.setLimits(0, 1, 0.1)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
* HTML 처리 관련 유틸리티 함수들
|
||||
*/
|
||||
|
||||
import { Platform } from 'obsidian';
|
||||
|
||||
/**
|
||||
* HTML에 안전하게 텍스트를 삽입하기 위해 이스케이프 처리합니다.
|
||||
* @param text 이스케이프할 텍스트
|
||||
|
|
@ -106,9 +108,7 @@ export function temporaryHighlight(
|
|||
* @returns 모바일 여부
|
||||
*/
|
||||
export function isMobileDevice(): boolean {
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||
navigator.userAgent
|
||||
);
|
||||
return Platform.isMobile;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue