diff --git a/src-react/src/App.tsx b/src-react/src/App.tsx
index 37aa416..5bde710 100644
--- a/src-react/src/App.tsx
+++ b/src-react/src/App.tsx
@@ -2,9 +2,10 @@ import "./App.css";
import { useEffect, useState } from "react";
-import { Button, Group, Input, Stack } from "@chakra-ui/react";
+import { Box, Button, Card, Group, Input, Stack } from "@chakra-ui/react";
import { invoke } from "@tauri-apps/api/core";
+import Glossary from "./components/Glossary";
import { Provider } from "./components/ui/provider";
function App() {
@@ -50,16 +51,35 @@ function App() {
value={q}
onChange={(e) => setQ(e.currentTarget.value)}
autoComplete="off"
+ spellCheck={false}
placeholder="Search..."
/>
-
- {entries.map((it, i) => (
- - {JSON.stringify(it)}
+
+ {entries.map(({ term, reading, glossary_json, ...it }, i) => (
+ -
+
+
+ {term} ({reading})
+
+
+
+ {JSON.stringify(it)}
+
+ {JSON.parse(glossary_json).map((g: any) => (
+ setQ(t + " ")}
+ />
+ ))}
+
+
+
+
))}
-
+
);
diff --git a/src-react/src/components/Glossary.tsx b/src-react/src/components/Glossary.tsx
new file mode 100644
index 0000000..5cc9eb1
--- /dev/null
+++ b/src-react/src/components/Glossary.tsx
@@ -0,0 +1,130 @@
+import { createElement as h } from "react";
+
+export default function Glossary({
+ glossary,
+ onTermClicked,
+}: {
+ glossary: any;
+ onTermClicked: (t: string) => void;
+}) {
+ // Plain string
+ if (typeof glossary === "string") {
+ return glossary;
+ }
+
+ // GlossaryText
+ if ("type" in glossary && glossary.type === "text") {
+ return glossary.text;
+ }
+
+ // GlossaryImage
+ if ("type" in glossary && glossary.type === "image") {
+ return ;
+ }
+
+ // GlossaryStructuredContent
+ if ("type" in glossary && glossary.type === "structured-content") {
+ const sc = glossary;
+ return (
+
+ );
+ }
+
+ // GlossaryDeinflection
+ if (Array.isArray(glossary) && glossary.length === 2) {
+ const [term, rules] = glossary;
+ return {`${term} (${rules.join(", ")})`};
+ }
+
+ return null;
+}
+
+function GlossaryImage(img: any) {
+ return (
+
+ );
+}
+
+function StructuredContent({
+ node,
+ onTermClicked,
+}: {
+ node: any;
+ onTermClicked: (t: string) => void;
+}): any {
+ if (typeof node === "string") {
+ return node;
+ }
+
+ if (Array.isArray(node)) {
+ return (
+ <>
+ {...node.map((n) => (
+
+ ))}
+ >
+ );
+ }
+
+ // Line break
+ if ("tag" in node && node.tag === "br") {
+ return
;
+ }
+
+ // Image
+ if ("tag" in node && node.tag === "img") {
+ return ;
+ }
+
+ // Link
+ if ("tag" in node && node.tag === "a") {
+ const link = node as any;
+ const { href, content } = link;
+
+ const isWebLink = /^https?:\/\//.test(href);
+
+ return (
+ {
+ if (isWebLink) return true;
+ ev.preventDefault();
+ const querySearch = "?query=";
+ if (href.startsWith(querySearch)) {
+ onTermClicked(href.substring(querySearch.length));
+ }
+ return false;
+ }}
+ >
+ {content ? (
+
+ ) : null}
+
+ );
+ }
+
+ // Block/container elements (span, div, ol, ul, li, ruby, table, etc.)
+ if ("tag" in node) {
+ const block = node as any;
+
+ return h(
+ block.tag,
+ { style: block.style, title: block.title, open: block.open },
+ StructuredContent({ node: block.content, onTermClicked }),
+ );
+ }
+
+ return null;
+}