Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ refactors, dependency updates, CI changes, and code cleanup do not belong here.

---

## [1.2.8] - 2026-08-15

### Changed

- Added consistent spacing between adjacent rendered code cards in Markdown live preview.
- Kept document location, status, live-preview, and line-wrapping controls pinned to the top of the editor while scrolling.

## [1.2.7] - 2026-08-14

### Changed
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/native-backend/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "neverwrite-native-backend"
version = "1.2.7"
version = "1.2.8"
edition = "2021"

[dependencies]
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "bifrostwrite-desktop",
"private": true,
"version": "1.2.7",
"version": "1.2.8",
"homepage": "https://github.com/bifrost-proxy/BifrostWrite",
"type": "module",
"engines": {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bifrostwrite-desktop"
version = "1.2.7"
version = "1.2.8"
edition = "2021"
description = "Tauri desktop shell for BifrostWrite"
license = "Apache-2.0"
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "BifrostWrite",
"version": "1.2.7",
"version": "1.2.8",
"identifier": "com.bifrostwrite.desktop",
"build": {
"beforeDevCommand": "npm run renderer:dev",
Expand Down
12 changes: 9 additions & 3 deletions apps/desktop/src/features/editor/Editor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2593,9 +2593,15 @@ describe("Editor", () => {
renderComponent(<Editor />);

const firstView = getEditorView();
expect(firstView.scrollDOM.firstElementChild).toHaveClass(
"cm-lp-scroll-header",
);
const firstScrollHeader = firstView.scrollDOM
.firstElementChild as HTMLElement;
expect(firstScrollHeader).toHaveClass("cm-lp-scroll-header");
expect(getComputedStyle(firstScrollHeader).display).toBe("contents");
expect(
firstScrollHeader.querySelector(
'[data-editor-note-toolbar="true"]',
),
).toHaveStyle({ position: "sticky", top: "0px" });

await act(async () => {
useEditorStore.getState().switchTab("tab-2");
Expand Down
38 changes: 34 additions & 4 deletions apps/desktop/src/features/editor/MarkdownNoteHeader.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ function renderHeader(lineWrapping: boolean) {
);

return {
spacer: document.querySelector(
'[data-editor-note-toolbar-spacer="true"]',
) as HTMLElement | null,
toolbar: document.querySelector(
'[data-editor-note-toolbar="true"]',
) as HTMLElement | null,
toolbarInner: document.querySelector(
'[data-editor-note-toolbar-inner="true"]',
) as HTMLElement | null,
outer: document.querySelector(
'[data-editor-note-header="true"]',
) as HTMLElement | null,
Expand All @@ -64,13 +73,29 @@ describe("MarkdownNoteHeader", () => {
});

it("keeps the centered reading layout when line wrapping is enabled", () => {
const { outer, inner } = renderHeader(true);
const { spacer, toolbar, toolbarInner, outer, inner } =
renderHeader(true);
expect(spacer).toHaveStyle({ height: "32px", flex: "0 0 100%" });
expect(toolbar).toHaveAttribute("data-line-wrapping", "true");
expect(toolbar).toHaveStyle({
position: "sticky",
top: "0px",
width: "100%",
flex: "0 0 100%",
padding: "6px var(--editor-horizontal-inset)",
});
expect(toolbarInner).toHaveStyle({
width: "min(100%, var(--editor-content-width))",
maxWidth: "var(--editor-content-width)",
margin: "0 auto",
});
expect(outer).not.toBeNull();
expect(inner).not.toBeNull();
expect(outer).toHaveAttribute("data-line-wrapping", "true");
expect(outer).toHaveStyle({
width: "100%",
padding: "40px var(--editor-horizontal-inset) 0",
padding: "14px var(--editor-horizontal-inset) 0",
flex: "0 0 100%",
});
expect(inner).toHaveStyle({
width: "min(100%, var(--editor-content-width))",
Expand All @@ -82,13 +107,18 @@ describe("MarkdownNoteHeader", () => {
});

it("switches to a left-aligned layout when line wrapping is disabled", () => {
const { outer, inner } = renderHeader(false);
const { toolbarInner, outer, inner } = renderHeader(false);
expect(outer).not.toBeNull();
expect(inner).not.toBeNull();
expect(outer).toHaveAttribute("data-line-wrapping", "false");
expect(outer).toHaveStyle({
width: "100%",
padding: "40px var(--editor-horizontal-inset) 0",
padding: "14px var(--editor-horizontal-inset) 0",
});
expect(toolbarInner).toHaveStyle({
width: "100%",
maxWidth: "none",
margin: "0px",
});
expect(inner).toHaveStyle({
width: "100%",
Expand Down
164 changes: 102 additions & 62 deletions apps/desktop/src/features/editor/MarkdownNoteHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,38 +182,56 @@ export function MarkdownNoteHeader({

const bannerCopy = status ? BANNER_COPY[status] : undefined;

const readingWidth = lineWrapping
? "min(100%, var(--editor-content-width))"
: "100%";
const readingMaxWidth = lineWrapping
? "var(--editor-content-width)"
: "none";
const readingMargin = lineWrapping ? "0 auto" : "0";

return (
<div
data-editor-note-header="true"
data-line-wrapping={String(lineWrapping)}
style={{
width: "100%",
padding: "40px var(--editor-horizontal-inset) 0",
boxSizing: "border-box",
}}
>
<>
<div
aria-hidden="true"
data-editor-note-toolbar-spacer="true"
style={{
flex: "0 0 100%",
width: "100%",
height: 32,
}}
/>
<div
data-editor-note-header-inner="true"
data-editor-note-toolbar="true"
data-line-wrapping={String(lineWrapping)}
style={{
width: lineWrapping
? "min(100%, var(--editor-content-width))"
: "100%",
maxWidth: lineWrapping
? "var(--editor-content-width)"
: "none",
minWidth: 0,
margin: lineWrapping ? "0 auto" : "0",
position: "sticky",
top: 0,
zIndex: 30,
flex: "0 0 100%",
width: "100%",
padding: "6px var(--editor-horizontal-inset)",
boxSizing: "border-box",
background:
"color-mix(in srgb, var(--bg-primary) 94%, var(--bg-secondary))",
borderBottom:
"1px solid color-mix(in srgb, var(--border) 72%, transparent)",
boxShadow: "0 5px 14px rgb(0 0 0 / 0.06)",
backdropFilter: "blur(14px)",
WebkitBackdropFilter: "blur(14px)",
}}
>
{/* Location breadcrumb + OKF status / type badges */}
<div
data-editor-note-toolbar-inner="true"
style={{
width: readingWidth,
maxWidth: readingMaxWidth,
minWidth: 0,
margin: readingMargin,
display: "flex",
alignItems: "center",
gap: 8,
flexWrap: "wrap",
minWidth: 0,
marginBottom: 14,
}}
>
{locationSegments.length > 0 && (
Expand Down Expand Up @@ -264,61 +282,83 @@ export function MarkdownNoteHeader({
<MetaBadge
label={translate("No OKF type")}
tone="muted"
title={translate("OKF vaults expect a type field in frontmatter")}
title={translate(
"OKF vaults expect a type field in frontmatter",
)}
onClick={() => {
if (!propertiesExpanded) onToggleProperties();
}}
/>
)}
</div>

{/* Title row */}
<EditableNoteTitle
value={editableTitle}
onChange={onTitleChange}
textareaRef={titleInputRef}
onContextMenu={onTitleContextMenu}
/>

{/* Toolbar: Properties toggle + Search */}
</div>
<div
data-editor-note-header="true"
data-line-wrapping={String(lineWrapping)}
style={{
flex: "0 0 100%",
width: "100%",
padding: "14px var(--editor-horizontal-inset) 0",
boxSizing: "border-box",
}}
>
<div
data-editor-note-header-inner="true"
style={{
display: "flex",
alignItems: "center",
gap: 4,
flexWrap: "wrap",
width: readingWidth,
maxWidth: readingMaxWidth,
minWidth: 0,
marginTop: 12,
marginBottom: 8,
margin: readingMargin,
}}
>
<ToolbarButton
label={translate("Properties")}
icon={<PropertiesIcon />}
active={propertiesExpanded}
onClick={onToggleProperties}
/>
<ToolbarButton
label={translate("Search")}
icon={<SearchIcon />}
onClick={onSearchClick}
{/* Title row */}
<EditableNoteTitle
value={editableTitle}
onChange={onTitleChange}
textareaRef={titleInputRef}
onContextMenu={onTitleContextMenu}
/>
</div>

{/* Trust banner (draft / in_review / deprecated / archived) */}
{status && bannerCopy && (
<TrustBanner status={status} copy={bannerCopy} />
)}

{/* Properties body (expanded below toolbar) */}
{propertiesExpanded && (
<div style={{ minWidth: 0, marginBottom: 8 }}>
<FrontmatterBody
raw={frontmatterRaw}
onChange={onFrontmatterChange}
{/* Toolbar: Properties toggle + Search */}
<div
style={{
display: "flex",
alignItems: "center",
gap: 4,
flexWrap: "wrap",
minWidth: 0,
marginTop: 12,
marginBottom: 8,
}}
>
<ToolbarButton
label={translate("Properties")}
icon={<PropertiesIcon />}
active={propertiesExpanded}
onClick={onToggleProperties}
/>
<ToolbarButton
label={translate("Search")}
icon={<SearchIcon />}
onClick={onSearchClick}
/>
</div>
)}

{/* Trust banner (draft / in_review / deprecated / archived) */}
{status && bannerCopy && (
<TrustBanner status={status} copy={bannerCopy} />
)}

{/* Properties body (expanded below toolbar) */}
{propertiesExpanded && (
<div style={{ minWidth: 0, marginBottom: 8 }}>
<FrontmatterBody
raw={frontmatterRaw}
onChange={onFrontmatterChange}
/>
</div>
)}
</div>
</div>

{statusMenu && (
Expand All @@ -340,7 +380,7 @@ export function MarkdownNoteHeader({
]}
/>
)}
</div>
</>
);
}

Expand Down
6 changes: 4 additions & 2 deletions apps/desktop/src/features/editor/editorExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,10 @@ export const baseTheme = EditorView.theme({
overflowY: "auto",
},
".cm-lp-scroll-header": {
flex: "0 0 100%",
boxSizing: "border-box",
// Let the spacer, sticky action bar, and document header participate
// directly in the scroller's flex layout. A sticky descendant would
// otherwise stop sticking at the bottom of this short wrapper.
display: "contents",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve a measurable box for viewport anchoring

When a user scrolls past the new sticky toolbar and switches tabs, display: contents makes scrollHeaderRef.current.getBoundingClientRect() in captureViewportAnchor return an empty rectangle. The anchor is consequently sampled at scrollRect.top + 8, underneath the sticky toolbar rather than below it; posAtCoords can then return no document position, causing the fallback viewport.from anchor to restore the tab at the wrong vertical position or behind the toolbar. Query the sticky toolbar's rectangle for anchoring, or otherwise retain a measurable header box.

Useful? React with 👍 / 👎.

},
".cm-content": {
flex: "1 1 0%",
Expand Down
Loading