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

195 lines
7.0 KiB
TypeScript

import { LocalInput, LocalSelect } from "@quixos/camino-react/ui";
import { ColorPicker, FontPicker, NumberControl, Options, Switch, UnderlinePreview } from "./controls.js";
import type { ComponentType } from "react";
import type { AnnotationRender } from "./render-hooks.js";
import { configDefaults, type ConfigValue, type ConfigValues, type ConfigNode } from "./config-schema.js";
export type { ConfigValue, ConfigValues, ConfigField, ConfigNode } from "./config-schema.js";
export type AnnotatorConfigProps = {
fields: ConfigNode[];
values: ConfigValues;
disabled: boolean;
change: (key: string, value: ConfigValue) => void;
};
// Authors may install a purpose-built inline component here. The component edits
// the same validated processor options as the generated controls; it does not
// own persistence or model calls. Every control, including appearance, is opt-in.
export const annotatorConfigViews: Record<string, ComponentType<AnnotatorConfigProps>> = {
pinyin: ({ fields, values, disabled, change }) => (
<div className="lr-hsk-control">
<label htmlFor="lr-hsk-level">Words above your HSK level</label>
<LocalInput
id="lr-hsk-level"
type="range"
min="0"
max="9"
step="1"
list="lr-hsk-notches"
aria-label="Your HSK level"
aria-valuetext={Number(values.minimumLevel ?? 2) ? `HSK ${values.minimumLevel ?? 2}` : "All words"}
value={Number(values.minimumLevel ?? 2)}
disabled={disabled}
onChange={(event) => change("minimumLevel", event.target.value)}
/>
<datalist id="lr-hsk-notches">
{Array.from({ length: 10 }, (_, level) => (
<option key={level} value={level} />
))}
</datalist>
<div className="lr-hsk-labels">
{Array.from({ length: 10 }, (_, level) => (
<span key={level}>{level || "All"}</span>
))}
</div>
<strong>{Number(values.minimumLevel ?? 2) ? `Above HSK ${values.minimumLevel ?? 2}` : "All words"}</strong>
<CustomConfig
processor="pinyin-appearance"
fields={fields.filter((field) => field.key !== "minimumLevel")}
values={values}
disabled={disabled}
change={change}
/>
</div>
),
};
export function configValues(raw: string): ConfigValues {
try {
const value = JSON.parse(raw);
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
} catch {
return {};
}
}
export function configDefinition(raw: string): {
fields: ConfigNode[];
description: string;
cache?: { identity: string; inputs: string[]; previousRevisions?: string[] };
renders?: AnnotationRender[];
} {
try {
const value = JSON.parse(raw);
return {
fields: value.fields ?? [],
description: value.description ?? "",
cache: value.cache,
renders: value.renders,
};
} catch {
return { fields: [], description: "" };
}
}
export const configFields = (raw: string) => configDefinition(raw).fields;
export function CustomConfig({
processor,
fields,
values,
change,
disabled,
}: AnnotatorConfigProps & { processor: string }) {
const Custom = annotatorConfigViews[processor];
if (Custom) return <Custom fields={fields} values={values} change={change} disabled={disabled} />;
return (
<div className="lr-processor-settings">
{fields.map((field) =>
field.kind === "group" ? (
<fieldset key={field.key} className="lr-config-group">
<legend>{field.label}</legend>
<CustomConfig
processor={processor + "/" + field.key}
fields={field.fields}
values={
typeof values[field.key] === "object"
? (values[field.key] as ConfigValues)
: configDefaults(field.fields)
}
disabled={disabled}
change={(key, value) =>
change(field.key, {
...configDefaults(field.fields),
...(typeof values[field.key] === "object" ? (values[field.key] as ConfigValues) : {}),
[key]: value,
})
}
/>
</fieldset>
) : (
<div key={field.key} className="lr-field">
<span>{field.label}</span>
{field.presentation === "font" ? (
<FontPicker
label={field.label}
value={String(values[field.key] ?? field.default)}
disabled={disabled}
inherit
change={(value) => change(field.key, value)}
/>
) : field.kind === "select" ? (
<Options
label={field.label}
value={String(values[field.key] ?? field.default)}
options={(field.options ?? []).map((option) => ({
...option,
...(field.presentation === "underline"
? { preview: <UnderlinePreview pattern={option.value} /> }
: {}),
}))}
disabled={disabled}
change={(value) => change(field.key, value)}
/>
) : field.kind === "toggle" ? (
<Switch
label={field.label}
value={Boolean(values[field.key] ?? field.default)}
disabled={disabled}
change={(value) => change(field.key, value)}
/>
) : field.kind === "color" ? (
<ColorPicker
label={field.label}
value={String(values[field.key] ?? field.default)}
disabled={disabled}
change={(value) => change(field.key, value)}
/>
) : field.kind === "number" ? (
<NumberControl
label={field.label}
value={Number(values[field.key] ?? field.default)}
disabled={disabled}
min={field.min}
max={field.max}
change={(value) => change(field.key, value)}
/>
) : (
<LocalInput
aria-label={field.label}
value={String(values[field.key] ?? field.default)}
disabled={disabled}
onChange={(e) => change(field.key, e.target.value)}
/>
)}
</div>
),
)}
</div>
);
}
// Creation suggestions only: these are never injected into a built annotator.
export const fontFields: ConfigNode[] = [
{ key: "fontFamily", label: "Annotation font", kind: "text", presentation: "font", default: "" },
{ key: "fontSize", label: "Annotation font size", kind: "number", default: 13, min: 6, max: 72 },
];
export const underlineFields: ConfigNode[] = [
{
key: "underlinePattern",
label: "Underline pattern",
kind: "select",
presentation: "underline",
default: "solid",
options: ["solid", "dashed", "squiggly"].map((value) => ({
value,
label: value[0].toUpperCase() + value.slice(1),
})),
},
];