feat(desktop): information architecture overhaul — home, records, meetings first
Wave 2 of the 2026-09-28 design overhaul ("records are the main
character, a quiet instrument"). Decisions: design.md.
- Shell: sidebar with primary Home / Records / Meetings (Ctrl+1..3) and a
secondary "Tools" group; every item is a button with aria-current.
The status bar is removed — its "100% LOCAL PRIVACY · ZERO CLOUD SYNC"
claim contradicted device sync; the sidebar shows only queried state.
- Navigation context (routes, params, back, useRouteRequest) so Home can
open a specific meeting or record.
- Home replaces the dashboard: dictation key, start actions, only real
problems, one-line summary, recent records and meetings, file
transcription, free-tier limits.
- Settings become a full-screen route with vertical tabs (tabs no longer
clip) and a shared row grammar; long-standing bugs fixed: team and
enterprise tiers crashed the license tab (currentTier undefined),
first-run onboarding could not be left, Pro+ saw "upgrade to Pro+".
- Records: date-grouped single list with in-place expansion, search and
tags together, mode/favorite filters, load more past 50, undoable
delete. Meetings: list with search/rename/delete, summary-first detail
(summary | transcript | documents | my notes), notes persisted locally
instead of being lost. Tools pages cleaned up; stale-closure example
chips and a 1550x mouse-distance bug fixed.
- Shared pieces: PageHeader, EmptyStateCard, SearchInput ("/" works),
ConfirmDialog, notify with undo; display-labels maps domain values to
typed i18n keys so missing keys fail to compile.
- Mono font stack falls back to Pretendard for Hangul (Korean labels no
longer render spaced out).
- i18n: new ko/en keys via scripts/i18n-add-keys.mjs (one read-merge-
write so parallel editors cannot clobber locale files).
Some renderer files also carried pre-existing in-flight changes (error
recovery deep links, meeting document hooks); they are included as-is.
This commit is contained in:
parent
ba9ef9741e
commit
f064c93197
96 changed files with 12886 additions and 8924 deletions
|
|
@ -1,93 +1,95 @@
|
|||
// src/renderer/components/shared/SearchInput.tsx
|
||||
// Precision Glass Search Bar with Tactile Clear & Keycap Indicator
|
||||
// 공유: 화면 안 검색창. "/" 를 누르면 여기로 포커스가 온다(입력 중이 아닐 때) — 표시만 하고 동작하지 않던
|
||||
// 키캡을 실제 단축키로 만들었다(2026-09-28). Esc 는 검색어를 지우고, 비어 있으면 포커스를 놓는다.
|
||||
|
||||
import React from 'react'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { TextField, InputAdornment, IconButton, Box } from '@mui/material'
|
||||
import { Search, X } from 'lucide-react'
|
||||
import { d3roPalette, d3roFontSans, d3roFontMono, d3roTypo, d3roRadius, d3roShadow } from '@d3ro/ui/theme'
|
||||
import { d3roPalette, d3roFontMono, d3roTypo, d3roRadius } from '@d3ro/ui/theme'
|
||||
import { useI18n } from '@d3ro/i18n'
|
||||
|
||||
export interface SearchInputProps {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
placeholder: string
|
||||
/** 포커스 단축키. '/' 만 지원. 빈 문자열이면 끈다 */
|
||||
shortcut?: string
|
||||
}
|
||||
|
||||
export function SearchInput({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
shortcut = '/',
|
||||
}: SearchInputProps): React.ReactElement {
|
||||
function isTypingTarget(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof HTMLElement)) return false
|
||||
return target.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName)
|
||||
}
|
||||
|
||||
export function SearchInput({ value, onChange, placeholder, shortcut = '/' }: SearchInputProps): React.ReactElement {
|
||||
const { t } = useI18n()
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (shortcut !== '/') return undefined
|
||||
const onKeyDown = (e: KeyboardEvent): void => {
|
||||
if (e.key !== '/' || e.ctrlKey || e.metaKey || e.altKey || isTypingTarget(e.target)) return
|
||||
e.preventDefault()
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
return () => window.removeEventListener('keydown', onKeyDown)
|
||||
}, [shortcut])
|
||||
|
||||
return (
|
||||
<TextField
|
||||
inputRef={inputRef}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key !== 'Escape') return
|
||||
if (value) onChange('')
|
||||
else inputRef.current?.blur()
|
||||
}}
|
||||
fullWidth
|
||||
sx={{
|
||||
mb: 2.5,
|
||||
mb: 2,
|
||||
'& .MuiOutlinedInput-root': {
|
||||
bgcolor: d3roPalette.glass.raised,
|
||||
backdropFilter: `blur(${d3roPalette.glass.blur})`,
|
||||
borderRadius: d3roRadius.inner,
|
||||
border: `1px solid ${d3roPalette.glass.hairline}`,
|
||||
boxShadow: d3roShadow.inset,
|
||||
transition: 'all 0.2s ease',
|
||||
'& fieldset': { border: 'none' },
|
||||
'&:hover': {
|
||||
borderColor: d3roPalette.glass.hairlineStrong,
|
||||
bgcolor: d3roPalette.bg.cardHover,
|
||||
},
|
||||
'&.Mui-focused': {
|
||||
border: `1px solid ${d3roPalette.accent.main}`,
|
||||
boxShadow: `${d3roShadow.inset}, 0 0 0 3px ${d3roPalette.accent.dim}`,
|
||||
},
|
||||
bgcolor: d3roPalette.bg.input,
|
||||
borderRadius: d3roRadius.button,
|
||||
'& fieldset': { borderColor: d3roPalette.border.default },
|
||||
'&:hover fieldset': { borderColor: d3roPalette.border.strong },
|
||||
'&.Mui-focused fieldset': { borderColor: d3roPalette.accent.main, borderWidth: 1 },
|
||||
},
|
||||
'& .MuiInputBase-input': {
|
||||
fontFamily: d3roFontSans,
|
||||
fontSize: d3roTypo.compact.size,
|
||||
color: d3roPalette.text.primary,
|
||||
py: 1.25,
|
||||
'&::placeholder': {
|
||||
color: d3roPalette.text.disabled,
|
||||
opacity: 1,
|
||||
},
|
||||
py: 1,
|
||||
'&::placeholder': { color: d3roPalette.text.tertiary, opacity: 1 },
|
||||
},
|
||||
}}
|
||||
slotProps={{
|
||||
htmlInput: { 'aria-label': placeholder, type: 'search' },
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start" sx={{ pl: 0.5 }}>
|
||||
<Search size={17} style={{ color: d3roPalette.text.secondary }} />
|
||||
<InputAdornment position="start">
|
||||
<Search size={16} aria-hidden style={{ color: d3roPalette.text.tertiary }} />
|
||||
</InputAdornment>
|
||||
),
|
||||
endAdornment: value ? (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => onChange('')}
|
||||
sx={{
|
||||
color: d3roPalette.text.inactive,
|
||||
p: 0.5,
|
||||
'&:hover': { color: d3roPalette.text.primary },
|
||||
}}
|
||||
>
|
||||
<IconButton size="small" aria-label={t('common.clear')} onClick={() => onChange('')} sx={{ p: 0.5 }}>
|
||||
<X size={15} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
) : shortcut ? (
|
||||
<InputAdornment position="end" sx={{ pr: 0.5 }}>
|
||||
<InputAdornment position="end" aria-hidden>
|
||||
<Box
|
||||
component="kbd"
|
||||
sx={{
|
||||
px: 0.85,
|
||||
py: 0.2,
|
||||
px: 0.75,
|
||||
py: 0.25,
|
||||
borderRadius: d3roRadius.xs,
|
||||
bgcolor: d3roPalette.bg.inset,
|
||||
border: `1px solid ${d3roPalette.border.subtle}`,
|
||||
border: `1px solid ${d3roPalette.border.default}`,
|
||||
fontFamily: d3roFontMono,
|
||||
fontSize: '11px',
|
||||
color: d3roPalette.text.dimLabel,
|
||||
color: d3roPalette.text.tertiary,
|
||||
lineHeight: 1,
|
||||
userSelect: 'none',
|
||||
}}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue