Export template source from e33714a5ca38429d1bdb77a961a14461697944f5:examples/local-first/packages/language-reader-presentation
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
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),
|
||||
})),
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user