Compare commits
6 Commits
v0.2026033
...
v0.2026033
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d787d6f84 | ||
|
|
3bc824940a | ||
|
|
cebeca678f | ||
|
|
dd59ee072d | ||
|
|
828d5f84a9 | ||
|
|
459ee8c7e3 |
@@ -1,2 +1,2 @@
|
||||
HOTSPOT_THRESHOLD=9.85
|
||||
AVERAGE_THRESHOLD=9.39
|
||||
HOTSPOT_THRESHOLD=9.84
|
||||
AVERAGE_THRESHOLD=9.38
|
||||
|
||||
@@ -162,9 +162,11 @@ if failed:
|
||||
sys.exit(1)
|
||||
|
||||
# Ratchet: update thresholds file if scores improved
|
||||
# Truncate (floor) to 2 decimals so the threshold never exceeds the actual score
|
||||
import math
|
||||
thresholds_file = '$THRESHOLDS_FILE'
|
||||
new_hotspot = max(hotspot_min, round(hotspot, 2))
|
||||
new_average = max(average_min, round(average, 2))
|
||||
new_hotspot = max(hotspot_min, math.floor(hotspot * 100) / 100)
|
||||
new_average = max(average_min, math.floor(average * 100) / 100)
|
||||
if new_hotspot > hotspot_min or new_average > average_min:
|
||||
with open(thresholds_file, 'w') as f:
|
||||
f.write(f'HOTSPOT_THRESHOLD={new_hotspot}\nAVERAGE_THRESHOLD={new_average}\n')
|
||||
|
||||
@@ -111,14 +111,9 @@ describe('BreadcrumbBar — archive/unarchive', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('BreadcrumbBar — title in breadcrumb on scroll', () => {
|
||||
it('does not show title when titleHidden is false', () => {
|
||||
render(<BreadcrumbBar entry={baseEntry} {...defaultProps} titleHidden={false} />)
|
||||
expect(screen.queryByText('Test Note')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows type and title when titleHidden is true', () => {
|
||||
render(<BreadcrumbBar entry={baseEntry} {...defaultProps} titleHidden={true} />)
|
||||
describe('BreadcrumbBar — title in breadcrumb (always rendered, CSS-toggled)', () => {
|
||||
it('always renders title elements in the DOM', () => {
|
||||
render(<BreadcrumbBar entry={baseEntry} {...defaultProps} />)
|
||||
expect(screen.getByText('Note')).toBeInTheDocument()
|
||||
expect(screen.getByText('›')).toBeInTheDocument()
|
||||
expect(screen.getByText('Test Note')).toBeInTheDocument()
|
||||
@@ -126,21 +121,29 @@ describe('BreadcrumbBar — title in breadcrumb on scroll', () => {
|
||||
|
||||
it('shows emoji icon when entry has an emoji icon', () => {
|
||||
const entryWithEmoji = { ...baseEntry, icon: '🚀' }
|
||||
render(<BreadcrumbBar entry={entryWithEmoji} {...defaultProps} titleHidden={true} />)
|
||||
render(<BreadcrumbBar entry={entryWithEmoji} {...defaultProps} />)
|
||||
expect(screen.getByText('🚀')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not show icon when entry has a non-emoji icon', () => {
|
||||
const entryWithPhosphor = { ...baseEntry, icon: 'cooking-pot' }
|
||||
render(<BreadcrumbBar entry={entryWithPhosphor} {...defaultProps} titleHidden={true} />)
|
||||
render(<BreadcrumbBar entry={entryWithPhosphor} {...defaultProps} />)
|
||||
expect(screen.queryByText('cooking-pot')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('falls back to "Note" when isA is null', () => {
|
||||
const entryNoType = { ...baseEntry, isA: null }
|
||||
render(<BreadcrumbBar entry={entryNoType} {...defaultProps} titleHidden={true} />)
|
||||
render(<BreadcrumbBar entry={entryNoType} {...defaultProps} />)
|
||||
expect(screen.getByText('Note')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shadow is controlled by data-title-hidden attribute via CSS', () => {
|
||||
const { container } = render(<BreadcrumbBar entry={baseEntry} {...defaultProps} />)
|
||||
const bar = container.querySelector('.breadcrumb-bar')!
|
||||
expect(bar).not.toHaveAttribute('data-title-hidden')
|
||||
bar.setAttribute('data-title-hidden', '')
|
||||
expect(bar).toHaveAttribute('data-title-hidden')
|
||||
})
|
||||
})
|
||||
|
||||
describe('BreadcrumbBar — raw editor toggle', () => {
|
||||
|
||||
@@ -32,8 +32,8 @@ interface BreadcrumbBarProps {
|
||||
onRestore?: () => void
|
||||
onArchive?: () => void
|
||||
onUnarchive?: () => void
|
||||
/** When true, the note title is scrolled out of view — show it inline. */
|
||||
titleHidden?: boolean
|
||||
/** Ref for direct DOM manipulation — avoids re-render on scroll. */
|
||||
barRef?: React.Ref<HTMLDivElement>
|
||||
}
|
||||
|
||||
const DISABLED_ICON_STYLE = { opacity: 0.4, cursor: 'not-allowed' } as const
|
||||
@@ -178,22 +178,21 @@ function BreadcrumbTitle({ entry }: { entry: VaultEntry }) {
|
||||
}
|
||||
|
||||
export const BreadcrumbBar = memo(function BreadcrumbBar({
|
||||
entry, titleHidden, ...actionProps
|
||||
entry, barRef, ...actionProps
|
||||
}: BreadcrumbBarProps) {
|
||||
return (
|
||||
<div
|
||||
ref={barRef}
|
||||
data-tauri-drag-region
|
||||
className="flex shrink-0 items-center"
|
||||
className="breadcrumb-bar flex shrink-0 items-center"
|
||||
style={{
|
||||
height: 52,
|
||||
background: 'var(--background)',
|
||||
padding: '6px 16px',
|
||||
boxShadow: titleHidden ? '0 1px 3px rgba(0,0,0,0.08)' : 'none',
|
||||
transition: 'box-shadow 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
{titleHidden && <BreadcrumbTitle entry={entry} />}
|
||||
<div className="breadcrumb-bar__title flex-1 min-w-0">
|
||||
<BreadcrumbTitle entry={entry} />
|
||||
</div>
|
||||
<BreadcrumbActions entry={entry} {...actionProps} />
|
||||
</div>
|
||||
|
||||
@@ -99,10 +99,10 @@ export function ColorEditableValue({ value, isEditing, onStartEdit, onSave, onCa
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="inline-flex min-w-0 items-center gap-1.5">
|
||||
<span className="inline-flex h-6 min-w-0 items-center gap-1.5">
|
||||
{showSwatch && <ColorSwatch color={value} onChange={handlePickerChange} />}
|
||||
<span
|
||||
className="min-w-0 cursor-pointer truncate rounded px-1 py-0.5 text-right text-[12px] text-secondary-foreground transition-colors hover:bg-muted"
|
||||
className="min-w-0 cursor-pointer truncate rounded px-1 text-right text-[12px] text-secondary-foreground transition-colors hover:bg-muted"
|
||||
onClick={onStartEdit}
|
||||
title={value || 'Click to edit'}
|
||||
>
|
||||
|
||||
@@ -53,7 +53,7 @@ function PropertyRow({ propKey, value, editingKey, displayMode, autoMode, vaultS
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group/prop grid min-w-0 grid-cols-2 items-center gap-2 rounded px-1.5 outline-none transition-colors hover:bg-muted focus:bg-muted focus:ring-1 focus:ring-primary" tabIndex={0} onKeyDown={handleKeyDown} data-testid="editable-property">
|
||||
<div className="group/prop grid min-h-7 min-w-0 grid-cols-2 items-center gap-2 rounded px-1.5 outline-none transition-colors hover:bg-muted focus:bg-muted focus:ring-1 focus:ring-primary" tabIndex={0} onKeyDown={handleKeyDown} data-testid="editable-property">
|
||||
<span className="flex min-w-0 items-center gap-1 text-[12px] text-muted-foreground">
|
||||
<span className="truncate">{toSentenceCase(propKey)}</span>
|
||||
{onDelete && (
|
||||
|
||||
@@ -66,7 +66,7 @@ export function UrlValue({
|
||||
return (
|
||||
<span className="group/url flex min-w-0 max-w-full items-center gap-1">
|
||||
<span
|
||||
className="min-w-0 cursor-pointer truncate rounded-md px-2 py-1 text-right text-[12px] text-[var(--accent-blue)] underline decoration-[var(--accent-blue)]/40 transition-colors hover:decoration-[var(--accent-blue)]"
|
||||
className="inline-flex h-6 min-w-0 cursor-pointer items-center truncate rounded-md px-2 text-right text-[12px] text-[var(--accent-blue)] underline decoration-[var(--accent-blue)]/40 transition-colors hover:decoration-[var(--accent-blue)]"
|
||||
onClick={handleOpen}
|
||||
title={value}
|
||||
data-testid="url-link"
|
||||
@@ -125,7 +125,7 @@ export function EditableValue({
|
||||
|
||||
return (
|
||||
<span
|
||||
className="min-w-0 max-w-full cursor-pointer truncate rounded-md px-2 py-1 text-right text-[12px] text-secondary-foreground transition-colors hover:bg-muted"
|
||||
className="inline-flex h-6 min-w-0 max-w-full cursor-pointer items-center truncate rounded-md px-2 text-right text-[12px] text-secondary-foreground transition-colors hover:bg-muted"
|
||||
onClick={onStartEdit}
|
||||
title={value || 'Click to edit'}
|
||||
>
|
||||
@@ -212,11 +212,11 @@ export function TagPillList({
|
||||
) : (
|
||||
<span
|
||||
key={idx}
|
||||
className="group/pill relative inline-flex cursor-pointer items-center rounded-md transition-colors"
|
||||
className="group/pill relative inline-flex h-6 cursor-pointer items-center rounded-md transition-colors"
|
||||
style={{
|
||||
...getTagStyle(item),
|
||||
backgroundColor: getTagStyle(item).bg,
|
||||
padding: '4px 8px',
|
||||
padding: '0 8px',
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
@@ -255,8 +255,8 @@ export function TagPillList({
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
className="inline-flex items-center justify-center rounded-md border-none bg-muted px-2 text-[12px] font-medium leading-none text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
style={{ padding: '4px 8px' }}
|
||||
className="inline-flex h-6 items-center justify-center rounded-md border-none bg-muted px-2 text-[12px] font-medium leading-none text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
style={{ padding: '0 8px' }}
|
||||
onClick={() => setIsAddingNew(true)}
|
||||
title={`Add ${label.toLowerCase()}`}
|
||||
>
|
||||
|
||||
@@ -23,6 +23,24 @@
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
/* Breadcrumb bar: title + shadow toggled via data attribute (no React re-render) */
|
||||
.breadcrumb-bar {
|
||||
transition: box-shadow 0.2s ease;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.breadcrumb-bar[data-title-hidden] {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.breadcrumb-bar__title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.breadcrumb-bar[data-title-hidden] .breadcrumb-bar__title {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Scroll area wrapping title + editor — single scroll context for alignment */
|
||||
.editor-scroll-area {
|
||||
flex: 1;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type React from 'react'
|
||||
import { useCallback, useRef, useState, useEffect } from 'react'
|
||||
import { useCallback, useRef, useEffect } from 'react'
|
||||
import type { VaultEntry, NoteStatus } from '../types'
|
||||
import type { useCreateBlockNote } from '@blocknote/react'
|
||||
import { DiffView } from './DiffView'
|
||||
@@ -120,9 +120,9 @@ function bindPath(cb: ((path: string) => void) | undefined, path: string) {
|
||||
return cb ? () => cb(path) : undefined
|
||||
}
|
||||
|
||||
function ActiveTabBreadcrumb({ activeTab, titleHidden, props }: {
|
||||
function ActiveTabBreadcrumb({ activeTab, barRef, props }: {
|
||||
activeTab: Tab
|
||||
titleHidden: boolean
|
||||
barRef: React.RefObject<HTMLDivElement | null>
|
||||
props: Omit<EditorContentProps, 'activeTab' | 'isLoadingNewTab' | 'entries' | 'editor' | 'onNavigateWikilink' | 'onEditorChange' | 'onRawContentChange' | 'onSave' | 'onDeleteNote'>
|
||||
}) {
|
||||
const wordCount = countWords(activeTab.content)
|
||||
@@ -131,7 +131,7 @@ function ActiveTabBreadcrumb({ activeTab, titleHidden, props }: {
|
||||
<BreadcrumbBar
|
||||
entry={activeTab.entry}
|
||||
wordCount={wordCount}
|
||||
titleHidden={titleHidden}
|
||||
barRef={barRef}
|
||||
showDiffToggle={props.showDiffToggle}
|
||||
diffMode={props.diffMode}
|
||||
diffLoading={props.diffLoading}
|
||||
@@ -171,18 +171,21 @@ export function EditorContent({
|
||||
const emojiIcon = entryIcon && isEmoji(entryIcon) ? entryIcon : null
|
||||
|
||||
const titleSectionRef = useRef<HTMLDivElement | null>(null)
|
||||
const [titleScrolledAway, setTitleScrolledAway] = useState(false)
|
||||
const titleHidden = showEditor && titleScrolledAway
|
||||
const breadcrumbBarRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const el = titleSectionRef.current
|
||||
if (!el) return
|
||||
const bar = breadcrumbBarRef.current
|
||||
if (!el || !bar) return
|
||||
const observer = new IntersectionObserver(
|
||||
([e]) => setTitleScrolledAway(!e.isIntersecting),
|
||||
([e]) => {
|
||||
if (e.isIntersecting) bar.removeAttribute('data-title-hidden')
|
||||
else bar.setAttribute('data-title-hidden', '')
|
||||
},
|
||||
{ threshold: 0 },
|
||||
)
|
||||
observer.observe(el)
|
||||
return () => observer.disconnect()
|
||||
return () => { observer.disconnect(); bar.removeAttribute('data-title-hidden') }
|
||||
}, [activeTab?.entry.path, showEditor])
|
||||
|
||||
const handleSetIcon = useCallback((emoji: string) => {
|
||||
@@ -198,7 +201,7 @@ export function EditorContent({
|
||||
{activeTab && (
|
||||
<ActiveTabBreadcrumb
|
||||
activeTab={activeTab}
|
||||
titleHidden={titleHidden}
|
||||
barRef={breadcrumbBarRef}
|
||||
props={{ diffMode, diffContent, onToggleDiff, rawMode, onToggleRaw, ...breadcrumbProps }}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -42,7 +42,7 @@ function StatusValue({ propKey, value, isEditing, vaultStatuses, onSave, onStart
|
||||
return (
|
||||
<span className="relative inline-flex min-w-0 items-center">
|
||||
<span
|
||||
className="inline-flex cursor-pointer items-center gap-1.5 rounded-md px-2 py-1 text-[12px] font-medium transition-opacity hover:opacity-80"
|
||||
className="inline-flex h-6 cursor-pointer items-center gap-1.5 rounded-md px-2 text-[12px] font-medium transition-opacity hover:opacity-80"
|
||||
style={{ backgroundColor: style.bg, color: style.color }}
|
||||
onClick={() => onStartEdit(propKey)}
|
||||
data-testid="status-badge"
|
||||
@@ -83,8 +83,8 @@ function TagsValue({ propKey, value, isEditing, vaultTags, onSave, onStartEdit }
|
||||
return (
|
||||
<span
|
||||
key={tag}
|
||||
className="group/tag relative inline-flex items-center overflow-hidden rounded-md"
|
||||
style={{ backgroundColor: style.bg, padding: '4px 8px', maxWidth: 120 }}
|
||||
className="group/tag relative inline-flex h-6 items-center overflow-hidden rounded-md"
|
||||
style={{ backgroundColor: style.bg, padding: '0 8px', maxWidth: 120 }}
|
||||
>
|
||||
<span
|
||||
className="transition-[max-width] duration-150 group-hover/tag:[mask-image:linear-gradient(to_right,black_60%,transparent_100%)]"
|
||||
@@ -110,8 +110,8 @@ function TagsValue({ propKey, value, isEditing, vaultTags, onSave, onStartEdit }
|
||||
)
|
||||
})}
|
||||
<button
|
||||
className="inline-flex shrink-0 items-center justify-center rounded-md border-none bg-muted text-[12px] font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
style={{ padding: '4px 8px' }}
|
||||
className="inline-flex h-6 shrink-0 items-center justify-center rounded-md border-none bg-muted text-[12px] font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
style={{ padding: '0 8px' }}
|
||||
onClick={() => onStartEdit(propKey)}
|
||||
title="Add tag"
|
||||
data-testid="tags-add-button"
|
||||
@@ -130,7 +130,7 @@ function TagsValue({ propKey, value, isEditing, vaultTags, onSave, onStartEdit }
|
||||
|
||||
function BooleanToggle({ value, onToggle }: { value: boolean; onToggle: () => void }) {
|
||||
return (
|
||||
<label className="inline-flex cursor-pointer items-center gap-1.5" data-testid="boolean-toggle">
|
||||
<label className="inline-flex h-6 cursor-pointer items-center gap-1.5" data-testid="boolean-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={value}
|
||||
@@ -164,7 +164,7 @@ function DateValue({ value, onSave }: {
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className={`inline-flex min-w-0 cursor-pointer items-center gap-1 border-none px-2 py-1 text-right text-[12px] font-medium transition-colors hover:opacity-80${formatted ? ' rounded-md bg-muted text-accent-foreground' : ' bg-transparent text-muted-foreground'}`}
|
||||
className={`inline-flex h-6 min-w-0 cursor-pointer items-center gap-1 border-none px-2 text-right text-[12px] font-medium transition-colors hover:opacity-80${formatted ? ' rounded-md bg-muted text-accent-foreground' : ' bg-transparent text-muted-foreground'}`}
|
||||
title={value}
|
||||
data-testid="date-display"
|
||||
>
|
||||
|
||||
@@ -437,7 +437,7 @@ export function StatusBar({ noteCount, modifiedCount = 0, vaultPath, vaults, onS
|
||||
|
||||
return (
|
||||
<footer style={{ height: 30, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'space-between', background: 'var(--sidebar)', borderTop: '1px solid var(--border)', padding: '0 8px', fontSize: 11, color: 'var(--muted-foreground)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flex: 1, minWidth: 0, overflow: 'hidden' }}>
|
||||
<VaultMenu vaults={vaults} vaultPath={vaultPath} onSwitchVault={onSwitchVault} onOpenLocalFolder={onOpenLocalFolder} onConnectGitHub={onConnectGitHub} hasGitHub={hasGitHub} onRemoveVault={onRemoveVault} />
|
||||
<span style={SEP_STYLE}>|</span>
|
||||
<span
|
||||
@@ -449,15 +449,15 @@ export function StatusBar({ noteCount, modifiedCount = 0, vaultPath, vaults, onS
|
||||
onMouseEnter={onCheckForUpdates ? (e) => { e.currentTarget.style.background = 'var(--hover)' } : undefined}
|
||||
onMouseLeave={onCheckForUpdates ? (e) => { e.currentTarget.style.background = 'transparent' } : undefined}
|
||||
><Package size={13} />{buildNumber ?? 'b?'}</span>
|
||||
<ChangesBadge count={modifiedCount} onClick={onClickPending} onCommitPush={onCommitPush} />
|
||||
<span style={SEP_STYLE}>|</span>
|
||||
<SyncBadge status={syncStatus} lastSyncTime={lastSyncTime} remoteStatus={remoteStatus} onTriggerSync={onTriggerSync} onPullAndPush={onPullAndPush} onOpenConflictResolver={onOpenConflictResolver} />
|
||||
{lastCommitInfo && <CommitBadge info={lastCommitInfo} />}
|
||||
<ConflictBadge count={conflictCount} onClick={onOpenConflictResolver} />
|
||||
<ChangesBadge count={modifiedCount} onClick={onClickPending} onCommitPush={onCommitPush} />
|
||||
<PulseBadge onClick={onClickPulse} disabled={!isGitVault} />
|
||||
{mcpStatus && <McpBadge status={mcpStatus} onInstall={onInstallMcp} />}
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexShrink: 0 }}>
|
||||
<span style={ICON_STYLE}><FileText size={13} />{noteCount.toLocaleString()} notes</span>
|
||||
{zoomLevel !== 100 && (
|
||||
<span
|
||||
|
||||
@@ -28,7 +28,7 @@ function ReadOnlyType({ isA, customColorKey, onNavigate }: { isA?: string | null
|
||||
{onNavigate ? (
|
||||
<button
|
||||
className="min-w-0 max-w-full truncate border-none cursor-pointer ring-inset hover:ring-1 hover:ring-current"
|
||||
style={{ background: getTypeLightColor(isA, customColorKey), color: getTypeColor(isA, customColorKey), borderRadius: 6, padding: '2px 8px', fontSize: 12, fontWeight: 500 }}
|
||||
style={{ background: getTypeLightColor(isA, customColorKey), color: getTypeColor(isA, customColorKey), borderRadius: 6, padding: '0 8px', fontSize: 12, fontWeight: 500, height: 24, display: 'inline-flex', alignItems: 'center' }}
|
||||
onClick={() => onNavigate(isA.toLowerCase())} title={isA}
|
||||
>{isA}</button>
|
||||
) : (
|
||||
@@ -68,7 +68,8 @@ export function TypeSelector({ isA, customColorKey, availableTypes, typeColorKey
|
||||
background: typeLightColor ?? undefined,
|
||||
color: typeColor ?? undefined,
|
||||
borderRadius: 6,
|
||||
padding: '4px 8px',
|
||||
padding: '0 8px',
|
||||
height: 24,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user