Merge pull request #2529 from lc6464/feat/web-code-highlight
feat(web): add markdown syntax highlighting for chat and skills
This commit is contained in:
commit
100e576609
7 changed files with 154 additions and 7 deletions
|
|
@ -26,6 +26,7 @@
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.20",
|
"dayjs": "^1.11.20",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
"i18next": "^26.0.3",
|
"i18next": "^26.0.3",
|
||||||
"i18next-browser-languagedetector": "^8.2.1",
|
"i18next-browser-languagedetector": "^8.2.1",
|
||||||
"jotai": "^2.19.1",
|
"jotai": "^2.19.1",
|
||||||
|
|
@ -35,6 +36,7 @@
|
||||||
"react-i18next": "^17.0.2",
|
"react-i18next": "^17.0.2",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-textarea-autosize": "^8.5.9",
|
"react-textarea-autosize": "^8.5.9",
|
||||||
|
"rehype-highlight": "^7.0.2",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"rehype-sanitize": "^6.0.0",
|
"rehype-sanitize": "^6.0.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
|
|
|
||||||
57
web/frontend/pnpm-lock.yaml
generated
57
web/frontend/pnpm-lock.yaml
generated
|
|
@ -35,6 +35,9 @@ importers:
|
||||||
dayjs:
|
dayjs:
|
||||||
specifier: ^1.11.20
|
specifier: ^1.11.20
|
||||||
version: 1.11.20
|
version: 1.11.20
|
||||||
|
highlight.js:
|
||||||
|
specifier: ^11.11.1
|
||||||
|
version: 11.11.1
|
||||||
i18next:
|
i18next:
|
||||||
specifier: ^26.0.3
|
specifier: ^26.0.3
|
||||||
version: 26.0.3(typescript@5.9.3)
|
version: 26.0.3(typescript@5.9.3)
|
||||||
|
|
@ -62,6 +65,9 @@ importers:
|
||||||
react-textarea-autosize:
|
react-textarea-autosize:
|
||||||
specifier: ^8.5.9
|
specifier: ^8.5.9
|
||||||
version: 8.5.9(@types/react@19.2.14)(react@19.2.5)
|
version: 8.5.9(@types/react@19.2.14)(react@19.2.5)
|
||||||
|
rehype-highlight:
|
||||||
|
specifier: ^7.0.2
|
||||||
|
version: 7.0.2
|
||||||
rehype-raw:
|
rehype-raw:
|
||||||
specifier: ^7.0.0
|
specifier: ^7.0.0
|
||||||
version: 7.0.0
|
version: 7.0.0
|
||||||
|
|
@ -2433,6 +2439,9 @@ packages:
|
||||||
hast-util-from-parse5@8.0.3:
|
hast-util-from-parse5@8.0.3:
|
||||||
resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
|
resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
|
||||||
|
|
||||||
|
hast-util-is-element@3.0.0:
|
||||||
|
resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==}
|
||||||
|
|
||||||
hast-util-parse-selector@4.0.0:
|
hast-util-parse-selector@4.0.0:
|
||||||
resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
|
resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
|
||||||
|
|
||||||
|
|
@ -2448,6 +2457,9 @@ packages:
|
||||||
hast-util-to-parse5@8.0.1:
|
hast-util-to-parse5@8.0.1:
|
||||||
resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==}
|
resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==}
|
||||||
|
|
||||||
|
hast-util-to-text@4.0.2:
|
||||||
|
resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==}
|
||||||
|
|
||||||
hast-util-whitespace@3.0.0:
|
hast-util-whitespace@3.0.0:
|
||||||
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
|
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
|
||||||
|
|
||||||
|
|
@ -2463,6 +2475,10 @@ packages:
|
||||||
hermes-parser@0.25.1:
|
hermes-parser@0.25.1:
|
||||||
resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
|
resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
|
||||||
|
|
||||||
|
highlight.js@11.11.1:
|
||||||
|
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
hono@4.12.12:
|
hono@4.12.12:
|
||||||
resolution: {integrity: sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==}
|
resolution: {integrity: sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==}
|
||||||
engines: {node: '>=16.9.0'}
|
engines: {node: '>=16.9.0'}
|
||||||
|
|
@ -2807,6 +2823,9 @@ packages:
|
||||||
longest-streak@3.1.0:
|
longest-streak@3.1.0:
|
||||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||||
|
|
||||||
|
lowlight@3.3.0:
|
||||||
|
resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==}
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||||
|
|
||||||
|
|
@ -3371,6 +3390,9 @@ packages:
|
||||||
resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==}
|
resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
|
rehype-highlight@7.0.2:
|
||||||
|
resolution: {integrity: sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==}
|
||||||
|
|
||||||
rehype-raw@7.0.0:
|
rehype-raw@7.0.0:
|
||||||
resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
|
resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
|
||||||
|
|
||||||
|
|
@ -3686,6 +3708,9 @@ packages:
|
||||||
unified@11.0.5:
|
unified@11.0.5:
|
||||||
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
|
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
|
||||||
|
|
||||||
|
unist-util-find-after@5.0.0:
|
||||||
|
resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==}
|
||||||
|
|
||||||
unist-util-is@6.0.1:
|
unist-util-is@6.0.1:
|
||||||
resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==}
|
resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==}
|
||||||
|
|
||||||
|
|
@ -6253,6 +6278,10 @@ snapshots:
|
||||||
vfile-location: 5.0.3
|
vfile-location: 5.0.3
|
||||||
web-namespaces: 2.0.1
|
web-namespaces: 2.0.1
|
||||||
|
|
||||||
|
hast-util-is-element@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/hast': 3.0.4
|
||||||
|
|
||||||
hast-util-parse-selector@4.0.0:
|
hast-util-parse-selector@4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/hast': 3.0.4
|
'@types/hast': 3.0.4
|
||||||
|
|
@ -6309,6 +6338,13 @@ snapshots:
|
||||||
web-namespaces: 2.0.1
|
web-namespaces: 2.0.1
|
||||||
zwitch: 2.0.4
|
zwitch: 2.0.4
|
||||||
|
|
||||||
|
hast-util-to-text@4.0.2:
|
||||||
|
dependencies:
|
||||||
|
'@types/hast': 3.0.4
|
||||||
|
'@types/unist': 3.0.3
|
||||||
|
hast-util-is-element: 3.0.0
|
||||||
|
unist-util-find-after: 5.0.0
|
||||||
|
|
||||||
hast-util-whitespace@3.0.0:
|
hast-util-whitespace@3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/hast': 3.0.4
|
'@types/hast': 3.0.4
|
||||||
|
|
@ -6329,6 +6365,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
hermes-estree: 0.25.1
|
hermes-estree: 0.25.1
|
||||||
|
|
||||||
|
highlight.js@11.11.1: {}
|
||||||
|
|
||||||
hono@4.12.12: {}
|
hono@4.12.12: {}
|
||||||
|
|
||||||
html-parse-stringify@3.0.1:
|
html-parse-stringify@3.0.1:
|
||||||
|
|
@ -6574,6 +6612,12 @@ snapshots:
|
||||||
|
|
||||||
longest-streak@3.1.0: {}
|
longest-streak@3.1.0: {}
|
||||||
|
|
||||||
|
lowlight@3.3.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/hast': 3.0.4
|
||||||
|
devlop: 1.1.0
|
||||||
|
highlight.js: 11.11.1
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
yallist: 3.1.1
|
yallist: 3.1.1
|
||||||
|
|
@ -7350,6 +7394,14 @@ snapshots:
|
||||||
tiny-invariant: 1.3.3
|
tiny-invariant: 1.3.3
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
rehype-highlight@7.0.2:
|
||||||
|
dependencies:
|
||||||
|
'@types/hast': 3.0.4
|
||||||
|
hast-util-to-text: 4.0.2
|
||||||
|
lowlight: 3.3.0
|
||||||
|
unist-util-visit: 5.1.0
|
||||||
|
vfile: 6.0.3
|
||||||
|
|
||||||
rehype-raw@7.0.0:
|
rehype-raw@7.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/hast': 3.0.4
|
'@types/hast': 3.0.4
|
||||||
|
|
@ -7744,6 +7796,11 @@ snapshots:
|
||||||
trough: 2.2.0
|
trough: 2.2.0
|
||||||
vfile: 6.0.3
|
vfile: 6.0.3
|
||||||
|
|
||||||
|
unist-util-find-after@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/unist': 3.0.3
|
||||||
|
unist-util-is: 6.0.1
|
||||||
|
|
||||||
unist-util-is@6.0.1:
|
unist-util-is@6.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
|
|
|
||||||
13
web/frontend/src/app-providers.tsx
Normal file
13
web/frontend/src/app-providers.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import type { ReactNode } from "react"
|
||||||
|
|
||||||
|
import { useHighlightTheme } from "./hooks/use-highlight-theme"
|
||||||
|
|
||||||
|
interface AppProvidersProps {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppProviders({ children }: AppProvidersProps) {
|
||||||
|
useHighlightTheme()
|
||||||
|
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
import type { ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import ReactMarkdown from "react-markdown"
|
import ReactMarkdown from "react-markdown"
|
||||||
|
import rehypeHighlight from "rehype-highlight"
|
||||||
import rehypeRaw from "rehype-raw"
|
import rehypeRaw from "rehype-raw"
|
||||||
import rehypeSanitize from "rehype-sanitize"
|
import rehypeSanitize from "rehype-sanitize"
|
||||||
import remarkGfm from "remark-gfm"
|
import remarkGfm from "remark-gfm"
|
||||||
|
|
@ -171,10 +172,10 @@ export function DetailSheet({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{detailView === "preview" ? (
|
{detailView === "preview" ? (
|
||||||
<div className="prose prose-zinc dark:prose-invert prose-sm sm:prose-base prose-pre:rounded-xl prose-pre:border prose-pre:border-border/40 prose-pre:bg-zinc-950/90 prose-pre:shadow-sm prose-headings:tracking-tight prose-a:text-primary prose-a:no-underline hover:prose-a:underline max-w-none">
|
<div className="prose prose-zinc dark:prose-invert prose-sm sm:prose-base prose-pre:rounded-xl prose-pre:border prose-pre:border-border/40 prose-pre:bg-zinc-100 prose-pre:p-0 prose-pre:shadow-sm dark:prose-pre:bg-zinc-950/90 prose-headings:tracking-tight prose-a:text-primary prose-a:no-underline hover:prose-a:underline max-w-none">
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm]}
|
||||||
rehypePlugins={[rehypeRaw, rehypeSanitize]}
|
rehypePlugins={[rehypeRaw, rehypeSanitize, rehypeHighlight]}
|
||||||
>
|
>
|
||||||
{selectedSkillDetail.content}
|
{selectedSkillDetail.content}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { IconBrain, IconCheck, IconCopy } from "@tabler/icons-react"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import ReactMarkdown from "react-markdown"
|
import ReactMarkdown from "react-markdown"
|
||||||
|
import rehypeHighlight from "rehype-highlight"
|
||||||
import rehypeRaw from "rehype-raw"
|
import rehypeRaw from "rehype-raw"
|
||||||
import rehypeSanitize from "rehype-sanitize"
|
import rehypeSanitize from "rehype-sanitize"
|
||||||
import remarkGfm from "remark-gfm"
|
import remarkGfm from "remark-gfm"
|
||||||
|
|
@ -63,7 +64,7 @@ export function AssistantMessage({
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"prose dark:prose-invert prose-pre:my-2 prose-pre:overflow-x-auto prose-pre:rounded-lg prose-pre:border prose-pre:bg-zinc-950 prose-pre:p-3 max-w-none [overflow-wrap:anywhere] break-words",
|
"prose dark:prose-invert prose-pre:my-2 prose-pre:overflow-x-auto prose-pre:rounded-lg prose-pre:border prose-pre:bg-zinc-100 prose-pre:p-0 dark:prose-pre:bg-zinc-950 max-w-none [overflow-wrap:anywhere] break-words",
|
||||||
isThought
|
isThought
|
||||||
? "prose-p:my-1.5 p-3 text-[13px] leading-relaxed opacity-90"
|
? "prose-p:my-1.5 p-3 text-[13px] leading-relaxed opacity-90"
|
||||||
: "prose-p:my-2 p-4 text-[15px] leading-relaxed",
|
: "prose-p:my-2 p-4 text-[15px] leading-relaxed",
|
||||||
|
|
@ -71,7 +72,7 @@ export function AssistantMessage({
|
||||||
>
|
>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm]}
|
||||||
rehypePlugins={[rehypeRaw, rehypeSanitize]}
|
rehypePlugins={[rehypeRaw, rehypeSanitize, rehypeHighlight]}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
|
|
|
||||||
70
web/frontend/src/hooks/use-highlight-theme.ts
Normal file
70
web/frontend/src/hooks/use-highlight-theme.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { useEffect } from "react"
|
||||||
|
|
||||||
|
import githubDarkCss from "highlight.js/styles/github-dark.css?inline"
|
||||||
|
import githubLightCss from "highlight.js/styles/github.css?inline"
|
||||||
|
|
||||||
|
const THEME_STYLE_ID = "hljs-theme-style"
|
||||||
|
const THEME_STYLE_OWNER_ATTR = "data-picoclaw-highlight-theme"
|
||||||
|
const THEME_STYLE_OWNER_VALUE = "true"
|
||||||
|
const MANAGED_THEME_STYLE_SELECTOR = `style[${THEME_STYLE_OWNER_ATTR}="${THEME_STYLE_OWNER_VALUE}"]`
|
||||||
|
const ID_THEME_STYLE_SELECTOR = `style#${THEME_STYLE_ID}`
|
||||||
|
|
||||||
|
function getOrCreateThemeStyleElement(): HTMLStyleElement {
|
||||||
|
const managedStyleElement = document.head.querySelector<HTMLStyleElement>(
|
||||||
|
MANAGED_THEME_STYLE_SELECTOR,
|
||||||
|
)
|
||||||
|
if (managedStyleElement) {
|
||||||
|
return managedStyleElement
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingStyleElement =
|
||||||
|
document.querySelector<HTMLStyleElement>(ID_THEME_STYLE_SELECTOR)
|
||||||
|
if (existingStyleElement) {
|
||||||
|
existingStyleElement.setAttribute(
|
||||||
|
THEME_STYLE_OWNER_ATTR,
|
||||||
|
THEME_STYLE_OWNER_VALUE,
|
||||||
|
)
|
||||||
|
return existingStyleElement
|
||||||
|
}
|
||||||
|
|
||||||
|
const conflictingElement = document.getElementById(THEME_STYLE_ID)
|
||||||
|
const styleElement = document.createElement("style")
|
||||||
|
if (!conflictingElement) {
|
||||||
|
styleElement.id = THEME_STYLE_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leave conflicting non-style nodes untouched and track the injected style explicitly.
|
||||||
|
styleElement.setAttribute(THEME_STYLE_OWNER_ATTR, THEME_STYLE_OWNER_VALUE)
|
||||||
|
document.head.appendChild(styleElement)
|
||||||
|
|
||||||
|
return styleElement
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useHighlightTheme() {
|
||||||
|
useEffect(() => {
|
||||||
|
const root = document.documentElement
|
||||||
|
const styleElement = getOrCreateThemeStyleElement()
|
||||||
|
|
||||||
|
const applyTheme = () => {
|
||||||
|
const nextThemeCss = root.classList.contains("dark")
|
||||||
|
? githubDarkCss
|
||||||
|
: githubLightCss
|
||||||
|
styleElement.textContent = nextThemeCss
|
||||||
|
}
|
||||||
|
|
||||||
|
applyTheme()
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
applyTheme()
|
||||||
|
})
|
||||||
|
|
||||||
|
observer.observe(root, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["class"],
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import { RouterProvider, createRouter } from "@tanstack/react-router"
|
||||||
import { StrictMode } from "react"
|
import { StrictMode } from "react"
|
||||||
import ReactDOM from "react-dom/client"
|
import ReactDOM from "react-dom/client"
|
||||||
|
|
||||||
|
import { AppProviders } from "./app-providers"
|
||||||
import "./i18n"
|
import "./i18n"
|
||||||
import "./index.css"
|
import "./index.css"
|
||||||
import { routeTree } from "./routeTree.gen"
|
import { routeTree } from "./routeTree.gen"
|
||||||
|
|
@ -27,9 +28,11 @@ if (!rootElement.innerHTML) {
|
||||||
const root = ReactDOM.createRoot(rootElement)
|
const root = ReactDOM.createRoot(rootElement)
|
||||||
root.render(
|
root.render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<QueryClientProvider client={queryClient}>
|
<AppProviders>
|
||||||
<RouterProvider router={router} />
|
<QueryClientProvider client={queryClient}>
|
||||||
</QueryClientProvider>
|
<RouterProvider router={router} />
|
||||||
|
</QueryClientProvider>
|
||||||
|
</AppProviders>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue