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
73 changes: 28 additions & 45 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useCallback, useState, useEffect, useRef } from "react";
import { openUrl } from "@tauri-apps/plugin-opener";
import { Sparkles } from "lucide-react";
import { PencilLine, Sparkles } from "lucide-react";
import { Editor } from "./components/Editor";
import { EmptyState as EmptyStateShell } from "./components/EmptyState";
import { PdfPreview } from "./components/PdfPreview";
import { StatusBar } from "./components/StatusBar";
import { Sidebar } from "./components/Sidebar";
Expand Down Expand Up @@ -195,15 +196,10 @@ function App() {
{!sidebarCollapsed && (
<div
onMouseDown={startSidebarResize}
className={`w-1.5 bg-[var(--border-light)] hover:bg-[var(--accent)] cursor-col-resize transition-colors shrink-0 relative group ${
resizingPanel === "sidebar" ? "bg-[var(--accent)]" : ""
className={`pane-resize-handle ${
resizingPanel === "sidebar" ? "is-active" : ""
}`}
>
<div className="absolute inset-y-0 -left-1 -right-1" />
{resizingPanel === "sidebar" && (
<div className="absolute inset-y-0 left-0 right-0 bg-[var(--accent)]/20" />
)}
</div>
/>
)}

{/* Main content area */}
Expand All @@ -224,15 +220,10 @@ function App() {
<div
ref={resizeRef}
onMouseDown={startPdfResize}
className={`w-1.5 bg-[var(--border-light)] hover:bg-[var(--accent)] cursor-col-resize transition-colors shrink-0 relative group ${
resizingPanel === "pdf" ? "bg-[var(--accent)]" : ""
className={`pane-resize-handle ${
resizingPanel === "pdf" ? "is-active" : ""
}`}
>
<div className="absolute inset-y-0 -left-1 -right-1" />
{resizingPanel === "pdf" && (
<div className="absolute inset-y-0 left-0 right-0 bg-[var(--accent)]/20" />
)}
</div>
/>
)}

{/* PDF Preview panel */}
Expand Down Expand Up @@ -281,34 +272,26 @@ function EmptyState() {
}, [workspacePath, createDocumentFromTemplate, openingExample]);

return (
<div className="h-full flex flex-col items-center justify-center gap-4">
<div className="w-16 h-20 rounded-xl border-2 border-dashed border-[var(--border)] flex flex-col items-center justify-center gap-2 bg-[var(--bg-secondary)]">
<span className="text-[24px] text-[var(--text-tertiary)]">
{"\u270E"}
</span>
</div>
<div className="text-center">
<p className="text-[14px] font-medium text-[var(--text-secondary)]">
{t("editor.noDocument")}
</p>
<p className="text-[12px] text-[var(--text-tertiary)] mt-1">
{t("editor.noDocumentHint")}
</p>
</div>
{workspacePath && (
<button
type="button"
onClick={handleOpenExample}
disabled={openingExample}
className="btn btn-soft mt-2"
>
<Sparkles size={14} />
{openingExample
? t("empty.openingExample")
: t("empty.openExample")}
</button>
)}
</div>
<EmptyStateShell
icon={<PencilLine size={22} />}
title={t("editor.noDocument")}
description={t("editor.noDocumentHint")}
cta={
workspacePath ? (
<button
type="button"
onClick={handleOpenExample}
disabled={openingExample}
className="btn btn-soft"
>
<Sparkles size={14} />
{openingExample
? t("empty.openingExample")
: t("empty.openExample")}
</button>
) : undefined
}
/>
);
}

Expand Down
21 changes: 10 additions & 11 deletions src/components/CoverPageDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,15 @@ export function CoverPageDialog({ open, block, editor, onClose }: Props) {
}}
>
<div className="cover-dialog" role="dialog" aria-modal="true">
{/* Header — single line with the dialog name on the left and the
preview-toggle + close button on the right. The earlier
decorative L logo + tag + subtitle stripe was three pieces of
chrome conveying one fact ("you're in the cover-page editor"),
which is already obvious from the modal context. */}
<div className="cover-dialog-header">
<div className="cover-dialog-header-left">
<div className="cover-dialog-logo" aria-hidden="true">
L
</div>
<div className="cover-dialog-titlegroup">
<div className="cover-dialog-tag">{t("cover.tag")}</div>
<div className="cover-dialog-subtitle">
{t("cover.dialogSubtitle")}
</div>
<div className="cover-dialog-titlegroup">
<div className="cover-dialog-subtitle">
{t("cover.dialogSubtitle")}
</div>
</div>
<div className="cover-dialog-header-right">
Expand All @@ -112,11 +111,11 @@ export function CoverPageDialog({ open, block, editor, onClose }: Props) {
<button
type="button"
onClick={onClose}
className="icon-btn"
className="icon-btn icon-btn-sm"
title={t("cover.close")}
aria-label={t("cover.close")}
>
<X size={15} />
<X size={14} />
</button>
</div>
</div>
Expand Down
7 changes: 6 additions & 1 deletion src/components/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,12 @@ export function Editor() {
editor={editor}
onInsertCitation={openCitationPicker}
/>
<div className="flex-1 min-h-0 overflow-auto" ref={editorContainerRef} onContextMenu={(e) => e.preventDefault()}>
{/* `relative` is required: flashCurrentLine appends an absolutely-
positioned overlay to this element with coordinates computed
relative to its scroll origin. Without a positioned ancestor
here, the overlay would resolve against the viewport (or some
ancestor) and land on the wrong row. */}
<div className="flex-1 min-h-0 overflow-auto relative" ref={editorContainerRef} onContextMenu={(e) => e.preventDefault()}>
<div className={`editor-content-wrap editor-citations-${citationDisplay}`}>
<BlockNoteView
editor={editor}
Expand Down
102 changes: 56 additions & 46 deletions src/components/EditorTopToolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import {
Bold,
BookMarked,
Check,
ChevronDown,
Eye,
EyeOff,
Italic,
List,
ListOrdered,
Pilcrow,
Quote,
Superscript,
Type,
Underline,
} from "lucide-react";
import type { BlockNoteEditor } from "@blocknote/core";
Expand Down Expand Up @@ -98,13 +97,6 @@ export function EditorTopToolbar({ editor, onInsertCitation }: EditorTopToolbarP
focus();
}

function setParagraph() {
const block = editor.getTextCursorPosition().block;
if (!block) return;
editor.updateBlock(block, { type: "paragraph", props: {} as any });
focus();
}

function setList(kind: "bullet" | "numbered") {
const block = editor.getTextCursorPosition().block;
if (!block) return;
Expand All @@ -129,6 +121,9 @@ export function EditorTopToolbar({ editor, onInsertCitation }: EditorTopToolbarP
<div className="editor-toolbar">
<div className="editor-toolbar-inner">
<div className="editor-toolbar-group">
{/* Headings only — clicking an already-active heading button
demotes the block back to a paragraph (see setHeading below),
so a dedicated Pilcrow paragraph button is redundant. */}
<ToolbarTextBtn
label="Heading 1"
active={blockType === "heading-1"}
Expand All @@ -143,13 +138,6 @@ export function EditorTopToolbar({ editor, onInsertCitation }: EditorTopToolbarP
>
H2
</ToolbarTextBtn>
<ToolbarBtn
label="Paragraph"
active={blockType === "paragraph"}
onClick={setParagraph}
>
<Pilcrow size={14} />
</ToolbarBtn>
</div>

<div className="editor-toolbar-divider" aria-hidden="true" />
Expand Down Expand Up @@ -193,39 +181,17 @@ export function EditorTopToolbar({ editor, onInsertCitation }: EditorTopToolbarP
>
<BookMarked size={14} />
</ToolbarBtn>
<CitationDisplayToggle />

{/* StyleChip carries the citation-style picker AND the chip /
footnote display toggle — they're conceptually one menu (how
citations render). The earlier Eye / EyeOff button has moved
inside the chip's dropdown. */}
<StyleChip />
</div>
</div>
);
}

/**
* Eye toggle that flips between `chip` and `footnote` rendering of citation
* tags in the editor. Purely a display setting — the underlying document is
* untouched.
*/
function CitationDisplayToggle() {
const display = useReferenceStore((s) => s.citationDisplay);
const toggle = useReferenceStore((s) => s.toggleCitationDisplay);
const isFootnote = display === "footnote";
const label = isFootnote
? "Citation display: footnote — show as chip"
: "Citation display: chip — show as footnote";
return (
<button
type="button"
className={`editor-toolbar-btn ${isFootnote ? "is-active" : ""}`}
title={label}
aria-label={label}
aria-pressed={isFootnote}
onClick={toggle}
>
{isFootnote ? <EyeOff size={14} /> : <Eye size={14} />}
</button>
);
}

interface BtnProps {
label: string;
Expand Down Expand Up @@ -272,13 +238,22 @@ function ToolbarTextBtn({ label, active, onClick, children }: BtnProps) {
function StyleChip() {
const citationStyle = useReferenceStore((s) => s.citationStyle);
const setCitationStyle = useReferenceStore((s) => s.setCitationStyle);
const display = useReferenceStore((s) => s.citationDisplay);
const toggleDisplay = useReferenceStore((s) => s.toggleCitationDisplay);
const [open, setOpen] = useState(false);
const [pos, setPos] = useState<{ top: number; left: number } | null>(null);
const anchorRef = useRef<HTMLButtonElement>(null);
const portalRef = useRef<HTMLDivElement>(null);

const editionLabel = STYLE_EDITION[citationStyle.toLowerCase()] ?? "";

const setDisplay = useCallback(
(mode: "chip" | "footnote") => {
if (display !== mode) toggleDisplay();
},
[display, toggleDisplay]
);

useEffect(() => {
if (!open) return;
const onDown = (event: MouseEvent) => {
Expand All @@ -294,7 +269,7 @@ function StyleChip() {
useEffect(() => {
if (!open || !anchorRef.current) return;
const r = anchorRef.current.getBoundingClientRect();
const menuWidth = 180;
const menuWidth = 200;
setPos({
top: r.bottom + 6,
left: Math.max(8, r.right - menuWidth),
Expand Down Expand Up @@ -332,8 +307,43 @@ function StyleChip() {
left: pos.left,
zIndex: 9999,
}}
className="menu-surface w-[180px] py-1 animate-fade-in"
className="menu-surface w-[200px] py-1 animate-fade-in"
>
{/* Display group — flips between in-line @key chips and
superscript-numbered footnote markers in the editor.
The PDF output is unaffected; this is purely how citations
render on screen. */}
<div className="px-3 pt-1.5 pb-1 text-[11px] font-bold uppercase tracking-wide text-[var(--text-tertiary)]">
Display
</div>
<button
type="button"
onClick={() => { setDisplay("chip"); setOpen(false); }}
className={`menu-item justify-between ${display === "chip" ? "text-[var(--accent-dark)] bg-[var(--accent-light)]" : ""}`}
>
<span className="inline-flex items-center gap-2">
<Type size={12} className="shrink-0" />
Inline chip
</span>
{display === "chip" && <Check size={12} className="shrink-0" />}
</button>
<button
type="button"
onClick={() => { setDisplay("footnote"); setOpen(false); }}
className={`menu-item justify-between ${display === "footnote" ? "text-[var(--accent-dark)] bg-[var(--accent-light)]" : ""}`}
>
<span className="inline-flex items-center gap-2">
<Superscript size={12} className="shrink-0" />
Footnote marker
</span>
{display === "footnote" && <Check size={12} className="shrink-0" />}
</button>

<div className="my-1 border-t border-[var(--border-light)]" />

<div className="px-3 pt-1 pb-1 text-[11px] font-bold uppercase tracking-wide text-[var(--text-tertiary)]">
Style
</div>
{ordered.map((name, i) => {
const active = name === citationStyle;
return (
Expand Down
48 changes: 48 additions & 0 deletions src/components/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { ReactNode } from "react";

/**
* Shared empty-state shell. Three places in the app show "nothing here
* yet" prompts (no workspace, no document, no PDF) and they previously
* each rolled their own dashed-border boxes at different sizes, fonts,
* and button treatments. This component standardises the visual scale
* so they read as the same kind of moment, even when the content
* differs.
*
* The icon prop receives the icon node (sized however the caller likes,
* but 22 px works best inside the 56 px container). The CTA is optional
* — leave it out for purely informative states.
*/
export function EmptyState({
icon,
title,
description,
cta,
className = "",
}: {
icon: ReactNode;
title: ReactNode;
description?: ReactNode;
cta?: ReactNode;
className?: string;
}) {
return (
<div
className={`flex-1 flex flex-col items-center justify-center gap-3 px-6 ${className}`}
>
<div className="w-14 h-14 rounded-xl bg-[var(--bg-tertiary)] flex items-center justify-center text-[var(--text-tertiary)]">
{icon}
</div>
<div className="text-center max-w-[240px]">
<p className="text-[13px] font-medium text-[var(--text-primary)]">
{title}
</p>
{description && (
<p className="text-[12px] text-[var(--text-tertiary)] mt-1 leading-relaxed">
{description}
</p>
)}
</div>
{cta && <div className="mt-1">{cta}</div>}
</div>
);
}
Loading
Loading