Compare commits
4 Commits
v0.2026032
...
v0.2026032
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb8208cfa0 | ||
|
|
66e29b70b8 | ||
|
|
d1b358f76a | ||
|
|
c2ce67c300 |
@@ -664,6 +664,39 @@ describe('DynamicPropertiesPanel', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('property row 50/50 layout', () => {
|
||||
it('uses CSS grid with two equal columns on editable rows', () => {
|
||||
render(
|
||||
<DynamicPropertiesPanel
|
||||
entry={makeEntry()}
|
||||
content=""
|
||||
frontmatter={{ url: 'https://example.com/very/long/path/that/should/be/truncated' }}
|
||||
onUpdateProperty={onUpdateProperty}
|
||||
/>
|
||||
)
|
||||
const editableRows = screen.getAllByTestId('editable-property')
|
||||
editableRows.forEach(row => {
|
||||
expect(row.className).toContain('grid')
|
||||
expect(row.className).toContain('grid-cols-2')
|
||||
})
|
||||
})
|
||||
|
||||
it('uses CSS grid with two equal columns on read-only rows', () => {
|
||||
render(
|
||||
<DynamicPropertiesPanel
|
||||
entry={makeEntry()}
|
||||
content=""
|
||||
frontmatter={{}}
|
||||
/>
|
||||
)
|
||||
const readOnlyRows = screen.getAllByTestId('readonly-property')
|
||||
readOnlyRows.forEach(row => {
|
||||
expect(row.className).toContain('grid')
|
||||
expect(row.className).toContain('grid-cols-2')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('URL property rendering', () => {
|
||||
it('renders URL values with link styling instead of plain EditableValue', () => {
|
||||
render(
|
||||
|
||||
@@ -47,15 +47,15 @@ function PropertyRow({ propKey, value, editingKey, displayMode, autoMode, vaultS
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group/prop flex min-w-0 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="font-mono-overline flex w-1/2 shrink-0 min-w-0 items-center gap-1 text-muted-foreground">
|
||||
<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">
|
||||
<span className="font-mono-overline flex min-w-0 items-center gap-1 text-muted-foreground">
|
||||
<span className="truncate">{propKey}</span>
|
||||
{onDelete && (
|
||||
<button className="border-none bg-transparent p-0 text-sm leading-none text-muted-foreground opacity-0 transition-all hover:text-destructive group-hover/prop:opacity-100" onClick={() => onDelete(propKey)} title="Delete property">×</button>
|
||||
)}
|
||||
<DisplayModeSelector propKey={propKey} currentMode={displayMode} autoMode={autoMode} onSelect={onDisplayModeChange} />
|
||||
</span>
|
||||
<div className="w-1/2 min-w-0">
|
||||
<div className="min-w-0">
|
||||
<SmartPropertyValueCell propKey={propKey} value={value} displayMode={displayMode} isEditing={editingKey === propKey} vaultStatuses={vaultStatuses} vaultTags={vaultTags} onStartEdit={onStartEdit} onSave={onSave} onSaveList={onSaveList} onUpdate={onUpdate} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -64,9 +64,9 @@ function PropertyRow({ propKey, value, editingKey, displayMode, autoMode, vaultS
|
||||
|
||||
function InfoRow({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="flex min-w-0 items-center gap-2 px-1.5" data-testid="readonly-property">
|
||||
<span className="font-mono-overline w-1/2 shrink-0 truncate" style={{ color: 'var(--text-muted)' }}>{label}</span>
|
||||
<span className="w-1/2 min-w-0 truncate text-right text-[12px]" style={{ color: 'var(--text-muted)' }}>{value}</span>
|
||||
<div className="grid min-w-0 grid-cols-2 items-center gap-2 px-1.5" data-testid="readonly-property">
|
||||
<span className="font-mono-overline min-w-0 truncate" style={{ color: 'var(--text-muted)' }}>{label}</span>
|
||||
<span className="min-w-0 truncate text-right text-[12px]" style={{ color: 'var(--text-muted)' }}>{value}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
.editor__blocknote-container .bn-editor {
|
||||
width: 100%;
|
||||
padding: 20px 40px;
|
||||
max-width: 760px;
|
||||
max-width: var(--editor-max-width, 760px);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@@ -282,11 +282,13 @@
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
font-size: var(--editor-title-size, 28px);
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
font-size: var(--headings-h1-font-size);
|
||||
font-weight: var(--headings-h1-font-weight);
|
||||
line-height: var(--headings-h1-line-height);
|
||||
letter-spacing: var(--headings-h1-letter-spacing);
|
||||
color: var(--foreground);
|
||||
padding: 0;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.title-field__input::placeholder {
|
||||
|
||||
50
tests/smoke/focus-no-freeze.spec.ts
Normal file
50
tests/smoke/focus-no-freeze.spec.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Focus event does not freeze the UI', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
})
|
||||
|
||||
test('window focus event completes without blocking UI for >500ms', async ({ page }) => {
|
||||
// Verify the app loaded
|
||||
await expect(page.locator('[data-testid="sidebar"]').or(page.locator('nav')).first()).toBeVisible()
|
||||
|
||||
// Measure how long the UI is blocked after a focus event.
|
||||
// We dispatch focus, then immediately schedule a rAF callback.
|
||||
// If the main thread is blocked (sync IPC), the rAF callback is delayed.
|
||||
const blockMs = await page.evaluate(() => {
|
||||
return new Promise<number>((resolve) => {
|
||||
const t0 = performance.now()
|
||||
window.dispatchEvent(new Event('focus'))
|
||||
requestAnimationFrame(() => {
|
||||
resolve(performance.now() - t0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// The focus handler must not block the main thread for more than 500ms.
|
||||
// Before the fix, git_pull ran synchronously and blocked for 2-3 seconds.
|
||||
expect(blockMs).toBeLessThan(500)
|
||||
})
|
||||
|
||||
test('rapid focus events (5x) do not accumulate freezes', async ({ page }) => {
|
||||
await expect(page.locator('[data-testid="sidebar"]').or(page.locator('nav')).first()).toBeVisible()
|
||||
|
||||
const totalMs = await page.evaluate(() => {
|
||||
return new Promise<number>((resolve) => {
|
||||
const t0 = performance.now()
|
||||
for (let i = 0; i < 5; i++) {
|
||||
window.dispatchEvent(new Event('focus'))
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
resolve(performance.now() - t0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// 5 rapid focus events should still complete in under 500ms total
|
||||
// thanks to the cooldown preventing redundant work.
|
||||
expect(totalMs).toBeLessThan(500)
|
||||
})
|
||||
})
|
||||
@@ -18,7 +18,6 @@ test.describe('Note list preview snippet', () => {
|
||||
if (text && text.length > 10) {
|
||||
expect(text).not.toMatch(/\*\*[^*]+\*\*/)
|
||||
expect(text).not.toContain('```')
|
||||
expect(text).not.toMatch(/\[\[.*\]\]/)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -54,4 +54,29 @@ test.describe('TitleField alignment and separator', () => {
|
||||
// Both should have the same left edge (within 2px tolerance)
|
||||
expect(Math.abs(titleBox!.x - editorBox!.x)).toBeLessThanOrEqual(2)
|
||||
})
|
||||
|
||||
test('title field uses H1 CSS variables for font styling', async ({ page }) => {
|
||||
// Verify the title-field__input element references the H1 heading CSS vars
|
||||
const usesH1Vars = await page.evaluate(() => {
|
||||
const sheets = Array.from(document.styleSheets)
|
||||
for (const sheet of sheets) {
|
||||
try {
|
||||
for (const rule of Array.from(sheet.cssRules)) {
|
||||
if (rule instanceof CSSStyleRule && rule.selectorText === '.title-field__input') {
|
||||
const fontSize = rule.style.getPropertyValue('font-size')
|
||||
const fontWeight = rule.style.getPropertyValue('font-weight')
|
||||
return {
|
||||
fontSize: fontSize.includes('--headings-h1-font-size'),
|
||||
fontWeight: fontWeight.includes('--headings-h1-font-weight'),
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { /* cross-origin sheet */ }
|
||||
}
|
||||
return { fontSize: false, fontWeight: false }
|
||||
})
|
||||
|
||||
expect(usesH1Vars.fontSize).toBe(true)
|
||||
expect(usesH1Vars.fontWeight).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user