Files
package-language-reader-pre…/src/reader-extensions.tsx
T

74 lines
2.9 KiB
TypeScript
Raw Normal View History

import type { ExactReference } from "@quixos/camino-react";
import { Icon } from "@quixos/camino-react/ui";
import { copyText } from "./canvas/selection.js";
import type { ReaderExtension } from "./extensions.js";
import { VocabularyLabel, VocabularySettings } from "./vocabulary.js";
/** Register reader-wide configuration alongside the extension's contributions.
* The host retains this section in shared presentation navigation. */
export const readerExtensionSettings: readonly Pick<ReaderExtension, "id" | "title" | "Settings">[] = [
{ id: "vocabulary", title: "Vocabulary", Settings: VocabularySettings },
];
/** Install application extensions here. A sidebar and a context menu share the
* same source-aware actions; neither needs to pretend to be an annotator. */
export function createReaderExtensions(vocabulary: {
vocabulary: ExactReference | null;
name: string;
known: ReadonlySet<string>;
busy: string | null;
ready: boolean;
remember(word: string): Promise<void>;
report(error: unknown): void;
}): ReaderExtension[] {
return [
{
id: "source",
title: "Text",
actions: (context) => {
const text = context.target ? context.text.slice(context.target.start, context.target.end) : "";
return text ? [{ id: "copy", label: "Copy original text", run: () => copyText(text) }] : [];
},
},
{
...readerExtensionSettings[0]!,
sidebarVisible: (context) => !!context.target,
actions: (context) => {
const word = context.target ? context.text.slice(context.target.start, context.target.end).trim() : "";
if (!word || word.length > 100 || !vocabulary.vocabulary) return [];
const saved = vocabulary.known.has(word);
return [
{
id: "vocabulary.save",
label: saved ? "Saved to vocabulary" : vocabulary.busy === word ? "Saving…" : "Add to vocabulary",
disabled: saved || !!vocabulary.busy || !vocabulary.ready,
run: () => vocabulary.remember(word),
},
];
},
Sidebar: ({ context, actions }) =>
!context.target ? null : (
<section className="lr-word-actions" aria-label="Word actions">
<div className="lr-word-action-buttons">
{actions.map((action) => (
<button
type="button"
key={action.id}
disabled={action.disabled}
onClick={() => void Promise.resolve().then(action.run).catch(vocabulary.report)}
>
{action.id === "vocabulary.save" && <Icon name="plus" size={14} />} {action.content ?? action.label}
</button>
))}
</div>
{vocabulary.vocabulary && (
<small>
Saved in <VocabularyLabel reference={vocabulary.vocabulary} />
</small>
)}
</section>
),
},
];
}