feat: add mobile shell and unified search
Some checks failed
ci / 정본·보안·린트·타입·테스트 (push) Failing after 50s
ci / 워크스페이스 빌드 검증 (push) Has been skipped
ci / Supabase Edge Functions + Cloudflare Worker (push) Successful in 20s
ci / .NET API 서버 테스트 (push) Successful in 12s
deploy-site / deploy (push) Successful in 39s
ci / 모바일 린트·타입·Jest (push) Failing after 3h10m31s
Some checks failed
ci / 정본·보안·린트·타입·테스트 (push) Failing after 50s
ci / 워크스페이스 빌드 검증 (push) Has been skipped
ci / Supabase Edge Functions + Cloudflare Worker (push) Successful in 20s
ci / .NET API 서버 테스트 (push) Successful in 12s
deploy-site / deploy (push) Successful in 39s
ci / 모바일 린트·타입·Jest (push) Failing after 3h10m31s
This commit is contained in:
parent
f505a03d34
commit
5ca3709b32
98 changed files with 5168 additions and 2093 deletions
|
|
@ -154,6 +154,10 @@ export const IPC_CHANNELS = {
|
|||
ADDED: 'history:added'
|
||||
},
|
||||
|
||||
UNIFIED_SEARCH: {
|
||||
QUERY: 'unifiedSearch:query'
|
||||
},
|
||||
|
||||
DICTIONARY: {
|
||||
GET_ALL: 'dictionary:getAll',
|
||||
ADD: 'dictionary:add',
|
||||
|
|
|
|||
|
|
@ -673,6 +673,37 @@ export interface HistorySearchParams {
|
|||
pageSize: number
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Unified search (기록 + 회의)
|
||||
// ============================================================
|
||||
|
||||
export type UnifiedSearchSource = 'record' | 'meeting'
|
||||
export type UnifiedSearchMatchedField = 'title' | 'content' | 'transcript'
|
||||
|
||||
/**
|
||||
* 기록과 회의를 한 목록에 표시하기 위한 읽기 전용 검색 결과.
|
||||
* 원본 데이터는 합치지 않고 source + id 로 각 상세 화면에 연결한다.
|
||||
*/
|
||||
export interface UnifiedSearchResult {
|
||||
source: UnifiedSearchSource
|
||||
id: string
|
||||
title: string | null
|
||||
excerpt: string
|
||||
matchedField: UnifiedSearchMatchedField
|
||||
occurredAt: number
|
||||
}
|
||||
|
||||
export interface UnifiedSearchParams {
|
||||
query: string
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface UnifiedSearchResponse {
|
||||
query: string
|
||||
results: UnifiedSearchResult[]
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface HistoryExportParams {
|
||||
format: 'json' | 'csv'
|
||||
from?: string
|
||||
|
|
|
|||
|
|
@ -257,17 +257,6 @@
|
|||
"license.modelOpus": "Opus",
|
||||
"license.quotaWeekly": "weekly",
|
||||
"license.quotaDaily": "daily",
|
||||
"mobile.works.title": "WORKS",
|
||||
"mobile.works.count": "{{count}} Funktionen",
|
||||
"mobile.works.searchPlaceholder": "Funktionen suchen",
|
||||
"mobile.works.pinned": "Angeheftet",
|
||||
"mobile.works.pin": "Anheften",
|
||||
"mobile.works.unpin": "Lösen",
|
||||
"mobile.works.emptySearch": "Keine passenden Funktionen.",
|
||||
"mobile.works.section.meetings": "Meetings-Arbeit",
|
||||
"mobile.works.section.team": "Team & Wissen",
|
||||
"mobile.works.section.data": "Daten & Hinweise",
|
||||
"mobile.works.historyDescription": "Transkripte von Aufnahmen und Importen suchen und verwalten.",
|
||||
"mobile.meetings.templateEmpty": "Keine Meeting-Dokumentvorlagen verfügbar. Sie können das Meeting ohne Vorlage starten.",
|
||||
"mobile.meetings.templateNone": "Ohne Vorlage starten",
|
||||
"mobile.meetings.templateNoneDesc": "Vorlage später bei der Dokumenterzeugung wählen",
|
||||
|
|
|
|||
|
|
@ -548,6 +548,14 @@
|
|||
"mobile.hist.metadata": "Metadata",
|
||||
"mobile.hist.mode": "Mode",
|
||||
"mobile.hist.status": "Status",
|
||||
"mobile.hist.mode.dictation": "Dictation",
|
||||
"mobile.hist.mode.translate": "Translation",
|
||||
"mobile.hist.mode.command": "Command",
|
||||
"mobile.hist.mode.caption": "Captions",
|
||||
"mobile.hist.mode.fileTranscription": "File transcription",
|
||||
"mobile.hist.status.completed": "Completed",
|
||||
"mobile.hist.status.cancelled": "Cancelled",
|
||||
"mobile.hist.status.error": "Error",
|
||||
"mobile.hist.language": "Language",
|
||||
"mobile.hist.duration": "Duration",
|
||||
"mobile.hist.model": "Model",
|
||||
|
|
@ -561,9 +569,24 @@
|
|||
"mobile.hist.audioResume": "Resume",
|
||||
"mobile.hist.audioStop": "Stop",
|
||||
"mobile.hist.audioPending": "Upload status: {{status}}",
|
||||
"mobile.hist.audioStatus.pending": "Pending",
|
||||
"mobile.hist.audioStatus.uploading": "Uploading",
|
||||
"mobile.hist.audioStatus.uploaded": "Uploaded",
|
||||
"mobile.hist.audioStatus.failed": "Failed",
|
||||
"mobile.hist.audioStatus.deleted": "Deleted",
|
||||
"mobile.hist.audioError": "The audio could not be opened safely.",
|
||||
"mobile.hist.jobs": "Processing jobs",
|
||||
"mobile.hist.jobsNone": "No processing jobs are linked.",
|
||||
"mobile.hist.jobKind.transcription": "Transcription",
|
||||
"mobile.hist.jobKind.summary": "Summary",
|
||||
"mobile.hist.jobKind.minutes": "Minutes",
|
||||
"mobile.hist.jobKind.diarization": "Speaker separation",
|
||||
"mobile.hist.jobKind.export": "Export",
|
||||
"mobile.hist.jobStatus.queued": "Queued",
|
||||
"mobile.hist.jobStatus.running": "Running",
|
||||
"mobile.hist.jobStatus.succeeded": "Completed",
|
||||
"mobile.hist.jobStatus.failed": "Failed",
|
||||
"mobile.hist.jobStatus.cancelled": "Cancelled",
|
||||
"mobile.hist.share": "Share",
|
||||
"mobile.hist.shareFailed": "The share sheet could not be opened.",
|
||||
"mobile.hist.editFailed": "The server did not confirm the edit.",
|
||||
|
|
@ -1134,6 +1157,7 @@
|
|||
"mobile.account.joinedAt": "JOINED",
|
||||
"mobile.account.lastLogin": "LAST SIGN-IN",
|
||||
"mobile.account.tier": "PLAN",
|
||||
"mobile.account.tierUnknown": "Unknown",
|
||||
"mobile.account.changePassword": "Change password",
|
||||
"mobile.account.logout": "Log out",
|
||||
"mobile.account.logoutConfirm": "Log out on this device?",
|
||||
|
|
@ -1529,6 +1553,11 @@
|
|||
"mobile.templates.deleteBody": "Delete the ‘{{name}}’ template? If it is selected, the safe builtin will be selected instead.",
|
||||
"mobile.templates.fieldCount": "{{count}} input fields",
|
||||
"mobile.templates.documentType": "Document type · {{type}}",
|
||||
"mobile.templates.type.minutes": "Meeting minutes",
|
||||
"mobile.templates.type.report": "Report",
|
||||
"mobile.templates.type.ideaNote": "Idea note",
|
||||
"mobile.templates.type.custom": "Custom",
|
||||
"mobile.templates.type.mindmap": "Mind map",
|
||||
"mobile.templates.loading": "Loading cloud templates",
|
||||
"mobile.templates.saving": "Saving",
|
||||
"mobile.templates.empty": "There are no templates of this type.",
|
||||
|
|
@ -1570,6 +1599,10 @@
|
|||
"mobile.memos.error.notFound": "The history item or tag was not found.",
|
||||
"mobile.memos.error.validation": "Tags must contain 1–80 non-blank characters.",
|
||||
"mobile.memos.error.server": "The memo tag operation or share could not be completed.",
|
||||
"mobile.ads.sponsored": "Sponsored",
|
||||
"mobile.ads.unavailable": "Ad unavailable",
|
||||
"mobile.ads.loading": "Loading ad",
|
||||
"mobile.ads.consent": "Checking ad consent",
|
||||
"mobile.admin.title": "Mobile administration",
|
||||
"mobile.admin.description": "Current server role: {{role}} · Every read and mutation is reauthorized by the server.",
|
||||
"mobile.admin.forbiddenTitle": "Administrator access required",
|
||||
|
|
@ -1640,17 +1673,6 @@
|
|||
"mobile.admin.error.timeout": "The server timed out. You can retry with the same idempotency key.",
|
||||
"mobile.admin.error.unavailable": "The safe server contract for this administrator feature is not deployed.",
|
||||
"mobile.admin.error.server": "The server response could not be verified safely, so the operation was stopped.",
|
||||
"mobile.works.title": "WORKS",
|
||||
"mobile.works.count": "{{count}} features",
|
||||
"mobile.works.searchPlaceholder": "Search features",
|
||||
"mobile.works.pinned": "PINNED",
|
||||
"mobile.works.pin": "Pin to top",
|
||||
"mobile.works.unpin": "Unpin",
|
||||
"mobile.works.emptySearch": "No matching features.",
|
||||
"mobile.works.section.meetings": "Meeting work",
|
||||
"mobile.works.section.team": "Team & knowledge",
|
||||
"mobile.works.section.data": "Data & alerts",
|
||||
"mobile.works.historyDescription": "Search and manage transcripts from recordings and imports.",
|
||||
"mobile.meetings.templateNone": "Start without template",
|
||||
"mobile.meetings.templateNoneDesc": "Pick a template later when generating documents",
|
||||
"keybinding.mouse.left": "Left Mouse Button",
|
||||
|
|
@ -2547,5 +2569,98 @@
|
|||
"tools.conversation.clearTitle": "Clear conversation history?",
|
||||
"tools.conversation.clearBody": "All {{count}} messages will be removed. This can't be undone.",
|
||||
"tools.conversation.clearConfirm": "Clear",
|
||||
"tools.conversation.clearFailed": "Couldn't clear the history. Try again."
|
||||
"tools.conversation.clearFailed": "Couldn't clear the history. Try again.",
|
||||
"mobile.nav.home": "Home",
|
||||
"mobile.nav.records": "Records",
|
||||
"mobile.nav.meetings": "Meetings",
|
||||
"mobile.nav.more": "More",
|
||||
"mobile.fab.open": "Open recording menu",
|
||||
"mobile.fab.close": "Close recording menu",
|
||||
"mobile.fab.dictation": "Dictation",
|
||||
"mobile.fab.dictationHint": "Opens the recording screen to start dictating.",
|
||||
"mobile.fab.meeting": "Meeting recording",
|
||||
"mobile.fab.meetingHint": "Create a new meeting, then record it from the meeting screen.",
|
||||
"mobile.fab.import": "Import audio file",
|
||||
"mobile.fab.importHint": "Pick an audio file on this device to transcribe.",
|
||||
"mobile.home.title": "Today",
|
||||
"mobile.home.recordHint": "Tap the record button at the bottom right to start dictation, a meeting recording, or a file import.",
|
||||
"mobile.home.summarySessions": "{{count}} today",
|
||||
"mobile.home.summaryWords": "{{count}} words",
|
||||
"mobile.home.summaryMinutes": "{{minutes}} min recorded",
|
||||
"mobile.home.summaryStreak": "{{count}}-day streak",
|
||||
"mobile.home.summaryNone": "Nothing recorded yet today",
|
||||
"mobile.home.loading": "Loading today's summary",
|
||||
"mobile.home.loadError": "Couldn't load today's summary.",
|
||||
"mobile.home.retry": "Try again",
|
||||
"mobile.home.recentRecords": "Recent records",
|
||||
"mobile.home.recentMeetings": "Recent meetings",
|
||||
"mobile.home.seeAll": "See all",
|
||||
"mobile.home.seeAllRecords": "See all records",
|
||||
"mobile.home.seeAllMeetings": "See all meetings",
|
||||
"mobile.home.noRecords": "No records yet. Use the record button to make your first one.",
|
||||
"mobile.home.noMeetings": "No meetings yet.",
|
||||
"mobile.home.meetingsLoadError": "Couldn't load recent meetings.",
|
||||
"mobile.home.freePlan": "You're on the free plan · See plans",
|
||||
"mobile.home.freePlanHint": "Opens plans and rewards.",
|
||||
"mobile.more.title": "More",
|
||||
"mobile.more.section.tools": "Tools",
|
||||
"mobile.more.section.account": "Account",
|
||||
"mobile.more.item.talk": "AI chat",
|
||||
"mobile.more.item.talkDescription": "Talk with AI by voice or text.",
|
||||
"mobile.more.item.commands": "Commands",
|
||||
"mobile.more.item.commandsDescription": "Create and run the AI commands you use often.",
|
||||
"mobile.more.item.dictionary": "Dictionary",
|
||||
"mobile.more.item.dictionaryDescription": "Register names and terms so they are recognized correctly.",
|
||||
"mobile.more.item.knowledge": "Knowledge",
|
||||
"mobile.more.item.knowledgeDescription": "Collect files and notes as searchable knowledge.",
|
||||
"mobile.more.item.templates": "Templates",
|
||||
"mobile.more.item.templatesDescription": "Manage dictation and meeting document formats.",
|
||||
"mobile.more.item.tags": "Record tags",
|
||||
"mobile.more.item.tagsDescription": "Tag records and find them by tag.",
|
||||
"mobile.more.item.teams": "Teams",
|
||||
"mobile.more.item.teamsDescription": "Manage team invites and shared meetings.",
|
||||
"mobile.more.item.actions": "Voice actions",
|
||||
"mobile.more.item.actionsDescription": "Confirm spoken requests, then run them as actions.",
|
||||
"mobile.more.item.notifications": "Notifications",
|
||||
"mobile.more.item.notificationsDescription": "Manage notification permission and this device's registration.",
|
||||
"mobile.more.item.portability": "Import & export data",
|
||||
"mobile.more.item.portabilityDescription": "Download or restore your account data.",
|
||||
"mobile.more.item.account": "Account",
|
||||
"mobile.more.item.accountDescription": "Manage your profile, sign-in methods, and password.",
|
||||
"mobile.more.item.plan": "Plan",
|
||||
"mobile.more.item.planDescription": "Current: {{tier}} · See plans and rewards.",
|
||||
"mobile.more.item.devices": "Devices",
|
||||
"mobile.more.item.devicesDescription": "See and disconnect devices linked to this account.",
|
||||
"mobile.more.item.settings": "Settings",
|
||||
"mobile.more.item.settingsDescription": "Appearance, language, recording, and sync.",
|
||||
"mobile.more.item.admin": "Admin",
|
||||
"mobile.more.item.adminDescription": "Manage users and reports.",
|
||||
"mobile.more.tier.free": "Free",
|
||||
"mobile.more.tier.pro": "Pro",
|
||||
"mobile.more.tier.pro_plus": "Pro+",
|
||||
"unifiedSearch.title": "Search",
|
||||
"unifiedSearch.homeHint": "Find records and meeting transcripts in one place.",
|
||||
"unifiedSearch.description": "Search record content, meeting titles, and transcripts together.",
|
||||
"unifiedSearch.placeholder": "Search records and meetings",
|
||||
"unifiedSearch.promptTitle": "Enter something to find",
|
||||
"unifiedSearch.promptDescription": "Search record content, meeting titles, and meeting transcripts.",
|
||||
"unifiedSearch.noResultsTitle": "No matching results",
|
||||
"unifiedSearch.noResultsDescription": "No records or meetings matched “{{query}}”.",
|
||||
"unifiedSearch.loading": "Searching",
|
||||
"unifiedSearch.loadErrorTitle": "Search failed",
|
||||
"unifiedSearch.loadErrorDescription": "There was a problem reading saved records and meetings. Try again.",
|
||||
"unifiedSearch.retry": "Search again",
|
||||
"unifiedSearch.resultCount": "{{count}} results",
|
||||
"unifiedSearch.resultsLabel": "Unified search results",
|
||||
"unifiedSearch.sourceRecord": "Record",
|
||||
"unifiedSearch.sourceMeeting": "Meeting",
|
||||
"unifiedSearch.untitledRecord": "Untitled record",
|
||||
"unifiedSearch.untitledMeeting": "Untitled meeting",
|
||||
"unifiedSearch.matchedTitle": "Title match",
|
||||
"unifiedSearch.matchedContent": "Content match",
|
||||
"unifiedSearch.matchedTranscript": "Transcript match",
|
||||
"unifiedSearch.openResult": "Open {{source}} “{{title}}”",
|
||||
"unifiedSearch.noPreview": "No preview available.",
|
||||
"unifiedSearch.back": "Back to previous screen",
|
||||
"unifiedSearch.signInRequired": "Sign in to use unified search."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -257,17 +257,6 @@
|
|||
"license.modelOpus": "Opus",
|
||||
"license.quotaWeekly": "weekly",
|
||||
"license.quotaDaily": "daily",
|
||||
"mobile.works.title": "WORKS",
|
||||
"mobile.works.count": "{{count}} funciones",
|
||||
"mobile.works.searchPlaceholder": "Buscar funciones",
|
||||
"mobile.works.pinned": "Fijado",
|
||||
"mobile.works.pin": "Fijar arriba",
|
||||
"mobile.works.unpin": "Quitar fijado",
|
||||
"mobile.works.emptySearch": "No hay funciones que coincidan.",
|
||||
"mobile.works.section.meetings": "Trabajo de reuniones",
|
||||
"mobile.works.section.team": "Equipo y conocimiento",
|
||||
"mobile.works.section.data": "Datos y avisos",
|
||||
"mobile.works.historyDescription": "Busca y gestiona transcripciones de grabaciones e importaciones.",
|
||||
"mobile.meetings.templateEmpty": "No hay plantillas de documento de reunión disponibles. Puedes iniciar la reunión sin una.",
|
||||
"mobile.meetings.templateNone": "Empezar sin plantilla",
|
||||
"mobile.meetings.templateNoneDesc": "Elige una plantilla al generar documentos",
|
||||
|
|
|
|||
|
|
@ -257,17 +257,6 @@
|
|||
"license.modelOpus": "Opus",
|
||||
"license.quotaWeekly": "weekly",
|
||||
"license.quotaDaily": "daily",
|
||||
"mobile.works.title": "WORKS",
|
||||
"mobile.works.count": "{{count}} fonctions",
|
||||
"mobile.works.searchPlaceholder": "Rechercher une fonction",
|
||||
"mobile.works.pinned": "Épinglé",
|
||||
"mobile.works.pin": "Épingler",
|
||||
"mobile.works.unpin": "Détacher",
|
||||
"mobile.works.emptySearch": "Aucune fonction correspondante.",
|
||||
"mobile.works.section.meetings": "Travail de réunion",
|
||||
"mobile.works.section.team": "Équipe et savoir",
|
||||
"mobile.works.section.data": "Données et alertes",
|
||||
"mobile.works.historyDescription": "Recherchez et gérez les transcriptions des enregistrements et importations.",
|
||||
"mobile.meetings.templateEmpty": "Aucun modèle de document de réunion disponible. Vous pouvez démarrer la réunion sans modèle.",
|
||||
"mobile.meetings.templateNone": "Démarrer sans modèle",
|
||||
"mobile.meetings.templateNoneDesc": "Choisissez un modèle lors de la génération de documents",
|
||||
|
|
|
|||
|
|
@ -257,17 +257,6 @@
|
|||
"license.modelOpus": "Opus",
|
||||
"license.quotaWeekly": "weekly",
|
||||
"license.quotaDaily": "daily",
|
||||
"mobile.works.title": "WORKS",
|
||||
"mobile.works.count": "機能 {{count}}件",
|
||||
"mobile.works.searchPlaceholder": "機能を検索",
|
||||
"mobile.works.pinned": "ピン留め",
|
||||
"mobile.works.pin": "ピン留め",
|
||||
"mobile.works.unpin": "ピン留め解除",
|
||||
"mobile.works.emptySearch": "一致する機能がありません。",
|
||||
"mobile.works.section.meetings": "会議ワーク",
|
||||
"mobile.works.section.team": "チーム・ナレッジ",
|
||||
"mobile.works.section.data": "データ・通知",
|
||||
"mobile.works.historyDescription": "録音とインポートの文字起こしを検索・管理します。",
|
||||
"mobile.meetings.templateEmpty": "利用可能な会議ドキュメントテンプレートがありません。テンプレートなしで会議を開始できます。",
|
||||
"mobile.meetings.templateNone": "テンプレートなしで開始",
|
||||
"mobile.meetings.templateNoneDesc": "ドキュメント生成時にテンプレートを選択します",
|
||||
|
|
|
|||
|
|
@ -483,17 +483,17 @@
|
|||
"theme.catppuccin": "카푸치노",
|
||||
"theme.dracula": "드라큘라",
|
||||
"mobile.dash.title": "D3RO-VOICE",
|
||||
"mobile.dash.sessionOverview": "SESSION OVERVIEW",
|
||||
"mobile.dash.today": "TODAY",
|
||||
"mobile.dash.words": "WORDS",
|
||||
"mobile.dash.streak": "STREAK",
|
||||
"mobile.dash.rec": "REC",
|
||||
"mobile.dash.min": "MIN",
|
||||
"mobile.dash.backend": "BACKEND",
|
||||
"mobile.dash.tier": "TIER",
|
||||
"mobile.dash.free": "FREE",
|
||||
"mobile.dash.sessionOverview": "오늘의 기록",
|
||||
"mobile.dash.today": "오늘",
|
||||
"mobile.dash.words": "단어",
|
||||
"mobile.dash.streak": "연속 사용",
|
||||
"mobile.dash.rec": "녹음",
|
||||
"mobile.dash.min": "분",
|
||||
"mobile.dash.backend": "처리 방식",
|
||||
"mobile.dash.tier": "요금제",
|
||||
"mobile.dash.free": "무료",
|
||||
"mobile.dash.tapToRecord": "REC 탭을 눌러 녹음 시작",
|
||||
"mobile.dash.usage": "USAGE",
|
||||
"mobile.dash.usage": "사용량",
|
||||
"mobile.dash.dictation": "받아쓰기",
|
||||
"mobile.dash.llmProcess": "LLM 처리",
|
||||
"mobile.dash.premiumQuota": "프리미엄 쿼터",
|
||||
|
|
@ -516,10 +516,10 @@
|
|||
"mobile.dash.noHistory": "완료된 전사 기록이 없습니다. 녹음 또는 파일 가져오기를 시작하세요.",
|
||||
"mobile.status.cloudModel": "클라우드 AI",
|
||||
"mobile.status.secureDataLink": "보안 데이터 연결",
|
||||
"mobile.hist.title": "HISTORY",
|
||||
"mobile.hist.all": "ALL",
|
||||
"mobile.hist.saved": "SAVED",
|
||||
"mobile.hist.processing": "PROC",
|
||||
"mobile.hist.title": "기록",
|
||||
"mobile.hist.all": "전체",
|
||||
"mobile.hist.saved": "저장됨",
|
||||
"mobile.hist.processing": "처리 중",
|
||||
"mobile.hist.empty": "아직 기록이 없습니다. 녹음을 시작해보세요!",
|
||||
"mobile.hist.emptySearch": "조건에 맞는 기록이 없습니다.",
|
||||
"mobile.hist.searchPlaceholder": "제목과 전사 내용 검색",
|
||||
|
|
@ -555,6 +555,14 @@
|
|||
"mobile.hist.metadata": "메타데이터",
|
||||
"mobile.hist.mode": "모드",
|
||||
"mobile.hist.status": "상태",
|
||||
"mobile.hist.mode.dictation": "받아쓰기",
|
||||
"mobile.hist.mode.translate": "번역",
|
||||
"mobile.hist.mode.command": "명령",
|
||||
"mobile.hist.mode.caption": "자막",
|
||||
"mobile.hist.mode.fileTranscription": "파일 전사",
|
||||
"mobile.hist.status.completed": "완료",
|
||||
"mobile.hist.status.cancelled": "취소됨",
|
||||
"mobile.hist.status.error": "오류",
|
||||
"mobile.hist.language": "언어",
|
||||
"mobile.hist.duration": "길이",
|
||||
"mobile.hist.model": "모델",
|
||||
|
|
@ -568,9 +576,24 @@
|
|||
"mobile.hist.audioResume": "계속 재생",
|
||||
"mobile.hist.audioStop": "정지",
|
||||
"mobile.hist.audioPending": "업로드 상태: {{status}}",
|
||||
"mobile.hist.audioStatus.pending": "대기 중",
|
||||
"mobile.hist.audioStatus.uploading": "업로드 중",
|
||||
"mobile.hist.audioStatus.uploaded": "업로드 완료",
|
||||
"mobile.hist.audioStatus.failed": "업로드 실패",
|
||||
"mobile.hist.audioStatus.deleted": "삭제됨",
|
||||
"mobile.hist.audioError": "오디오를 안전하게 열지 못했습니다.",
|
||||
"mobile.hist.jobs": "처리 작업",
|
||||
"mobile.hist.jobsNone": "연결된 처리 작업이 없습니다.",
|
||||
"mobile.hist.jobKind.transcription": "전사",
|
||||
"mobile.hist.jobKind.summary": "요약",
|
||||
"mobile.hist.jobKind.minutes": "회의록",
|
||||
"mobile.hist.jobKind.diarization": "화자 분리",
|
||||
"mobile.hist.jobKind.export": "내보내기",
|
||||
"mobile.hist.jobStatus.queued": "대기 중",
|
||||
"mobile.hist.jobStatus.running": "처리 중",
|
||||
"mobile.hist.jobStatus.succeeded": "완료",
|
||||
"mobile.hist.jobStatus.failed": "실패",
|
||||
"mobile.hist.jobStatus.cancelled": "취소됨",
|
||||
"mobile.hist.share": "공유",
|
||||
"mobile.hist.shareFailed": "공유 시트를 열지 못했습니다.",
|
||||
"mobile.hist.editFailed": "서버에서 편집을 확정하지 못했습니다.",
|
||||
|
|
@ -579,7 +602,7 @@
|
|||
"mobile.hist.savedSuccess": "서버에 저장했습니다.",
|
||||
"mobile.hist.validationTitle": "제목은 160자 이하여야 합니다.",
|
||||
"mobile.hist.validationOriginal": "원문은 비워둘 수 없습니다.",
|
||||
"mobile.dictionary.title": "CUSTOM DICTIONARY",
|
||||
"mobile.dictionary.title": "사용자 사전",
|
||||
"mobile.dictionary.count": "{{count}}개 단어",
|
||||
"mobile.dictionary.add": "단어 추가",
|
||||
"mobile.dictionary.search": "단어 또는 발음 검색",
|
||||
|
|
@ -614,11 +637,11 @@
|
|||
"mobile.dictionary.error.notFound": "이 단어가 없거나 접근 권한이 없습니다.",
|
||||
"mobile.dictionary.error.validation": "단어는 1~120자, 발음은 200자 이하로 입력하세요.",
|
||||
"mobile.dictionary.error.server": "서버에서 사전 작업을 확정하지 못했습니다.",
|
||||
"mobile.commands.title": "AI COMMANDS",
|
||||
"mobile.commands.title": "AI 명령어",
|
||||
"mobile.commands.count": "{{count}}개 서버 실행 프리셋",
|
||||
"mobile.commands.readOnlyBadge": "READ ONLY",
|
||||
"mobile.commands.readOnlyBadge": "읽기 전용",
|
||||
"mobile.commands.contractNotice": "현재 서버에는 사용자 명령 저장 스키마가 없습니다. 이 화면은 변경 가능한 목록으로 위장하지 않으며, 검증된 기본 명령을 인증된 Cloud AI에 실제로 실행하는 테스트 벤치만 제공합니다. 녹음 후 자동 실행과는 아직 연결되지 않았습니다.",
|
||||
"mobile.commands.available": "AVAILABLE SERVER COMMANDS",
|
||||
"mobile.commands.available": "사용 가능한 서버 명령어",
|
||||
"mobile.commands.translate.name": "영문 번역",
|
||||
"mobile.commands.translate.description": "입력 텍스트를 자연스러운 영어로 번역하고 번역문만 반환합니다.",
|
||||
"mobile.commands.summarize.name": "핵심 요약",
|
||||
|
|
@ -627,14 +650,14 @@
|
|||
"mobile.commands.formal.description": "원래 의미를 유지하며 격식 있는 비즈니스 문체로 다듬습니다.",
|
||||
"mobile.commands.explain.name": "코드 설명",
|
||||
"mobile.commands.explain.description": "코드의 역할과 주의점을 한국어로 설명합니다.",
|
||||
"mobile.commands.testBench": "AUTHENTICATED TEST BENCH",
|
||||
"mobile.commands.testBench": "인증된 명령 테스트",
|
||||
"mobile.commands.selected": "선택: {{name}}",
|
||||
"mobile.commands.inputPlaceholder": "서버 명령으로 처리할 실제 텍스트를 입력하세요.",
|
||||
"mobile.commands.run": "서버에서 실행",
|
||||
"mobile.commands.running": "실행 중...",
|
||||
"mobile.commands.cancel": "취소",
|
||||
"mobile.commands.serverProcessing": "인증 및 사용량 확인 후 Cloud AI가 처리 중입니다.",
|
||||
"mobile.commands.serverResult": "SERVER RESULT",
|
||||
"mobile.commands.serverResult": "서버 결과",
|
||||
"mobile.commands.retry": "다시 실행",
|
||||
"mobile.commands.error.auth": "로그인 세션이 만료됐습니다. 다시 로그인하세요.",
|
||||
"mobile.commands.error.invalid": "입력 내용이 비어 있거나 허용 길이를 초과했습니다.",
|
||||
|
|
@ -670,7 +693,7 @@
|
|||
"mobile.commands.syncError.conflict": "다른 기기에서 먼저 수정했습니다. 최신 목록을 다시 불러왔습니다.",
|
||||
"mobile.commands.syncError.notFound": "명령이 없거나 접근 권한이 없습니다.",
|
||||
"mobile.commands.syncError.server": "서버에서 명령 변경을 확정하지 못했습니다.",
|
||||
"mobile.knowledge.title": "KNOWLEDGE",
|
||||
"mobile.knowledge.title": "지식",
|
||||
"mobile.knowledge.description": "모바일 파일과 텍스트를 지식 문서로 동기화하고 실제 검색을 실행합니다.",
|
||||
"mobile.knowledge.count": "{{count}}개 문서",
|
||||
"mobile.knowledge.import": "텍스트 파일 가져오기",
|
||||
|
|
@ -696,7 +719,7 @@
|
|||
"mobile.knowledge.delete": "삭제",
|
||||
"mobile.knowledge.deleting": "삭제 중",
|
||||
"mobile.knowledge.retry": "다시 불러오기",
|
||||
"mobile.knowledge.library": "DOCUMENT LIBRARY",
|
||||
"mobile.knowledge.library": "문서 목록",
|
||||
"mobile.knowledge.loading": "서버 지식 문서를 불러오는 중",
|
||||
"mobile.knowledge.empty": "저장된 지식 문서가 없습니다. UTF-8 TXT 또는 Markdown 파일을 가져오세요.",
|
||||
"mobile.knowledge.emptySearch": "제목 검색 조건에 맞는 문서가 없습니다.",
|
||||
|
|
@ -705,9 +728,9 @@
|
|||
"mobile.knowledge.teamShared": "팀에서 공유한 문서 · 소유자만 인덱싱하거나 삭제할 수 있습니다.",
|
||||
"mobile.knowledge.indexing": "인덱싱 중",
|
||||
"mobile.knowledge.retryIndex": "인덱스 재시도",
|
||||
"mobile.knowledge.statusProcessing": "PROCESSING",
|
||||
"mobile.knowledge.statusReady": "READY",
|
||||
"mobile.knowledge.statusPending": "PENDING",
|
||||
"mobile.knowledge.statusProcessing": "처리 중",
|
||||
"mobile.knowledge.statusReady": "준비됨",
|
||||
"mobile.knowledge.statusPending": "대기 중",
|
||||
"mobile.knowledge.unknownDocument": "접근 가능한 문서",
|
||||
"mobile.knowledge.error.auth": "지식 베이스를 사용하려면 다시 로그인해야 합니다.",
|
||||
"mobile.knowledge.error.conflict": "다른 기기에서 문서가 먼저 변경되거나 삭제됐습니다. 최신 목록을 확인하세요.",
|
||||
|
|
@ -721,10 +744,10 @@
|
|||
"mobile.knowledge.error.validation": "1MB 이하의 유효한 UTF-8 TXT·Markdown 파일과 올바른 입력을 사용하세요.",
|
||||
"mobile.knowledge.error.cancelled": "지식 작업을 취소했습니다.",
|
||||
"mobile.knowledge.error.server": "지식 서버가 작업 완료를 확인하지 못했습니다.",
|
||||
"mobile.actions.title": "ACTIONS",
|
||||
"mobile.actions.title": "음성 액션",
|
||||
"mobile.actions.description": "자연어 명령을 안전한 모바일 작업으로 분석하고 확인 후 실행합니다.",
|
||||
"mobile.actions.subtitle": "확인 후 실행하는 모바일 안전 액션",
|
||||
"mobile.actions.confirmBadge": "CONFIRM FIRST",
|
||||
"mobile.actions.confirmBadge": "확인 후 실행",
|
||||
"mobile.actions.contractNotice": "Cloud AI는 명령을 엄격한 JSON으로 파싱만 합니다. 회의 생성·지식 검색·회의 메모 저장만 모바일 허용 목록에 있으며 항상 사용자가 확인해야 실행됩니다. 팀 초대와 OS·외부 앱 부작용은 자동 실행하지 않습니다. 실행 기록은 현재 이 기기에 계정별로 저장되며 서버 감사 로그는 아직 없습니다.",
|
||||
"mobile.actions.commandTitle": "자연어 명령",
|
||||
"mobile.actions.inputPlaceholder": "실행하려는 작업을 구체적으로 입력하세요",
|
||||
|
|
@ -734,8 +757,8 @@
|
|||
"mobile.actions.serverParsing": "인증된 Cloud AI가 액션 계약을 분석하고 있습니다.",
|
||||
"mobile.actions.executing": "실행 중",
|
||||
"mobile.actions.reviewTitle": "실행 전 확인",
|
||||
"mobile.actions.allowed": "ALLOWLISTED",
|
||||
"mobile.actions.blocked": "BLOCKED",
|
||||
"mobile.actions.allowed": "실행 가능",
|
||||
"mobile.actions.blocked": "실행 차단",
|
||||
"mobile.actions.type": "작업 유형",
|
||||
"mobile.actions.arguments": "서버가 해석한 값",
|
||||
"mobile.actions.rationale": "해석 근거",
|
||||
|
|
@ -776,20 +799,20 @@
|
|||
"mobile.actions.error.quota": "오늘 사용할 수 있는 Cloud AI 한도를 초과했습니다.",
|
||||
"mobile.actions.error.timeout": "서버 응답 시간이 초과됐습니다. 실행 결과를 확인한 뒤 재시도하세요.",
|
||||
"mobile.actions.error.server": "서버가 액션 완료를 확인하지 못했습니다.",
|
||||
"mobile.rec.title": "RECORDER",
|
||||
"mobile.rec.session": "REC_SESSION",
|
||||
"mobile.rec.ready": "READY",
|
||||
"mobile.rec.recording": "RECORDING",
|
||||
"mobile.rec.processing": "PROCESSING",
|
||||
"mobile.rec.done": "DONE",
|
||||
"mobile.rec.error": "ERROR",
|
||||
"mobile.rec.start": "START RECORDING",
|
||||
"mobile.rec.stop": "STOP",
|
||||
"mobile.rec.cancel": "CANCEL",
|
||||
"mobile.rec.newRecording": "NEW RECORDING",
|
||||
"mobile.rec.listening": "LISTENING_",
|
||||
"mobile.rec.realtime": "REAL-TIME TRANSCRIPT",
|
||||
"mobile.rec.initLog": "> initializing voice recording protocol...",
|
||||
"mobile.rec.title": "녹음",
|
||||
"mobile.rec.session": "녹음 중",
|
||||
"mobile.rec.ready": "준비",
|
||||
"mobile.rec.recording": "녹음 중",
|
||||
"mobile.rec.processing": "처리 중",
|
||||
"mobile.rec.done": "완료",
|
||||
"mobile.rec.error": "오류",
|
||||
"mobile.rec.start": "녹음 시작",
|
||||
"mobile.rec.stop": "녹음 중지",
|
||||
"mobile.rec.cancel": "취소",
|
||||
"mobile.rec.newRecording": "새 녹음",
|
||||
"mobile.rec.listening": "듣는 중",
|
||||
"mobile.rec.realtime": "전사 내용",
|
||||
"mobile.rec.initLog": "녹음을 준비하고 있습니다...",
|
||||
"mobile.rec.micDenied": "마이크 권한이 거부되었습니다",
|
||||
"mobile.rec.supabaseNotConfigured": "Supabase가 설정되지 않았습니다",
|
||||
"mobile.rec.loginRequired": "로그인이 필요합니다",
|
||||
|
|
@ -839,21 +862,21 @@
|
|||
"mobile.rec.processRecovered": "복구 오디오 처리",
|
||||
"mobile.rec.discardRecovered": "복구 오디오 삭제",
|
||||
"mobile.rec.retryNow": "지금 다시 시도",
|
||||
"mobile.talk.title": "TALK",
|
||||
"mobile.talk.live": "LIVE",
|
||||
"mobile.talk.you": "YOU",
|
||||
"mobile.talk.claude": "CLAUDE",
|
||||
"mobile.talk.title": "AI 대화",
|
||||
"mobile.talk.live": "대화",
|
||||
"mobile.talk.you": "나",
|
||||
"mobile.talk.claude": "AI",
|
||||
"mobile.talk.placeholder": "입력하거나 말하세요...",
|
||||
"mobile.talk.greeting": "안녕하세요! 무엇을 도와드릴까요?",
|
||||
"mobile.talk.cloudEngine": "보안 클라우드 AI",
|
||||
"mobile.talk.clear": "지우기",
|
||||
"mobile.talk.empty": "새 대화를 시작해.",
|
||||
"mobile.talk.empty": "새 대화를 시작하세요.",
|
||||
"mobile.talk.thinking": "답변을 생성하고 있습니다...",
|
||||
"mobile.talk.transcribing": "기기 또는 보안 클라우드에서 음성을 전사하고 있습니다...",
|
||||
"mobile.talk.recording": "녹음 중",
|
||||
"mobile.talk.releaseToSend": "손을 떼면 전사 후 전송해",
|
||||
"mobile.talk.releaseToSend": "손을 떼면 전사 후 전송합니다",
|
||||
"mobile.talk.holdToTalk": "누르고 말하기",
|
||||
"mobile.talk.pttHint": "마이크를 누른 채 말하고, 손을 떼면 전송해.",
|
||||
"mobile.talk.pttHint": "마이크를 누른 채 말하고, 손을 떼면 전송합니다.",
|
||||
"mobile.talk.cancel": "취소",
|
||||
"mobile.talk.playSpeech": "이 답변 음성으로 재생",
|
||||
"mobile.talk.stopSpeech": "음성 재생 중지",
|
||||
|
|
@ -865,21 +888,21 @@
|
|||
"mobile.talk.send": "메시지 보내기",
|
||||
"mobile.talk.retry": "다시 시도",
|
||||
"mobile.talk.error.auth": "세션이 만료되었습니다. 다시 로그인해 주세요.",
|
||||
"mobile.talk.error.invalid": "보낼 수 없는 메시지야. 내용을 줄여서 다시 시도해.",
|
||||
"mobile.talk.error.invalid": "보낼 수 없는 메시지입니다. 내용을 줄여서 다시 시도하세요.",
|
||||
"mobile.talk.error.quota": "오늘 사용할 수 있는 AI 대화 한도를 모두 사용했습니다.",
|
||||
"mobile.talk.error.model": "현재 요금제에서 선택한 AI 모델을 사용할 수 없습니다.",
|
||||
"mobile.talk.error.provider": "AI 공급자가 설정되지 않았거나 잠시 사용할 수 없습니다.",
|
||||
"mobile.talk.error.timeout": "AI 응답 시간이 초과되었습니다.",
|
||||
"mobile.talk.error.network": "네트워크 연결을 확인하고 다시 시도해.",
|
||||
"mobile.talk.error.network": "네트워크 연결을 확인하고 다시 시도하세요.",
|
||||
"mobile.talk.error.noSpeech": "음성이 인식되지 않았습니다. 마이크에 가까이에서 다시 말씀해 주세요.",
|
||||
"mobile.talk.error.response": "AI가 유효한 답변을 반환하지 않았습니다.",
|
||||
"mobile.talk.error.cancelled": "요청이 취소되었습니다.",
|
||||
"mobile.talk.error.micPermission": "눌러서 말하려면 마이크 권한이 필요해.",
|
||||
"mobile.talk.error.micPermission": "눌러서 말하려면 마이크 권한이 필요합니다.",
|
||||
"mobile.talk.error.recording": "마이크 녹음을 완료하지 못했습니다.",
|
||||
"mobile.talk.error.tooShort": "마이크를 조금 더 오래 누른 뒤 손을 떼 주세요.",
|
||||
"mobile.talk.error.sttQuota": "오늘 사용할 수 있는 음성 전사 한도를 모두 사용했습니다.",
|
||||
"mobile.talk.error.stt": "보안 클라우드와 기기 음성 인식이 모두 실패했습니다.",
|
||||
"mobile.talk.error.ttsUnavailable": "안드로이드 음성 엔진이 없거나 이 언어를 지원하지 않아.",
|
||||
"mobile.talk.error.ttsUnavailable": "안드로이드 음성 엔진이 없거나 이 언어를 지원하지 않습니다.",
|
||||
"mobile.talk.error.tts": "안드로이드가 이 답변을 음성으로 재생하지 못했습니다.",
|
||||
"mobile.talk.debugAudioE2e": "실제 오디오 E2E 실행",
|
||||
"mobile.talk.error.server": "AI 요청을 처리하지 못했습니다.",
|
||||
|
|
@ -915,17 +938,17 @@
|
|||
"mobile.report.error.network": "신고를 전송하지 못했습니다. 연결을 확인하고 다시 시도해 주세요.",
|
||||
"mobile.report.error.server": "서버가 신고 접수를 확인하지 못했습니다. 다시 시도해 주세요.",
|
||||
"mobile.set.title": "설정",
|
||||
"mobile.set.accountPlan": "ACCOUNT & PLAN",
|
||||
"mobile.set.backendConfig": "BACKEND CONFIG",
|
||||
"mobile.set.preferences": "PREFERENCES",
|
||||
"mobile.set.llmModel": "LLM Model",
|
||||
"mobile.set.llmDesc": "AI processing backend",
|
||||
"mobile.set.cloudStt": "Cloud STT",
|
||||
"mobile.set.cloudSttDesc": "Google Cloud Speech",
|
||||
"mobile.set.language": "Language",
|
||||
"mobile.set.autoPolish": "Auto Polish",
|
||||
"mobile.set.haptic": "Haptic Feedback",
|
||||
"mobile.set.logout": "LOGOUT",
|
||||
"mobile.set.accountPlan": "계정과 요금제",
|
||||
"mobile.set.backendConfig": "AI 설정",
|
||||
"mobile.set.preferences": "환경설정",
|
||||
"mobile.set.llmModel": "AI 모델",
|
||||
"mobile.set.llmDesc": "AI 처리에 사용할 모델",
|
||||
"mobile.set.cloudStt": "클라우드 음성 인식",
|
||||
"mobile.set.cloudSttDesc": "Google Cloud 음성 인식",
|
||||
"mobile.set.language": "언어",
|
||||
"mobile.set.autoPolish": "자동 다듬기",
|
||||
"mobile.set.haptic": "진동 피드백",
|
||||
"mobile.set.logout": "로그아웃",
|
||||
"mobile.set.logoutConfirm": "정말 로그아웃 하시겠습니까?",
|
||||
"mobile.set.loggingOut": "로그아웃 중...",
|
||||
"mobile.set.openAccount": "계정 정보 열기",
|
||||
|
|
@ -1053,13 +1076,13 @@
|
|||
"mobile.onboarding.completeHint": "선택한 설정을 저장하고 계정 화면으로 이동합니다.",
|
||||
"mobile.onboarding.skipHint": "튜토리얼을 완료로 표시하고 로그인 화면으로 이동합니다. 설정에서 다시 볼 수 있습니다.",
|
||||
"mobile.login.title": "D3RO-VOICE",
|
||||
"mobile.login.subtitle": "PRECISION VOICE INTELLIGENCE",
|
||||
"mobile.login.subtitle": "정확한 음성 기록",
|
||||
"mobile.login.google": "Google로 로그인",
|
||||
"mobile.login.apple": "Apple로 로그인",
|
||||
"mobile.login.github": "GitHub로 로그인",
|
||||
"mobile.login.or": "또는",
|
||||
"mobile.login.email": "EMAIL",
|
||||
"mobile.login.password": "PASSWORD",
|
||||
"mobile.login.email": "이메일",
|
||||
"mobile.login.password": "비밀번호",
|
||||
"mobile.login.signIn": "로그인",
|
||||
"mobile.login.noAccount": "계정이 없으신가요?",
|
||||
"mobile.login.forgotPassword": "비밀번호를 잊으셨나요?",
|
||||
|
|
@ -1081,7 +1104,7 @@
|
|||
"mobile.auth.localDataCleanupTitle": "기기 데이터 보호가 필요합니다",
|
||||
"mobile.auth.localDataCleanupBody": "이전 계정의 로컬 데이터를 완전히 삭제하지 못해 로그인과 앱 사용을 차단했습니다. 다시 시도해주세요.",
|
||||
"mobile.auth.localDataCleanupRetry": "보호 데이터 정리 다시 시도",
|
||||
"mobile.signup.title": "CREATE ACCOUNT",
|
||||
"mobile.signup.title": "계정 만들기",
|
||||
"mobile.signup.subtitle": "Google 또는 이메일로 D3RO-VOICE 계정을 만드세요",
|
||||
"mobile.signup.google": "Google로 가입",
|
||||
"mobile.signup.googleLoading": "Google 연결 중...",
|
||||
|
|
@ -1107,13 +1130,13 @@
|
|||
"mobile.signup.created": "계정이 생성되어 로그인되었습니다.",
|
||||
"mobile.signup.existing": "이미 가입된 이메일입니다. 로그인하거나 비밀번호 찾기를 이용해주세요.",
|
||||
"mobile.signup.invalidResponse": "인증 서버에서 유효한 사용자 정보를 반환하지 않았습니다.",
|
||||
"mobile.forgot.title": "RESET PASSWORD",
|
||||
"mobile.forgot.title": "비밀번호 재설정",
|
||||
"mobile.forgot.subtitle": "비밀번호 재설정 링크를 받으세요",
|
||||
"mobile.forgot.send": "재설정 메일 보내기",
|
||||
"mobile.forgot.sentTitle": "메일을 확인해주세요",
|
||||
"mobile.forgot.sentBody": "계정이 존재하면 {{email}}로 비밀번호 재설정 링크가 전송됩니다.",
|
||||
"mobile.forgot.resend": "재설정 메일 다시 보내기",
|
||||
"mobile.updatePassword.title": "NEW PASSWORD",
|
||||
"mobile.updatePassword.title": "새 비밀번호",
|
||||
"mobile.updatePassword.subtitle": "계정에 사용할 새 비밀번호를 설정하세요",
|
||||
"mobile.updatePassword.newPassword": "새 비밀번호",
|
||||
"mobile.updatePassword.confirmPassword": "새 비밀번호 확인",
|
||||
|
|
@ -1123,9 +1146,9 @@
|
|||
"mobile.updatePassword.completed": "비밀번호가 변경되었습니다",
|
||||
"mobile.updatePassword.completedBody": "새 비밀번호를 바로 사용할 수 있습니다.",
|
||||
"mobile.updatePassword.continue": "계속",
|
||||
"mobile.account.title": "ACCOUNT",
|
||||
"mobile.account.profile": "PROFILE",
|
||||
"mobile.account.accountInfo": "ACCOUNT INFO",
|
||||
"mobile.account.title": "계정",
|
||||
"mobile.account.profile": "프로필",
|
||||
"mobile.account.accountInfo": "계정 정보",
|
||||
"mobile.account.displayName": "표시 이름",
|
||||
"mobile.account.avatarUrl": "아바타 URL",
|
||||
"mobile.account.avatarPlaceholder": "https://example.com/avatar.png",
|
||||
|
|
@ -1141,10 +1164,11 @@
|
|||
"mobile.account.joinedAt": "가입일",
|
||||
"mobile.account.lastLogin": "최근 로그인",
|
||||
"mobile.account.tier": "구독 등급",
|
||||
"mobile.account.tierUnknown": "알 수 없음",
|
||||
"mobile.account.changePassword": "비밀번호 변경",
|
||||
"mobile.account.logout": "로그아웃",
|
||||
"mobile.account.logoutConfirm": "이 기기에서 로그아웃할까요?",
|
||||
"mobile.account.dangerZone": "DANGER ZONE",
|
||||
"mobile.account.dangerZone": "주의가 필요한 작업",
|
||||
"mobile.account.deleteWarning": "계정을 삭제하면 클라우드 데이터가 영구 삭제되며 되돌릴 수 없습니다. 이용 중인 Google Play 구독은 자동 취소되지 않으므로 Google Play에서 별도로 취소해야 합니다.",
|
||||
"mobile.account.deleteAcknowledge": "삭제의 영향을 이해했습니다.",
|
||||
"mobile.account.delete": "계정 삭제",
|
||||
|
|
@ -1536,6 +1560,11 @@
|
|||
"mobile.templates.deleteBody": "‘{{name}}’ 템플릿을 삭제할까요? 선택 중인 템플릿이면 안전한 기본 템플릿으로 돌아갑니다.",
|
||||
"mobile.templates.fieldCount": "입력 필드 {{count}}개",
|
||||
"mobile.templates.documentType": "문서 유형 · {{type}}",
|
||||
"mobile.templates.type.minutes": "회의록",
|
||||
"mobile.templates.type.report": "보고서",
|
||||
"mobile.templates.type.ideaNote": "아이디어 노트",
|
||||
"mobile.templates.type.custom": "사용자 지정",
|
||||
"mobile.templates.type.mindmap": "마인드맵",
|
||||
"mobile.templates.loading": "클라우드 템플릿을 불러오는 중",
|
||||
"mobile.templates.saving": "저장 중",
|
||||
"mobile.templates.empty": "이 유형의 템플릿이 없습니다.",
|
||||
|
|
@ -1577,6 +1606,10 @@
|
|||
"mobile.memos.error.notFound": "히스토리 또는 태그를 찾을 수 없습니다.",
|
||||
"mobile.memos.error.validation": "태그는 공백을 제외하고 1~80자로 입력하세요.",
|
||||
"mobile.memos.error.server": "메모 태그 작업 또는 공유를 완료하지 못했습니다.",
|
||||
"mobile.ads.sponsored": "광고",
|
||||
"mobile.ads.unavailable": "광고를 불러올 수 없습니다",
|
||||
"mobile.ads.loading": "광고 불러오는 중",
|
||||
"mobile.ads.consent": "광고 동의 확인 중",
|
||||
"mobile.admin.title": "모바일 관리자",
|
||||
"mobile.admin.description": "현재 서버 역할: {{role}} · 모든 조회와 변경 권한을 서버에서 다시 확인합니다.",
|
||||
"mobile.admin.forbiddenTitle": "관리자 권한이 없습니다",
|
||||
|
|
@ -1647,17 +1680,6 @@
|
|||
"mobile.admin.error.timeout": "서버 응답 시간이 초과됐습니다. 같은 멱등 키로 다시 시도할 수 있습니다.",
|
||||
"mobile.admin.error.unavailable": "이 관리자 기능의 안전한 서버 계약이 아직 배포되지 않았습니다.",
|
||||
"mobile.admin.error.server": "서버 응답을 안전하게 검증하지 못해 작업을 중단했습니다.",
|
||||
"mobile.works.title": "WORKS",
|
||||
"mobile.works.count": "기능 {{count}}개",
|
||||
"mobile.works.searchPlaceholder": "기능 검색",
|
||||
"mobile.works.pinned": "고정",
|
||||
"mobile.works.pin": "고정",
|
||||
"mobile.works.unpin": "고정 해제",
|
||||
"mobile.works.emptySearch": "조건에 맞는 기능이 없습니다.",
|
||||
"mobile.works.section.meetings": "회의 작업",
|
||||
"mobile.works.section.team": "팀 · 지식",
|
||||
"mobile.works.section.data": "데이터 · 알림",
|
||||
"mobile.works.historyDescription": "녹음과 가져오기의 전사 기록을 검색하고 관리합니다.",
|
||||
"mobile.meetings.templateNone": "템플릿 없이 시작",
|
||||
"mobile.meetings.templateNoneDesc": "문서 생성 시점에 템플릿을 선택합니다",
|
||||
"keybinding.mouse.left": "마우스 왼쪽 버튼",
|
||||
|
|
@ -2554,5 +2576,98 @@
|
|||
"tools.conversation.clearTitle": "대화 기록을 지울까요?",
|
||||
"tools.conversation.clearBody": "메시지 {{count}}개가 모두 지워지며 되돌릴 수 없습니다.",
|
||||
"tools.conversation.clearConfirm": "지우기",
|
||||
"tools.conversation.clearFailed": "대화 기록을 지우지 못했습니다. 다시 시도하세요."
|
||||
"tools.conversation.clearFailed": "대화 기록을 지우지 못했습니다. 다시 시도하세요.",
|
||||
"mobile.nav.home": "홈",
|
||||
"mobile.nav.records": "기록",
|
||||
"mobile.nav.meetings": "회의",
|
||||
"mobile.nav.more": "더보기",
|
||||
"mobile.fab.open": "녹음 시작 메뉴 열기",
|
||||
"mobile.fab.close": "녹음 시작 메뉴 닫기",
|
||||
"mobile.fab.dictation": "받아쓰기 녹음",
|
||||
"mobile.fab.dictationHint": "녹음 화면을 열어 받아쓰기를 시작합니다.",
|
||||
"mobile.fab.meeting": "회의 녹음",
|
||||
"mobile.fab.meetingHint": "새 회의를 만든 뒤 회의 화면에서 녹음합니다.",
|
||||
"mobile.fab.import": "오디오 파일 가져오기",
|
||||
"mobile.fab.importHint": "기기에 있는 오디오 파일을 골라 전사합니다.",
|
||||
"mobile.home.title": "오늘",
|
||||
"mobile.home.recordHint": "오른쪽 아래 녹음 버튼을 누르면 받아쓰기·회의 녹음·파일 가져오기를 시작할 수 있습니다.",
|
||||
"mobile.home.summarySessions": "오늘 {{count}}회",
|
||||
"mobile.home.summaryWords": "{{count}}단어",
|
||||
"mobile.home.summaryMinutes": "녹음 {{minutes}}분",
|
||||
"mobile.home.summaryStreak": "연속 {{count}}일",
|
||||
"mobile.home.summaryNone": "오늘은 아직 기록이 없습니다",
|
||||
"mobile.home.loading": "오늘 요약을 불러오고 있습니다",
|
||||
"mobile.home.loadError": "오늘 요약을 불러오지 못했습니다.",
|
||||
"mobile.home.retry": "다시 시도",
|
||||
"mobile.home.recentRecords": "최근 기록",
|
||||
"mobile.home.recentMeetings": "최근 회의",
|
||||
"mobile.home.seeAll": "모두 보기",
|
||||
"mobile.home.seeAllRecords": "기록 모두 보기",
|
||||
"mobile.home.seeAllMeetings": "회의 모두 보기",
|
||||
"mobile.home.noRecords": "아직 기록이 없습니다. 녹음 버튼으로 첫 기록을 남겨 보세요.",
|
||||
"mobile.home.noMeetings": "아직 회의가 없습니다.",
|
||||
"mobile.home.meetingsLoadError": "최근 회의를 불러오지 못했습니다.",
|
||||
"mobile.home.freePlan": "무료 요금제를 쓰고 있습니다 · 요금제 보기",
|
||||
"mobile.home.freePlanHint": "요금제와 리워드 화면으로 이동합니다.",
|
||||
"mobile.more.title": "더보기",
|
||||
"mobile.more.section.tools": "도구",
|
||||
"mobile.more.section.account": "계정",
|
||||
"mobile.more.item.talk": "AI 대화",
|
||||
"mobile.more.item.talkDescription": "말이나 글로 AI와 대화합니다.",
|
||||
"mobile.more.item.commands": "명령어",
|
||||
"mobile.more.item.commandsDescription": "자주 쓰는 AI 명령을 만들고 실행합니다.",
|
||||
"mobile.more.item.dictionary": "사전",
|
||||
"mobile.more.item.dictionaryDescription": "고유명사와 전문 용어를 잘 알아듣도록 등록합니다.",
|
||||
"mobile.more.item.knowledge": "지식",
|
||||
"mobile.more.item.knowledgeDescription": "파일과 글을 지식 문서로 모아 검색합니다.",
|
||||
"mobile.more.item.templates": "템플릿",
|
||||
"mobile.more.item.templatesDescription": "받아쓰기와 회의 문서의 형식을 관리합니다.",
|
||||
"mobile.more.item.tags": "기록 태그",
|
||||
"mobile.more.item.tagsDescription": "기록에 태그를 달고 태그로 찾습니다.",
|
||||
"mobile.more.item.teams": "팀",
|
||||
"mobile.more.item.teamsDescription": "팀원 초대와 공유 회의를 관리합니다.",
|
||||
"mobile.more.item.actions": "음성 액션",
|
||||
"mobile.more.item.actionsDescription": "말로 한 요청을 확인한 뒤 작업으로 실행합니다.",
|
||||
"mobile.more.item.notifications": "알림",
|
||||
"mobile.more.item.notificationsDescription": "알림 권한과 이 기기의 알림 등록을 관리합니다.",
|
||||
"mobile.more.item.portability": "데이터 가져오기·내보내기",
|
||||
"mobile.more.item.portabilityDescription": "계정 데이터를 내려받거나 복원합니다.",
|
||||
"mobile.more.item.account": "계정",
|
||||
"mobile.more.item.accountDescription": "프로필, 로그인 방법, 비밀번호를 관리합니다.",
|
||||
"mobile.more.item.plan": "요금제",
|
||||
"mobile.more.item.planDescription": "현재 {{tier}} · 요금제와 리워드를 확인합니다.",
|
||||
"mobile.more.item.devices": "기기",
|
||||
"mobile.more.item.devicesDescription": "이 계정에 연결된 기기를 확인하고 해제합니다.",
|
||||
"mobile.more.item.settings": "설정",
|
||||
"mobile.more.item.settingsDescription": "화면, 언어, 녹음, 동기화를 설정합니다.",
|
||||
"mobile.more.item.admin": "관리자",
|
||||
"mobile.more.item.adminDescription": "사용자와 신고를 관리합니다.",
|
||||
"mobile.more.tier.free": "무료",
|
||||
"mobile.more.tier.pro": "Pro",
|
||||
"mobile.more.tier.pro_plus": "Pro+",
|
||||
"unifiedSearch.title": "통합 검색",
|
||||
"unifiedSearch.homeHint": "기록과 회의 전사를 한 번에 찾습니다.",
|
||||
"unifiedSearch.description": "기록 본문과 회의 제목·전사를 함께 찾습니다.",
|
||||
"unifiedSearch.placeholder": "기록과 회의 검색",
|
||||
"unifiedSearch.promptTitle": "찾을 내용을 입력하세요",
|
||||
"unifiedSearch.promptDescription": "기록 본문, 회의 제목, 회의 전사에서 검색합니다.",
|
||||
"unifiedSearch.noResultsTitle": "일치하는 결과가 없습니다",
|
||||
"unifiedSearch.noResultsDescription": "‘{{query}}’와 일치하는 기록이나 회의를 찾지 못했습니다.",
|
||||
"unifiedSearch.loading": "검색하는 중",
|
||||
"unifiedSearch.loadErrorTitle": "검색하지 못했습니다",
|
||||
"unifiedSearch.loadErrorDescription": "저장된 기록과 회의를 읽는 중 문제가 생겼습니다. 다시 시도해 주세요.",
|
||||
"unifiedSearch.retry": "다시 검색",
|
||||
"unifiedSearch.resultCount": "결과 {{count}}건",
|
||||
"unifiedSearch.resultsLabel": "통합 검색 결과",
|
||||
"unifiedSearch.sourceRecord": "기록",
|
||||
"unifiedSearch.sourceMeeting": "회의",
|
||||
"unifiedSearch.untitledRecord": "제목 없는 기록",
|
||||
"unifiedSearch.untitledMeeting": "제목 없는 회의",
|
||||
"unifiedSearch.matchedTitle": "제목 일치",
|
||||
"unifiedSearch.matchedContent": "본문 일치",
|
||||
"unifiedSearch.matchedTranscript": "전사 일치",
|
||||
"unifiedSearch.openResult": "{{source}} ‘{{title}}’ 열기",
|
||||
"unifiedSearch.noPreview": "미리 볼 내용이 없습니다.",
|
||||
"unifiedSearch.back": "이전 화면으로",
|
||||
"unifiedSearch.signInRequired": "통합 검색을 사용하려면 로그인해 주세요."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -257,17 +257,6 @@
|
|||
"license.modelOpus": "Opus",
|
||||
"license.quotaWeekly": "weekly",
|
||||
"license.quotaDaily": "daily",
|
||||
"mobile.works.title": "WORKS",
|
||||
"mobile.works.count": "{{count}} funções",
|
||||
"mobile.works.searchPlaceholder": "Pesquisar funções",
|
||||
"mobile.works.pinned": "Fixado",
|
||||
"mobile.works.pin": "Fixar no topo",
|
||||
"mobile.works.unpin": "Desafixar",
|
||||
"mobile.works.emptySearch": "Nenhuma função correspondente.",
|
||||
"mobile.works.section.meetings": "Trabalho de reuniões",
|
||||
"mobile.works.section.team": "Equipe e conhecimento",
|
||||
"mobile.works.section.data": "Dados e alertas",
|
||||
"mobile.works.historyDescription": "Pesquise e gerencie transcrições de gravações e importações.",
|
||||
"mobile.meetings.templateEmpty": "Nenhuma template de documento de reunião disponível. Você pode iniciar a reunião sem uma.",
|
||||
"mobile.meetings.templateNone": "Começar sem template",
|
||||
"mobile.meetings.templateNoneDesc": "Escolha uma template ao gerar documentos",
|
||||
|
|
|
|||
|
|
@ -257,17 +257,6 @@
|
|||
"license.modelOpus": "Opus",
|
||||
"license.quotaWeekly": "weekly",
|
||||
"license.quotaDaily": "daily",
|
||||
"mobile.works.title": "WORKS",
|
||||
"mobile.works.count": "{{count}} функций",
|
||||
"mobile.works.searchPlaceholder": "Поиск функций",
|
||||
"mobile.works.pinned": "Закреплено",
|
||||
"mobile.works.pin": "Закрепить",
|
||||
"mobile.works.unpin": "Открепить",
|
||||
"mobile.works.emptySearch": "Подходящих функций нет.",
|
||||
"mobile.works.section.meetings": "Работа с встречами",
|
||||
"mobile.works.section.team": "Команда и знания",
|
||||
"mobile.works.section.data": "Данные и оповещения",
|
||||
"mobile.works.historyDescription": "Поиск и управление расшифровками записей и импорта.",
|
||||
"mobile.meetings.templateEmpty": "Нет доступных шаблонов документов встреч. Можно начать встречу без шаблона.",
|
||||
"mobile.meetings.templateNone": "Начать без шаблона",
|
||||
"mobile.meetings.templateNoneDesc": "Шаблон можно выбрать при создании документов",
|
||||
|
|
|
|||
|
|
@ -257,17 +257,6 @@
|
|||
"license.modelOpus": "Opus",
|
||||
"license.quotaWeekly": "weekly",
|
||||
"license.quotaDaily": "daily",
|
||||
"mobile.works.title": "WORKS",
|
||||
"mobile.works.count": "{{count}} ฟีเจอร์",
|
||||
"mobile.works.searchPlaceholder": "ค้นหาฟีเจอร์",
|
||||
"mobile.works.pinned": "ปักหมุดแล้ว",
|
||||
"mobile.works.pin": "ปักหมุดไว้ด้านบน",
|
||||
"mobile.works.unpin": "ยกเลิกปักหมุด",
|
||||
"mobile.works.emptySearch": "ไม่พบฟีเจอร์ที่ตรงกัน",
|
||||
"mobile.works.section.meetings": "งานประชุม",
|
||||
"mobile.works.section.team": "ทีมและความรู้",
|
||||
"mobile.works.section.data": "ข้อมูลและการแจ้งเตือน",
|
||||
"mobile.works.historyDescription": "ค้นหาและจัดการบทถอดความจากการบันทึกเสียงและการนำเข้า",
|
||||
"mobile.meetings.templateEmpty": "ไม่มีเทมเพลตเอกสารประชุมที่ใช้ได้ เริ่มประชุมโดยไม่ใช้เทมเพลตได้",
|
||||
"mobile.meetings.templateNone": "เริ่มโดยไม่ใช้เทมเพลต",
|
||||
"mobile.meetings.templateNoneDesc": "เลือกเทมเพลตภายหลังเมื่อสร้างเอกสาร",
|
||||
|
|
|
|||
|
|
@ -257,17 +257,6 @@
|
|||
"license.modelOpus": "Opus",
|
||||
"license.quotaWeekly": "weekly",
|
||||
"license.quotaDaily": "daily",
|
||||
"mobile.works.title": "WORKS",
|
||||
"mobile.works.count": "{{count}} tính năng",
|
||||
"mobile.works.searchPlaceholder": "Tìm tính năng",
|
||||
"mobile.works.pinned": "Đã ghim",
|
||||
"mobile.works.pin": "Ghim lên đầu",
|
||||
"mobile.works.unpin": "Bỏ ghim",
|
||||
"mobile.works.emptySearch": "Không có tính năng phù hợp.",
|
||||
"mobile.works.section.meetings": "Công việc họp",
|
||||
"mobile.works.section.team": "Nhóm & kiến thức",
|
||||
"mobile.works.section.data": "Dữ liệu & thông báo",
|
||||
"mobile.works.historyDescription": "Tìm kiếm và quản lý bản chép lời từ bản ghi và tệp nhập.",
|
||||
"mobile.meetings.templateEmpty": "Không có mẫu tài liệu họp nào. Bạn có thể bắt đầu cuộc họp mà không cần mẫu.",
|
||||
"mobile.meetings.templateNone": "Bắt đầu không cần mẫu",
|
||||
"mobile.meetings.templateNoneDesc": "Chọn mẫu sau khi khi tạo tài liệu",
|
||||
|
|
|
|||
|
|
@ -257,17 +257,6 @@
|
|||
"license.modelOpus": "Opus",
|
||||
"license.quotaWeekly": "weekly",
|
||||
"license.quotaDaily": "daily",
|
||||
"mobile.works.title": "WORKS",
|
||||
"mobile.works.count": "{{count}} 個功能",
|
||||
"mobile.works.searchPlaceholder": "搜尋功能",
|
||||
"mobile.works.pinned": "已釘選",
|
||||
"mobile.works.pin": "釘選",
|
||||
"mobile.works.unpin": "取消釘選",
|
||||
"mobile.works.emptySearch": "沒有符合的功能。",
|
||||
"mobile.works.section.meetings": "會議工作",
|
||||
"mobile.works.section.team": "團隊與知識",
|
||||
"mobile.works.section.data": "資料與通知",
|
||||
"mobile.works.historyDescription": "搜尋和管理錄音與匯入的逐字稿。",
|
||||
"mobile.meetings.templateEmpty": "沒有可用的會議文件範本。可以不使用範本開始會議。",
|
||||
"mobile.meetings.templateNone": "不使用範本開始",
|
||||
"mobile.meetings.templateNoneDesc": "稍後產生文件時再選擇範本",
|
||||
|
|
|
|||
|
|
@ -257,17 +257,6 @@
|
|||
"license.modelOpus": "Opus",
|
||||
"license.quotaWeekly": "weekly",
|
||||
"license.quotaDaily": "daily",
|
||||
"mobile.works.title": "WORKS",
|
||||
"mobile.works.count": "{{count}} 个功能",
|
||||
"mobile.works.searchPlaceholder": "搜索功能",
|
||||
"mobile.works.pinned": "已固定",
|
||||
"mobile.works.pin": "置顶",
|
||||
"mobile.works.unpin": "取消置顶",
|
||||
"mobile.works.emptySearch": "没有匹配的功能。",
|
||||
"mobile.works.section.meetings": "会议工作",
|
||||
"mobile.works.section.team": "团队与知识",
|
||||
"mobile.works.section.data": "数据与通知",
|
||||
"mobile.works.historyDescription": "搜索和管理录音与导入的转写记录。",
|
||||
"mobile.meetings.templateEmpty": "没有可用的会议文档模板。可以不使用模板开始会议。",
|
||||
"mobile.meetings.templateNone": "不使用模板开始",
|
||||
"mobile.meetings.templateNoneDesc": "稍后生成文档时再选择模板",
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@ export function FilterChip({
|
|||
minHeight: 48,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: active ? palette.accent.main : palette.bg.card,
|
||||
backgroundColor: active ? palette.accent.solid : palette.bg.card,
|
||||
borderWidth: 1,
|
||||
borderColor: active ? palette.accent.main : palette.border.default,
|
||||
borderColor: active ? palette.accent.solid : palette.border.default,
|
||||
borderRadius: d3roNativeRadius.pill,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 6
|
||||
|
|
@ -43,8 +43,8 @@ export function FilterChip({
|
|||
>
|
||||
<PhosphorText
|
||||
variant="small"
|
||||
color={active ? 'primary' : 'muted'}
|
||||
style={active ? { color: palette.text.onAccent ?? palette.bg.app } : undefined}
|
||||
color={active ? 'primary' : 'tertiary'}
|
||||
style={active ? { color: palette.accent.ink } : undefined}
|
||||
>
|
||||
{label}
|
||||
</PhosphorText>
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
// packages/ui-native/src/components/Header.tsx
|
||||
// RN Header — D3RO styled screen header with LEDs
|
||||
// safe-area insets handled by caller (paddingTop prop or wrapper)
|
||||
|
||||
import { View, type ViewStyle, type StyleProp } from 'react-native'
|
||||
import { Led } from './Led'
|
||||
import { PhosphorText } from './PhosphorText'
|
||||
import { useNativePalette } from '../theme-context'
|
||||
|
||||
export interface HeaderProps {
|
||||
title: string
|
||||
showBorder?: boolean
|
||||
rightContent?: React.ReactNode
|
||||
paddingTop?: number
|
||||
style?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
export function Header({
|
||||
title,
|
||||
showBorder = true,
|
||||
rightContent,
|
||||
paddingTop = 0,
|
||||
style
|
||||
}: HeaderProps): React.ReactElement {
|
||||
const palette = useNativePalette()
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: paddingTop + 8,
|
||||
paddingBottom: 12,
|
||||
borderBottomWidth: showBorder ? 1 : 0,
|
||||
borderBottomColor: palette.border.subtle
|
||||
},
|
||||
style
|
||||
]}
|
||||
>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
||||
<Led color="amber" size={8} />
|
||||
<PhosphorText
|
||||
accessibilityRole="header"
|
||||
variant="label"
|
||||
color="muted"
|
||||
>
|
||||
{title}
|
||||
</PhosphorText>
|
||||
</View>
|
||||
{rightContent ?? (
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
||||
<Led color="green" size={6} />
|
||||
<Led color="amber" size={6} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,35 +1,37 @@
|
|||
// packages/ui-native/src/components/Led.tsx
|
||||
// RN용 Led — 작은 발광 원
|
||||
// v4: 상태 점. 번짐(글로우) 없음. 색만으로 상태를 말하지 않는다 — 옆에 글자를 둔다.
|
||||
|
||||
import { View, type ViewStyle } from 'react-native'
|
||||
import { useNativePalette } from '../theme-context'
|
||||
|
||||
export interface LedProps {
|
||||
color?: 'amber' | 'green' | 'red' | 'orange' | 'blue'
|
||||
/** 'amber' 는 v1 호환 별칭 — 현재 테마의 액센트 */
|
||||
color?: 'amber' | 'green' | 'red' | 'orange' | 'blue' | 'off'
|
||||
on?: boolean
|
||||
size?: number
|
||||
}
|
||||
|
||||
export function Led({ color = 'amber', on = true, size = 8 }: LedProps): React.ReactElement {
|
||||
export function Led({ color = 'off', on = true, size = 8 }: LedProps): React.ReactElement {
|
||||
const palette = useNativePalette()
|
||||
const colorMap: Record<NonNullable<LedProps['color']>, string> = {
|
||||
amber: palette.accent.main,
|
||||
green: palette.tag.green,
|
||||
red: palette.tag.red,
|
||||
orange: palette.tag.orange,
|
||||
blue: palette.tag.blue
|
||||
green: palette.status.success,
|
||||
red: palette.status.danger,
|
||||
orange: palette.status.warning,
|
||||
blue: palette.status.info,
|
||||
off: palette.led.off
|
||||
}
|
||||
const c = colorMap[color]
|
||||
const style: ViewStyle = {
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: size / 2,
|
||||
backgroundColor: on ? c : palette.led.off,
|
||||
shadowColor: c,
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: on ? 0.8 : 0,
|
||||
shadowRadius: on ? 4 : 0,
|
||||
elevation: on ? 2 : 0
|
||||
backgroundColor: on ? colorMap[color] : palette.led.off
|
||||
}
|
||||
return <View style={style} />
|
||||
return (
|
||||
<View
|
||||
style={style}
|
||||
accessibilityElementsHidden
|
||||
importantForAccessibility="no-hide-descendants"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// packages/ui-native/src/components/MetalCard.tsx
|
||||
// RN용 MetalCard — View + 섀시 스타일
|
||||
// v4: 평평한 카드 — 표면 한 단 + 1px 선. 그림자·광택 없음. (데스크톱 MetalCard 와 같은 방식)
|
||||
// inset: 카드 안의 한 단 들어간 영역 (카드 안의 카드가 아니라 우묵한 판).
|
||||
|
||||
import { View, type ViewProps, type ViewStyle, type StyleProp } from 'react-native'
|
||||
import { d3roNativeRadius } from '../theme'
|
||||
|
|
@ -15,16 +16,10 @@ export function MetalCard({ children, inset = false, style, ...rest }: MetalCard
|
|||
const baseStyle: ViewStyle = {
|
||||
backgroundColor: inset ? palette.bg.inset : palette.bg.card,
|
||||
borderRadius: inset ? d3roNativeRadius.inner : d3roNativeRadius.card,
|
||||
padding: inset ? 6 : 16,
|
||||
borderWidth: inset ? 0 : 1,
|
||||
borderColor: palette.border.subtle,
|
||||
overflow: 'hidden',
|
||||
// RN 그림자
|
||||
shadowColor: palette.shadow,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: inset ? 0 : 0.25,
|
||||
shadowRadius: inset ? 0 : 8,
|
||||
elevation: inset ? 0 : 4
|
||||
borderWidth: 1,
|
||||
borderColor: inset ? palette.border.subtle : palette.border.default,
|
||||
padding: inset ? 12 : 16,
|
||||
overflow: 'hidden'
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,42 +1,59 @@
|
|||
// packages/ui-native/src/components/PhosphorText.tsx
|
||||
// RN용 PhosphorText — v3: 시스템 산세리프 기반 (한글은 Apple SD Gothic Neo /
|
||||
// Noto Sans KR 자동 폴백). 텍스트 글로우는 제거하고 명도로 위계를 만든다.
|
||||
// RN 텍스트 — v4: 시스템 산세리프(한글은 Apple SD Gothic Neo / Noto Sans KR 자동 폴백).
|
||||
// 글로우 없음, 대문자 변환 없음. 위계는 크기(variant)와 명도(color 3단)로 만든다.
|
||||
// 기본 색은 모든 변형에서 text.primary — 큰 숫자·제목을 액센트로 칠하지 않는다.
|
||||
|
||||
import { Text, type TextProps, type TextStyle, type StyleProp } from 'react-native'
|
||||
import { d3roNativeFonts, d3roNativeTypo, type D3roNativeTypoKey } from '../theme'
|
||||
import { d3roNativeFonts, d3roNativeTypo, type D3roNativePalette, type D3roNativeTypoKey } from '../theme'
|
||||
import { useNativePalette } from '../theme-context'
|
||||
|
||||
export type PhosphorVariant = D3roNativeTypoKey
|
||||
|
||||
type PhosphorColor =
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'tertiary'
|
||||
| 'accent'
|
||||
/** @deprecated v4: 'accent' 별칭 */
|
||||
| 'amber'
|
||||
/** @deprecated v4: 'tertiary' 별칭 */
|
||||
| 'label'
|
||||
/** @deprecated v4: 'tertiary' 별칭 */
|
||||
| 'muted'
|
||||
|
||||
export interface PhosphorTextProps extends TextProps {
|
||||
variant?: PhosphorVariant
|
||||
color?: 'amber' | 'primary' | 'secondary' | 'label' | 'muted'
|
||||
color?: PhosphorColor
|
||||
style?: StyleProp<TextStyle>
|
||||
}
|
||||
|
||||
function resolveColor(palette: D3roNativePalette, color: PhosphorColor): string {
|
||||
switch (color) {
|
||||
case 'accent':
|
||||
case 'amber':
|
||||
return palette.accent.light
|
||||
case 'secondary':
|
||||
return palette.text.secondary
|
||||
case 'tertiary':
|
||||
case 'label':
|
||||
case 'muted':
|
||||
return palette.text.tertiary
|
||||
case 'primary':
|
||||
return palette.text.primary
|
||||
}
|
||||
}
|
||||
|
||||
export function PhosphorText({
|
||||
children,
|
||||
variant = 'body',
|
||||
color,
|
||||
color = 'primary',
|
||||
style,
|
||||
...rest
|
||||
}: PhosphorTextProps): React.ReactElement {
|
||||
const palette = useNativePalette()
|
||||
const typo = d3roNativeTypo[variant]
|
||||
const isAmberVariant = variant === 'hero' || variant === 'title' || variant === 'value'
|
||||
|
||||
const resolvedColor: string = (() => {
|
||||
if (color === 'amber') return palette.accent.main
|
||||
if (color === 'primary') return palette.text.primary
|
||||
if (color === 'secondary') return palette.text.secondary
|
||||
if (color === 'label') return palette.text.label
|
||||
if (color === 'muted') return palette.text.muted
|
||||
return isAmberVariant ? palette.accent.main : palette.text.primary
|
||||
})()
|
||||
|
||||
const baseStyle: TextStyle = {
|
||||
...typo,
|
||||
color: resolvedColor,
|
||||
...d3roNativeTypo[variant],
|
||||
color: resolveColor(palette, color),
|
||||
fontFamily: d3roNativeFonts.sans
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// packages/ui-native/src/components/PhysicalButton.tsx
|
||||
// RN용 PhysicalButton — Pressable 기반 누르는 느낌
|
||||
// v4 "조용한 계기": 버튼 한 종류, 톤 넷 — 데스크톱 PhysicalButton 과 같은 방식.
|
||||
// accent = 채움(accent.solid + accent.ink) / glass = 1px 선 / danger = 선 + 위험색 글자 / ghost = 글자만.
|
||||
// 채움은 accent 하나뿐. 눌림은 색 변화뿐 — 떠오르거나 가라앉지 않는다. 굵기 500.
|
||||
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Pressable,
|
||||
Text,
|
||||
|
|
@ -10,60 +11,60 @@ import {
|
|||
type TextStyle,
|
||||
type StyleProp
|
||||
} from 'react-native'
|
||||
import { d3roNativeRadius, d3roNativeTypo } from '../theme'
|
||||
import { d3roNativeControl, d3roNativeFonts, d3roNativeRadius, d3roNativeTypo, type D3roNativePalette } from '../theme'
|
||||
import { useNativePalette } from '../theme-context'
|
||||
|
||||
export type PhysicalButtonTone = 'accent' | 'glass' | 'danger' | 'ghost'
|
||||
|
||||
export interface PhysicalButtonProps extends Omit<PressableProps, 'children' | 'style'> {
|
||||
label: string
|
||||
/** 시각 톤. 지정하면 variant 보다 우선한다. */
|
||||
tone?: PhysicalButtonTone
|
||||
/** @deprecated v4: tone 을 쓴다. primary=accent, secondary=glass, danger=danger */
|
||||
variant?: 'primary' | 'secondary' | 'danger'
|
||||
disabled?: boolean
|
||||
style?: StyleProp<ViewStyle>
|
||||
textStyle?: StyleProp<TextStyle>
|
||||
}
|
||||
|
||||
interface ToneStyle {
|
||||
bg: string
|
||||
bgPressed: string
|
||||
color: string
|
||||
border: string
|
||||
}
|
||||
|
||||
export function physicalButtonTone(palette: D3roNativePalette, tone: PhysicalButtonTone): ToneStyle {
|
||||
switch (tone) {
|
||||
case 'accent':
|
||||
return { bg: palette.accent.solid, bgPressed: palette.accent.solidHover, color: palette.accent.ink, border: palette.accent.solid }
|
||||
case 'danger':
|
||||
return { bg: 'transparent', bgPressed: palette.status.dangerBg, color: palette.status.danger, border: palette.border.strong }
|
||||
case 'ghost':
|
||||
return { bg: 'transparent', bgPressed: palette.overlay.soft, color: palette.text.secondary, border: 'transparent' }
|
||||
case 'glass':
|
||||
return { bg: 'transparent', bgPressed: palette.overlay.soft, color: palette.text.primary, border: palette.border.strong }
|
||||
}
|
||||
}
|
||||
|
||||
const VARIANT_TONE: Record<NonNullable<PhysicalButtonProps['variant']>, PhysicalButtonTone> = {
|
||||
primary: 'accent',
|
||||
secondary: 'glass',
|
||||
danger: 'danger'
|
||||
}
|
||||
|
||||
export function PhysicalButton({
|
||||
label,
|
||||
tone,
|
||||
variant = 'primary',
|
||||
disabled = false,
|
||||
style,
|
||||
textStyle,
|
||||
onPress,
|
||||
...rest
|
||||
}: PhysicalButtonProps): React.ReactElement {
|
||||
const palette = useNativePalette()
|
||||
const [pressed, setPressed] = useState(false)
|
||||
|
||||
const bgColor =
|
||||
variant === 'primary'
|
||||
? palette.accent.main
|
||||
: variant === 'danger'
|
||||
? palette.tag.red
|
||||
: 'transparent'
|
||||
|
||||
const borderColor =
|
||||
variant === 'secondary' ? palette.border.strong : 'transparent'
|
||||
|
||||
const textColor =
|
||||
variant === 'primary' || variant === 'danger'
|
||||
? palette.text.onAccent ?? palette.text.primary
|
||||
: palette.text.secondary
|
||||
|
||||
const containerStyle: ViewStyle = {
|
||||
backgroundColor: bgColor,
|
||||
borderColor,
|
||||
borderWidth: variant === 'secondary' ? 1 : 0,
|
||||
borderRadius: d3roNativeRadius.button,
|
||||
minHeight: 48,
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 24,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
opacity: disabled ? 0.4 : pressed ? 0.75 : 1,
|
||||
transform: [{ translateY: pressed && !disabled ? 1 : 0 }]
|
||||
}
|
||||
|
||||
const textStyle: TextStyle = {
|
||||
...d3roNativeTypo.heading,
|
||||
color: textColor
|
||||
}
|
||||
const t = physicalButtonTone(palette, tone ?? VARIANT_TONE[variant])
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
|
|
@ -74,12 +75,37 @@ export function PhysicalButton({
|
|||
disabled,
|
||||
}}
|
||||
onPress={onPress}
|
||||
onPressIn={() => setPressed(true)}
|
||||
onPressOut={() => setPressed(false)}
|
||||
disabled={disabled}
|
||||
style={[containerStyle, style]}
|
||||
style={({ pressed }) => [
|
||||
{
|
||||
backgroundColor: pressed && !disabled ? t.bgPressed : t.bg,
|
||||
borderColor: t.border,
|
||||
borderWidth: 1,
|
||||
borderRadius: d3roNativeRadius.button,
|
||||
minHeight: d3roNativeControl.h,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: d3roNativeControl.padX,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
opacity: disabled ? 0.5 : 1
|
||||
},
|
||||
style
|
||||
]}
|
||||
>
|
||||
<Text style={textStyle}>{label}</Text>
|
||||
<Text
|
||||
style={[
|
||||
{
|
||||
...d3roNativeTypo.body,
|
||||
fontWeight: '500',
|
||||
fontFamily: d3roNativeFonts.sans,
|
||||
color: t.color,
|
||||
textAlign: 'center'
|
||||
},
|
||||
textStyle
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// packages/ui-native/src/components/ScreenPanel.tsx
|
||||
// RN ScreenPanel — inset bg + dot grid background
|
||||
// v4: 우묵한 판 — inset 표면 + 1px 선. (점 격자·CRT 장식 없음)
|
||||
|
||||
import { View, type ViewProps, type ViewStyle, type StyleProp } from 'react-native'
|
||||
import { d3roNativeRadius } from '../theme'
|
||||
|
|
@ -14,7 +14,9 @@ export function ScreenPanel({ children, style, ...rest }: ScreenPanelProps): Rea
|
|||
const baseStyle: ViewStyle = {
|
||||
backgroundColor: palette.bg.inset,
|
||||
borderRadius: d3roNativeRadius.inner,
|
||||
padding: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: palette.border.subtle,
|
||||
padding: 16,
|
||||
overflow: 'hidden'
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
// packages/ui-native/src/components/StatusBar.tsx
|
||||
// RN StatusBar — bottom status strip with LED + model label
|
||||
|
||||
import { View, type ViewStyle, type StyleProp } from 'react-native'
|
||||
import { d3roNativeFonts } from '../theme'
|
||||
import { useNativePalette } from '../theme-context'
|
||||
import { Led } from './Led'
|
||||
import { PhosphorText } from './PhosphorText'
|
||||
|
||||
export interface AppStatusBarProps {
|
||||
model?: string
|
||||
label?: string
|
||||
style?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
export function AppStatusBar({
|
||||
model = 'CLOUD CLAUDE',
|
||||
label = 'PRECISION DATA LINK',
|
||||
style
|
||||
}: AppStatusBarProps): React.ReactElement {
|
||||
const palette = useNativePalette()
|
||||
const containerStyle: ViewStyle = {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 16,
|
||||
gap: 8,
|
||||
opacity: 0.5
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[containerStyle, style]}>
|
||||
<Led color="green" size={5} />
|
||||
<PhosphorText
|
||||
variant="label"
|
||||
color="muted"
|
||||
style={{ fontFamily: d3roNativeFonts.mono, fontSize: 10, letterSpacing: 0.6 }}
|
||||
>
|
||||
{model}
|
||||
</PhosphorText>
|
||||
<View style={{ width: 1, height: 8, backgroundColor: palette.border.default }} />
|
||||
<PhosphorText
|
||||
variant="label"
|
||||
color="muted"
|
||||
style={{ fontFamily: d3roNativeFonts.mono, fontSize: 10, letterSpacing: 0.6 }}
|
||||
>
|
||||
{label}
|
||||
</PhosphorText>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
|
@ -3,11 +3,24 @@
|
|||
|
||||
export {
|
||||
d3roNativePalette,
|
||||
d3roNativeDarkPalette,
|
||||
d3roNativeLightPalette,
|
||||
getD3roNativePalette,
|
||||
D3RO_NATIVE_RAW,
|
||||
D3RO_NATIVE_STATUS,
|
||||
d3roNativeTypo,
|
||||
d3roNativeRadius,
|
||||
d3roNativeFonts,
|
||||
d3roNativeSpace,
|
||||
d3roNativeControl,
|
||||
d3roNativeWeight,
|
||||
d3roNativeMotion,
|
||||
d3roNativeNumerals,
|
||||
d3roNativeOverlayShadow,
|
||||
type D3roNativePalette,
|
||||
type D3roNativePaletteKey,
|
||||
type D3roNativeStatus,
|
||||
type D3roNativeThemeKey,
|
||||
type D3roNativeTypoKey
|
||||
} from './theme'
|
||||
export {
|
||||
|
|
@ -25,6 +38,4 @@ export { Led, type LedProps } from './components/Led'
|
|||
export { PhysicalButton, type PhysicalButtonProps } from './components/PhysicalButton'
|
||||
export { ScreenPanel, type ScreenPanelProps } from './components/ScreenPanel'
|
||||
export { WaveBars, type WaveBarsProps } from './components/WaveBars'
|
||||
export { AppStatusBar, type AppStatusBarProps } from './components/StatusBar'
|
||||
export { Header, type HeaderProps } from './components/Header'
|
||||
export { FilterChip, type FilterChipProps } from './components/FilterChip'
|
||||
|
|
|
|||
|
|
@ -1,8 +1,31 @@
|
|||
// packages/ui-native/src/theme.ts
|
||||
// RN용 테마 토큰 — MUI 없이 순수 색상/타이포/그림자 값
|
||||
// packages/ui의 d3roPalette와 의도적으로 동기화 (RN은 CSS var 불가, 정적 값)
|
||||
// RN 디자인 토큰 v4 "조용한 계기" — 모바일 팔레트의 단일 출처.
|
||||
//
|
||||
// 색 값은 데스크톱 SSOT `packages/ui/src/theme-raw.ts` 의 BASE.dark / BASE.light 와 STATUS 에서 옮겨 왔다.
|
||||
// RN 은 CSS 변수를 못 쓰고, Metro 가 packages/ui 를 watchFolders 에 두지 않아 직접 import 하지 않는다.
|
||||
// ⚠ theme-raw.ts 와 같은 값 — 바뀌면 함께 바꾼다. apps/mobile-rn/__tests__/theme-parity.test.ts 가 값이 같은지 검사한다.
|
||||
//
|
||||
// 원칙 (루트 design.md "2026-09-28 전면 개편"):
|
||||
// - 표면은 평평하다. 위계는 크기·명도(텍스트 3단)·1px 선·여백으로 만든다. 글로우·그라디언트·장식 LED 없음.
|
||||
// - 텍스트 3단(primary/secondary/tertiary)은 app/card/elevated/sidebar 표면에서 4.5:1 이상.
|
||||
// 옛 label/inactive/muted 는 tertiary 의 호환 별칭이다.
|
||||
// - accent.solid 는 채운 버튼 배경, accent.ink 는 그 위 글자 (4.5:1 이상). accent.main 은 채움에 쓰지 않는다.
|
||||
// - 굵기는 400/500 (강조 600). 대문자 변환·자간 없음. 모노 폰트 없음(한글 글리프가 없다).
|
||||
|
||||
import { Platform } from 'react-native'
|
||||
import { Platform, type TextStyle, type ViewStyle } from 'react-native'
|
||||
|
||||
export type D3roNativeThemeKey = 'dark' | 'light'
|
||||
|
||||
export interface D3roNativeStatus {
|
||||
success: string
|
||||
successBg: string
|
||||
warning: string
|
||||
warningBg: string
|
||||
danger: string
|
||||
dangerBg: string
|
||||
info: string
|
||||
infoBg: string
|
||||
}
|
||||
|
||||
export interface D3roNativePalette {
|
||||
bg: {
|
||||
|
|
@ -10,26 +33,50 @@ export interface D3roNativePalette {
|
|||
card: string
|
||||
cardHover: string
|
||||
elevated: string
|
||||
input: string
|
||||
sidebar: string
|
||||
inset: string
|
||||
/** @deprecated v4: bg.card 와 같다 */
|
||||
chassis: string
|
||||
}
|
||||
text: {
|
||||
primary: string
|
||||
white: string
|
||||
onAccent?: string
|
||||
secondary: string
|
||||
label: string
|
||||
/** 가장 옅은 읽기용 글자 — 그래도 4.5:1 이상 */
|
||||
tertiary: string
|
||||
/** 대비 예외: 비활성 표시 전용 */
|
||||
disabled: string
|
||||
/** @deprecated v4: accent.ink 와 같다 (채운 버튼 위 글자) */
|
||||
onAccent: string
|
||||
/** 스위치 손잡이·체크 표시처럼 테마와 무관하게 흰 것 */
|
||||
white: string
|
||||
/** @deprecated v4: text.tertiary 별칭 */
|
||||
label: string
|
||||
/** @deprecated v4: text.tertiary 별칭 */
|
||||
inactive: string
|
||||
/** @deprecated v4: text.tertiary 별칭 */
|
||||
muted: string
|
||||
}
|
||||
accent: {
|
||||
/** 선·아이콘·선택 표시 */
|
||||
main: string
|
||||
pressed?: string
|
||||
/** 표면 위 강조 글자 (라이트 테마에서는 더 진한 값) */
|
||||
light: string
|
||||
dark: string
|
||||
/** 옅은 선택 배경 */
|
||||
dim: string
|
||||
/** 채운 버튼 배경 — ink 와 4.5:1 이상 */
|
||||
solid: string
|
||||
solidHover: string
|
||||
/** solid 위 글자 */
|
||||
ink: string
|
||||
/** @deprecated v4: accent.solidHover 별칭 (눌림) */
|
||||
pressed: string
|
||||
/** @deprecated v4: 글로우 없음 — accent.dim 과 같다 */
|
||||
glow: string
|
||||
/** @deprecated v4: status.success 별칭 */
|
||||
green: string
|
||||
/** @deprecated v4: status.successBg 별칭 */
|
||||
greenGlow: string
|
||||
}
|
||||
border: {
|
||||
|
|
@ -37,6 +84,7 @@ export interface D3roNativePalette {
|
|||
default: string
|
||||
strong: string
|
||||
}
|
||||
/** 카테고리·역할 구분색. 글자로도 쓰이므로 테마마다 대비를 맞춘 값 (red/orange/green/blue = STATUS). */
|
||||
tag: {
|
||||
purple: string
|
||||
orange: string
|
||||
|
|
@ -44,125 +92,187 @@ export interface D3roNativePalette {
|
|||
green: string
|
||||
blue: string
|
||||
}
|
||||
/** v3: 시맨틱 상태 역할 (액센트와 분리) */
|
||||
status: {
|
||||
success: string
|
||||
successBg: string
|
||||
warning: string
|
||||
warningBg: string
|
||||
danger: string
|
||||
dangerBg: string
|
||||
info: string
|
||||
infoBg: string
|
||||
}
|
||||
/** 시맨틱 상태 역할 (액센트와 분리) — theme-raw STATUS */
|
||||
status: D3roNativeStatus
|
||||
/** 모달 뒤 스크림 */
|
||||
scrim: string
|
||||
/** 표면 위 옅은 오버레이 (눌림·선택) */
|
||||
overlay: { soft: string; strong: string }
|
||||
led: { off: string }
|
||||
shadow?: string
|
||||
/** 떠 있는 층(시트·메뉴·플로팅 버튼) 그림자 색 — 표면에는 그림자를 쓰지 않는다 */
|
||||
shadow: string
|
||||
}
|
||||
|
||||
export const d3roNativePalette: D3roNativePalette = {
|
||||
bg: {
|
||||
app: '#19191b',
|
||||
card: '#242427',
|
||||
cardHover: '#2a2a2d',
|
||||
elevated: '#2e2e32',
|
||||
sidebar: '#1e1f21',
|
||||
inset: '#0f0f11',
|
||||
chassis: '#242528'
|
||||
// ── theme-raw.ts 원시값 사본 ─────────────────────────────────────────
|
||||
// theme-raw.ts 와 같은 값 — 바뀌면 함께. (BASE.dark / BASE.light 의 bg·text·border·accent, STATUS.dark / STATUS.light)
|
||||
export const D3RO_NATIVE_RAW = {
|
||||
dark: {
|
||||
bg: { app: '#0a0e1c', card: '#111a30', cardHover: '#152039', elevated: '#1a2540', input: '#0d1526', sidebar: '#080b17', inset: '#0a111f' },
|
||||
text: { primary: '#eef2fb', secondary: '#93a4c8', tertiary: '#7e8eb0', disabled: '#3c4763', hover: '#c9d5f2' },
|
||||
border: { subtle: 'rgba(148,180,255,0.07)', default: 'rgba(148,180,255,0.12)', strong: 'rgba(148,180,255,0.20)' },
|
||||
accent: { main: '#3b82f6', light: '#60a5fa', dark: '#2563eb', dim: 'rgba(59,130,246,0.14)', solid: '#2563eb', solidHover: '#1d4ed8', ink: '#ffffff' },
|
||||
},
|
||||
text: {
|
||||
primary: '#d4d4d8',
|
||||
white: '#ffffff',
|
||||
secondary: '#8e8e93',
|
||||
label: '#7c7c82',
|
||||
disabled: '#4a4a4e',
|
||||
inactive: '#77797c',
|
||||
muted: '#71717a'
|
||||
light: {
|
||||
bg: { app: '#f2f5fb', card: '#ffffff', cardHover: '#f7f9fd', elevated: '#eef2f9', input: '#ffffff', sidebar: '#e9eef7', inset: '#e4e9f2' },
|
||||
text: { primary: '#111827', secondary: '#4b5a75', tertiary: '#56647c', disabled: '#b5bfd1', hover: '#2b3a55' },
|
||||
border: { subtle: 'rgba(30,58,138,0.07)', default: 'rgba(30,58,138,0.12)', strong: 'rgba(30,58,138,0.22)' },
|
||||
accent: { main: '#2563eb', light: '#1d4ed8', dark: '#1e40af', dim: 'rgba(37,99,235,0.10)', solid: '#2563eb', solidHover: '#1d4ed8', ink: '#ffffff' },
|
||||
},
|
||||
accent: {
|
||||
// 브랜드 통일: 앱(Midnight Glass) blue — 레거시 amber 폐기
|
||||
main: '#3b82f6',
|
||||
dim: 'rgba(59, 130, 246, 0.15)',
|
||||
glow: 'rgba(59, 130, 246, 0.5)',
|
||||
green: '#4ade80',
|
||||
greenGlow: 'rgba(74, 222, 128, 0.6)'
|
||||
},
|
||||
border: {
|
||||
subtle: 'rgba(255,255,255,0.04)',
|
||||
default: '#2e2e32',
|
||||
strong: 'rgba(255,255,255,0.12)'
|
||||
},
|
||||
tag: {
|
||||
purple: '#b854f5',
|
||||
orange: '#f59e0b',
|
||||
red: '#ef4444',
|
||||
green: '#22c55e',
|
||||
blue: '#3b82f6'
|
||||
},
|
||||
status: {
|
||||
success: '#4ade80',
|
||||
successBg: 'rgba(74, 222, 128, 0.14)',
|
||||
warning: '#fbbf24',
|
||||
warningBg: 'rgba(251, 191, 36, 0.14)',
|
||||
danger: '#f87171',
|
||||
dangerBg: 'rgba(248, 113, 113, 0.14)',
|
||||
info: '#60a5fa',
|
||||
infoBg: 'rgba(96, 165, 250, 0.14)'
|
||||
},
|
||||
led: {
|
||||
off: '#111111'
|
||||
},
|
||||
shadow: '#000000'
|
||||
} as const
|
||||
|
||||
// ── v3: 간격·컨트롤·웨이트·모션 (RN은 px 숫자) ──
|
||||
export const D3RO_NATIVE_STATUS: Record<D3roNativeThemeKey, D3roNativeStatus> = {
|
||||
dark: {
|
||||
success: '#34d399', successBg: 'rgba(52,211,153,0.12)',
|
||||
warning: '#fbbf24', warningBg: 'rgba(251,191,36,0.12)',
|
||||
danger: '#f87171', dangerBg: 'rgba(248,113,113,0.12)',
|
||||
info: '#60a5fa', infoBg: 'rgba(96,165,250,0.12)',
|
||||
},
|
||||
light: {
|
||||
success: '#047857', successBg: 'rgba(4,120,87,0.10)',
|
||||
warning: '#b45309', warningBg: 'rgba(180,83,9,0.10)',
|
||||
danger: '#b91c1c', dangerBg: 'rgba(185,28,28,0.10)',
|
||||
info: '#1d4ed8', infoBg: 'rgba(29,78,216,0.10)',
|
||||
},
|
||||
}
|
||||
|
||||
/** theme-raw 에 없는 모바일 전용 값. 보라만 글자 대비를 맞춘 역할색(violet 400/700)이다. */
|
||||
const NATIVE_EXTRA: Record<D3roNativeThemeKey, {
|
||||
purple: string
|
||||
scrim: string
|
||||
overlaySoft: string
|
||||
overlayStrong: string
|
||||
shadow: string
|
||||
}> = {
|
||||
dark: {
|
||||
purple: '#a78bfa',
|
||||
scrim: 'rgba(0,0,0,0.6)', // theme-raw --d3-scrim (dark)
|
||||
overlaySoft: 'rgba(238,242,251,0.06)', // text.primary 6%
|
||||
overlayStrong: 'rgba(238,242,251,0.12)',
|
||||
shadow: '#030712',
|
||||
},
|
||||
light: {
|
||||
purple: '#6d28d9',
|
||||
scrim: 'rgba(17,24,39,0.32)', // theme-raw --d3-scrim (light)
|
||||
overlaySoft: 'rgba(17,24,39,0.06)',
|
||||
overlayStrong: 'rgba(17,24,39,0.12)',
|
||||
shadow: '#1e3a8a',
|
||||
},
|
||||
}
|
||||
|
||||
function buildPalette(key: D3roNativeThemeKey): D3roNativePalette {
|
||||
const raw = D3RO_NATIVE_RAW[key]
|
||||
const status = D3RO_NATIVE_STATUS[key]
|
||||
const extra = NATIVE_EXTRA[key]
|
||||
return {
|
||||
bg: { ...raw.bg, chassis: raw.bg.card },
|
||||
text: {
|
||||
primary: raw.text.primary,
|
||||
secondary: raw.text.secondary,
|
||||
tertiary: raw.text.tertiary,
|
||||
disabled: raw.text.disabled,
|
||||
onAccent: raw.accent.ink,
|
||||
white: '#ffffff',
|
||||
label: raw.text.tertiary,
|
||||
inactive: raw.text.tertiary,
|
||||
muted: raw.text.tertiary,
|
||||
},
|
||||
accent: {
|
||||
...raw.accent,
|
||||
pressed: raw.accent.solidHover,
|
||||
glow: raw.accent.dim,
|
||||
green: status.success,
|
||||
greenGlow: status.successBg,
|
||||
},
|
||||
border: { ...raw.border },
|
||||
tag: {
|
||||
purple: extra.purple,
|
||||
orange: status.warning,
|
||||
red: status.danger,
|
||||
green: status.success,
|
||||
blue: status.info,
|
||||
},
|
||||
status: { ...status },
|
||||
scrim: extra.scrim,
|
||||
overlay: { soft: extra.overlaySoft, strong: extra.overlayStrong },
|
||||
led: { off: raw.text.disabled },
|
||||
shadow: extra.shadow,
|
||||
}
|
||||
}
|
||||
|
||||
export const d3roNativeDarkPalette: D3roNativePalette = buildPalette('dark')
|
||||
export const d3roNativeLightPalette: D3roNativePalette = buildPalette('light')
|
||||
|
||||
/** 프로바이더 밖 기본값 (다크) */
|
||||
export const d3roNativePalette: D3roNativePalette = d3roNativeDarkPalette
|
||||
|
||||
export function getD3roNativePalette(key: D3roNativeThemeKey): D3roNativePalette {
|
||||
return key === 'light' ? d3roNativeLightPalette : d3roNativeDarkPalette
|
||||
}
|
||||
|
||||
// ── 간격·컨트롤·웨이트·모션 (RN 은 숫자) ──
|
||||
export const d3roNativeSpace = {
|
||||
s1: 4, s2: 8, s3: 12, s4: 16, s5: 24, s6: 40, s7: 64, s8: 96
|
||||
} as const
|
||||
|
||||
/** 터치 타깃: iOS 44pt / Android 48dp — 두 플랫폼 모두 48 로 맞춘다. 버튼처럼 생긴 것은 이 높이 이상. */
|
||||
export const d3roNativeControl = {
|
||||
h: 44, hSm: 36, padX: 16, padXSm: 10
|
||||
h: 48,
|
||||
padX: 16,
|
||||
padXSm: 12
|
||||
} as const
|
||||
|
||||
export const d3roNativeWeight = {
|
||||
body: '400' as const, medium: '500' as const, strong: '600' as const
|
||||
} as const
|
||||
|
||||
/** theme-raw MOTION 과 같은 길이(ms). 상태 변화 피드백만 — 등장 연출은 쓰지 않는다. */
|
||||
export const d3roNativeMotion = {
|
||||
instant: 100, quick: 200, normal: 350, slow: 600
|
||||
instant: 100, quick: 160, normal: 240, slow: 400
|
||||
} as const
|
||||
|
||||
// v3 "타이포그래피 퍼스트" — packages/ui d3roTypo v3와 동일 철학.
|
||||
// 볼드(700) 폐기, 크기·행간·명도로 위계. 한글 행간 1.5~1.6,
|
||||
// 자간 0 근처 (양수 트래킹은 라틴 대문자 라벨에만 소량).
|
||||
// RN letterSpacing 단위는 px.
|
||||
// v4 타입 — 데스크톱 d3roTypo v4 와 같은 철학: 크기·명도로 위계, 굵기 400/500, 자간 0(큰 제목만 살짝 음수).
|
||||
// RN letterSpacing 단위는 px, lineHeight 는 px. 한글 본문 행간 1.6.
|
||||
export const d3roNativeTypo = {
|
||||
hero: { fontSize: 42, fontWeight: '400' as const, letterSpacing: -0.5, lineHeight: 50 },
|
||||
title: { fontSize: 28, fontWeight: '400' as const, letterSpacing: -0.3, lineHeight: 36 },
|
||||
hero: { fontSize: 28, fontWeight: '500' as const, letterSpacing: -0.4, lineHeight: 36 },
|
||||
title: { fontSize: 22, fontWeight: '500' as const, letterSpacing: -0.2, lineHeight: 30 },
|
||||
value: { fontSize: 20, fontWeight: '500' as const, letterSpacing: 0, lineHeight: 26 },
|
||||
heading: { fontSize: 16, fontWeight: '500' as const, letterSpacing: 0, lineHeight: 24 },
|
||||
body: { fontSize: 14, fontWeight: '400' as const, letterSpacing: 0, lineHeight: 22 },
|
||||
small: { fontSize: 12, fontWeight: '400' as const, letterSpacing: 0, lineHeight: 18 },
|
||||
meta: { fontSize: 11, fontWeight: '500' as const, letterSpacing: 0.3, lineHeight: 16 },
|
||||
label: { fontSize: 11, fontWeight: '500' as const, letterSpacing: 0.4, lineHeight: 15 }
|
||||
meta: { fontSize: 12, fontWeight: '400' as const, letterSpacing: 0, lineHeight: 18 },
|
||||
label: { fontSize: 11, fontWeight: '500' as const, letterSpacing: 0, lineHeight: 16 }
|
||||
} as const
|
||||
|
||||
/** theme-raw RADIUS 와 같은 값 — 정밀 도구: 작고 일정하게. */
|
||||
export const d3roNativeRadius = {
|
||||
card: 22,
|
||||
inner: 12,
|
||||
card: 10,
|
||||
inner: 8,
|
||||
button: 8,
|
||||
small: 6,
|
||||
xs: 4,
|
||||
pill: 999
|
||||
} as const
|
||||
|
||||
// 폰트 전략 (bare RN, 커스텀 폰트 번들링 없음):
|
||||
// - sans: 플랫폼 시스템 산세리프. iOS=SF Pro(한글은 Apple SD Gothic Neo로 자동
|
||||
// 폴백), Android=Roboto(한글은 Noto Sans KR로 자동 폴백). Apple HIG/Material
|
||||
// 권장을 따르는 표준 방식이며 한글 렌더링 품질이 가장 안정적.
|
||||
// - mono: 텔레메트리·코드 등 등폭이 필요한 곳에만 사용 (한글엔 사용 금지).
|
||||
// 폰트: 플랫폼 시스템 산세리프 하나. iOS=SF Pro(한글 Apple SD Gothic Neo 자동 폴백),
|
||||
// Android=Roboto(한글 Noto Sans KR 자동 폴백). 모노 폰트 토큰은 폐기(design.md v4) —
|
||||
// 수치 정렬이 필요하면 fontVariant: ['tabular-nums'] 를 쓴다.
|
||||
export const d3roNativeFonts = {
|
||||
sans: Platform.OS === 'ios' ? 'System' : 'Roboto',
|
||||
mono: Platform.OS === 'ios' ? 'Menlo' : 'monospace'
|
||||
sans: Platform.OS === 'ios' ? 'System' : 'Roboto'
|
||||
} as const
|
||||
|
||||
export type D3roNativePaletteKey = keyof typeof d3roNativePalette
|
||||
/** 수치 정렬 — 흔들리는 숫자 방지 */
|
||||
export const d3roNativeNumerals: TextStyle = { fontVariant: ['tabular-nums'] }
|
||||
|
||||
/** 떠 있는 층(시트·메뉴·플로팅 버튼) 전용 그림자. 카드·버튼 표면에는 쓰지 않는다. */
|
||||
export function d3roNativeOverlayShadow(palette: D3roNativePalette): ViewStyle {
|
||||
return {
|
||||
shadowColor: palette.shadow,
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.28,
|
||||
shadowRadius: 16,
|
||||
elevation: 8
|
||||
}
|
||||
}
|
||||
|
||||
export type D3roNativePaletteKey = keyof D3roNativePalette
|
||||
export type D3roNativeTypoKey = keyof typeof d3roNativeTypo
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue