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