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
8 changes: 4 additions & 4 deletions src/editor/AssetsDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ export function AssetsDrawer({ onInsert, onClose, onUpload }: Props) {
};

return (
<aside className="assets-drawer" role="dialog" aria-label="Asset library">
<header className="assets-drawer__head">
<h2>Assets</h2>
<button type="button" className="assets-drawer__close" onClick={onClose} aria-label="Close">
<aside className="drawer assets-drawer" role="dialog" aria-label="Asset library">
<header className="drawer__header">
<h2 className="drawer__title">Assets</h2>
<button type="button" className="drawer__close" onClick={onClose} aria-label="Close">
×
</button>
</header>
Expand Down
10 changes: 8 additions & 2 deletions src/editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,10 @@ export function Editor({ deckId }: Props) {
className={`editor__nav-link editor__nav-link--button ${
assetsOpen ? 'editor__nav-link--active' : ''
}`}
onClick={() => setAssetsOpen((v) => !v)}
onClick={() => {
setAssetsOpen((v) => !v);
setDrawerOpen(false);
}}
aria-pressed={assetsOpen}
>
Assets
Expand All @@ -298,7 +301,10 @@ export function Editor({ deckId }: Props) {
className={`editor__nav-link editor__nav-link--button ${
drawerOpen ? 'editor__nav-link--active' : ''
}`}
onClick={() => setDrawerOpen((v) => !v)}
onClick={() => {
setDrawerOpen((v) => !v);
setAssetsOpen(false);
}}
aria-pressed={drawerOpen}
>
Design
Expand Down
47 changes: 1 addition & 46 deletions src/editor/editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -1508,52 +1508,7 @@
line-height: 1;
}

/* ─── Assets drawer ─────────────────────────────────────────────────────── */

.assets-drawer {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 360px;
background: var(--surface);
border-left: 1px solid var(--line);
display: flex;
flex-direction: column;
z-index: 30;
box-shadow: -16px 0 32px -16px rgba(0, 0, 0, 0.25);
}

.assets-drawer__head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--line);
}

.assets-drawer__head h2 {
margin: 0;
font-size: 14px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--text);
}

.assets-drawer__close {
background: transparent;
border: none;
color: var(--text-muted);
font-size: 22px;
line-height: 1;
cursor: pointer;
padding: 4px 8px;
}

.assets-drawer__close:hover {
color: var(--text);
}
/* ─── Assets drawer (rides on top of the shared .drawer styles) ────── */

.assets-drawer__upload {
padding: 16px 20px;
Expand Down
38 changes: 23 additions & 15 deletions src/ir/source-edit.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,48 @@
import matter from 'gray-matter';

const SLIDE_MARKER_RE = /^::slide(\{[^}]*\})?\s*$/;

/**
* Split the markdown source into slide sections without losing frontmatter.
* Returns the frontmatter prefix plus an array of slide-section strings, each
* including its leading `::slide` line if present.
* Split markdown into a frontmatter prefix and a list of marker-less slide
* chunks. The first slide implicitly has no `::slide` marker; subsequent
* slides start with one. We strip the marker from every chunk so reordering
* can shuffle slides freely; `joinSlides` re-emits markers between them.
*/
function splitSourceIntoSlides(source: string): { prefix: string; slides: string[] } {
const fm = matter(source);
const body = fm.content;
const lines = body.split('\n');
const sections: string[][] = [[]];
for (const line of lines) {
if (/^::slide(\{[^}]*\})?\s*$/.test(line)) {
sections.push([line]);
if (SLIDE_MARKER_RE.test(line)) {
sections.push([]);
continue;
}
sections[sections.length - 1].push(line);
}
const slides = sections.map((chunk) => chunk.join('\n'));
// The first chunk may be empty if the very first line is ::slide; that is
// still a slide. Filter out an empty leading chunk only when there is no
// body content at all, otherwise preserve.
// If the body started with `::slide` (or only blank lines before one),
// the leading empty section is just whitespace — drop it so we don't
// turn it into a phantom slide.
if (sections.length > 1 && sections[0].every((l) => l.trim() === '')) {
sections.shift();
}
const slides = sections.map((chunk) => chunk.join('\n').trim());
const fmPrefix = source.slice(0, source.length - body.length);
return { prefix: fmPrefix, slides };
}

function joinSlides(prefix: string, slides: string[]): string {
// Reassemble with the original frontmatter prefix plus the slide chunks
// joined back with newlines. Each chunk that started with ::slide already
// carries that marker; the very first chunk does not, so we just join.
return prefix + slides.join('\n').replace(/\n+$/, '\n');
if (slides.length === 0) return prefix;
const body = slides.filter((s) => s.length > 0).join('\n\n::slide\n\n');
let out = prefix;
if (out.length > 0 && !out.endsWith('\n')) out += '\n';
if (out.length > 0) out += '\n';
return out + body + '\n';
}

/**
* Move slide at `from` index so it appears at `to` index. Pure: same input,
* same output. Returns the new source string. Out-of-range indices are no-ops.
* Move slide at `from` to position `to`. Out-of-range indices and `from === to`
* are no-ops. Pure: same input, same output.
*/
export function reorderSlide(source: string, from: number, to: number): string {
if (from === to) return source;
Expand Down
20 changes: 20 additions & 0 deletions tests/ir/source-edit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,24 @@ title: Demo
const next = reorderSlide(sample, 0, 2);
expect(next).toMatch(/^---\ntitle: Demo\n---/);
});

it('preserves slide count after a reorder (no merge, no phantom slide)', () => {
const moves: [number, number][] = [
[0, 2],
[2, 0],
[1, 0],
[0, 1],
];
for (const [from, to] of moves) {
const next = reorderSlide(sample, from, to);
expect(countSlides(next)).toBe(3);
}
});

it('handles a body that starts with ::slide marker without phantom slide', () => {
const src = '::slide\n\n# A\n\n::slide\n\n# B\n';
expect(countSlides(src)).toBe(2);
const next = reorderSlide(src, 0, 1);
expect(countSlides(next)).toBe(2);
});
});
Loading