74 lines
2.9 KiB
TypeScript
74 lines
2.9 KiB
TypeScript
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>
|
||
|
|
),
|
||
|
|
},
|
||
|
|
];
|
||
|
|
}
|