Skip to content
Draft
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
59 changes: 59 additions & 0 deletions apps/web/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1610,6 +1610,65 @@ button {
padding: 8px;
}

/* Tab group (```tabs fenced block) */
.tab-group {
border: 1px solid var(--border);
border-radius: 6px;
overflow: hidden;
margin: 12px 0;
}

.tab-group__bar {
display: flex;
flex-wrap: wrap;
gap: 0;
background: var(--bg-subtle, var(--bg-2));
border-bottom: 1px solid var(--border);
}

.tab-group__tab {
padding: 6px 16px;
border: none;
background: transparent;
color: var(--fg-muted);
cursor: pointer;
font-size: 13px;
border-bottom: 2px solid transparent;
transition:
color 0.15s,
border-color 0.15s;
user-select: none;
}

.tab-group__tab:hover {
color: var(--fg);
}

.tab-group__tab--active {
color: var(--accent, var(--fg));
border-bottom-color: var(--accent, var(--fg));
}

.tab-group__panel {
padding: 12px 16px;
background: var(--bg);
}

.tab-group__panel > *:first-child {
margin-top: 0;
}

.tab-group__panel > *:last-child {
margin-bottom: 0;
}

.tab-group__empty {
padding: 12px 16px;
color: var(--fg-muted);
font-style: italic;
font-size: 13px;
}

.image-note {
display: flex;
flex-direction: column;
Expand Down
32 changes: 2 additions & 30 deletions package-lock.json

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

1 change: 1 addition & 0 deletions packages/editor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from "./note-toolbar";
export * from "./suggestion-popup";
export * from "./wikilink-decorator";
export * from "./embed-extension";
export * from "./tabs-extension";
export * from "./image-node";
export * from "./prompt-dialog";
export * from "./native-source-editor";
2 changes: 2 additions & 0 deletions packages/editor/src/rendered-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from "react";
import { Markdown } from "tiptap-markdown";
import { Embed } from "./embed-extension";
import { TabGroupExtension } from "./tabs-extension";
import { ImageNode } from "./image-node";
import { StyledTextMark } from "./styled-text-mark";
import { SuggestionPopup } from "./suggestion-popup";
Expand Down Expand Up @@ -184,6 +185,7 @@ export function RenderedEditor({
StyledTextMark,
Markdown.configure({ html: true, tightLists: true, transformPastedText: true }),
WikilinkDecorator,
TabGroupExtension,
...(callbacks?.renderEmbed
? [
Embed.configure({
Expand Down
62 changes: 62 additions & 0 deletions packages/editor/src/tabs-extension.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { describe, expect, it } from "vitest";
import { parseTabsContent, serializeTabsContent } from "./tabs-extension";

describe("parseTabsContent", () => {
it("parses two tabs separated by ## headings", () => {
const input = "## Tab 1\nHello from tab one.\n\n## Tab 2\nHello from tab two.";
const result = parseTabsContent(input);
expect(result).toHaveLength(2);
expect(result[0]).toEqual({ label: "Tab 1", content: "Hello from tab one." });
expect(result[1]).toEqual({ label: "Tab 2", content: "Hello from tab two." });
});

it("trims surrounding whitespace from each tab's content", () => {
const input = "## My Tab\n\n content here \n\n";
const result = parseTabsContent(input);
expect(result[0].content).toBe("content here");
});

it("returns empty array when no ## headings are present", () => {
const result = parseTabsContent("just some text with no headings");
expect(result).toHaveLength(0);
});

it("discards text before the first ## heading", () => {
const input = "preamble\n\n## First\ncontent";
const result = parseTabsContent(input);
expect(result).toHaveLength(1);
expect(result[0].label).toBe("First");
});

it("preserves multi-line content within a tab", () => {
const input = "## Tab A\nLine 1\nLine 2\n\n## Tab B\nOnly line";
const result = parseTabsContent(input);
expect(result[0].content).toBe("Line 1\nLine 2");
expect(result[1].content).toBe("Only line");
});

it("handles a single tab", () => {
const result = parseTabsContent("## Solo\nSingle tab content.");
expect(result).toHaveLength(1);
expect(result[0]).toEqual({ label: "Solo", content: "Single tab content." });
});
});

describe("serializeTabsContent", () => {
it("round-trips through parse → serialize", () => {
const original = "## Tab 1\nContent one\n\n## Tab 2\nContent two";
const tabs = parseTabsContent(original);
const serialized = serializeTabsContent(tabs);
const reparsed = parseTabsContent(serialized);
expect(reparsed).toEqual(tabs);
});

it("returns empty string for empty tab array", () => {
expect(serializeTabsContent([])).toBe("");
});

it("produces the expected format for a single tab", () => {
const tabs = [{ label: "Foo", content: "bar" }];
expect(serializeTabsContent(tabs)).toBe("## Foo\nbar");
});
});
Loading