Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9,621 changes: 7,249 additions & 2,372 deletions webui/package-lock.json

Large diffs are not rendered by default.

33 changes: 19 additions & 14 deletions webui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,27 @@
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.0",
"rehype-react": "^7.1.1",
"remark-gemoji": "^7.0.1",
"remark-html": "^15.0.1",
"remark-parse": "^10.0.1",
"remark-react": "^9.0.1",
"remark-rehype": "^10.1.0",
"unified": "^10.1.2"
"rehype-highlight": "^7.0.2",
"rehype-raw": "^7.0.0",
"rehype-react": "^8.0.0",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.1",
"remark-breaks": "^4.0.0",
"remark-gemoji": "^8.0.0",
"remark-gfm": "^4.0.1",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"unified": "^11.0.5"
},
"devDependencies": {
"@babel/runtime": "^7.27.1",
"@graphql-codegen/cli": "^2.16.5",
"@graphql-codegen/fragment-matcher": "^3.3.3",
"@graphql-codegen/introspection": "^2.2.3",
"@graphql-codegen/near-operation-file-preset": "^2.5.0",
"@graphql-codegen/typescript": "^2.8.8",
"@graphql-codegen/typescript-operations": "^2.5.13",
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
"@graphql-codegen/cli": "^5.0.6",
"@graphql-codegen/fragment-matcher": "^5.1.0",
"@graphql-codegen/introspection": "^4.0.3",
"@graphql-codegen/near-operation-file-preset": "^3.0.0",
"@graphql-codegen/typescript": "^4.1.6",
"@graphql-codegen/typescript-operations": "^4.6.1",
"@graphql-codegen/typescript-react-apollo": "^4.3.2",
"@graphql-eslint/eslint-plugin": "^3.20.1",
"@types/jest": "^29.5.14",
"@types/node": "^22.15.18",
Expand All @@ -43,6 +47,7 @@
"eslint-plugin-prettier": "^5.4.0",
"prettier": "^3.5.3",
"react-scripts": "^5.0.1",
"sass": "^1.89.0",
"typescript": "^4.9.5"
},
"scripts": {
Expand Down
21 changes: 12 additions & 9 deletions webui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as React from 'react';
import { Route, Routes } from 'react-router';

import Layout from './components/Header';
Expand All @@ -9,14 +10,16 @@ import NotFoundPage from './pages/notfound/NotFoundPage';

export default function App() {
return (
<Layout>
<Routes>
<Route path="/" element={<ListPage />} />
<Route path="/new" element={<NewBugPage />} />
<Route path="/bug/:id" element={<BugPage />} />
<Route path="/user/:id" element={<IdentityPage />} />
<Route element={<NotFoundPage />} />
</Routes>
</Layout>
<React.StrictMode>
<Layout>
<Routes>
<Route path="/" element={<ListPage />} />
<Route path="/new" element={<NewBugPage />} />
<Route path="/bug/:id" element={<BugPage />} />
<Route path="/user/:id" element={<IdentityPage />} />
<Route element={<NotFoundPage />} />
</Routes>
</Layout>
</React.StrictMode>
);
}
98 changes: 75 additions & 23 deletions webui/src/components/Content/index.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,96 @@
import { createElement, Fragment, useEffect, useState } from 'react';
import type { Root as HastRoot } from 'hast';
import type { Root as MdRoot } from 'mdast';
import { useEffect, useState } from 'react';
import * as React from 'react';
import * as production from 'react/jsx-runtime';
import rehypeHighlight, {
Options as RehypeHighlightOpts,
} from 'rehype-highlight';
import rehypeReact from 'rehype-react';
import gemoji from 'remark-gemoji';
import html from 'remark-html';
import parse from 'remark-parse';
import rehypeSanitize from 'rehype-sanitize';
import remarkBreaks from 'remark-breaks';
import remarkGemoji from 'remark-gemoji';
import remarkGfm from 'remark-gfm';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import type { Options as RemarkRehypeOptions } from 'remark-rehype';
import { unified } from 'unified';
import type { Plugin, Processor } from 'unified';
import { Node as UnistNode } from 'unified/lib';

import { ThemeContext } from '../Themer';

import AnchorTag from './AnchorTag';
import BlockQuoteTag from './BlockQuoteTag';
import ImageTag from './ImageTag';
import PreTag from './PreTag';

type Props = { markdown: string };

// @lygaret 2025/05/16
// type inference for some of this doesn't work, but the pipeline is fine
// this might get better when we upgrade typescript

type RemarkPlugin = Plugin<[], MdRoot, HastRoot>;
type RemarkRehypePlugin = Plugin<RemarkRehypeOptions[], MdRoot, HastRoot>;
type RehypePlugin<Options extends unknown[] = []> = Plugin<
Options,
HastRoot,
HastRoot
>;

const markdownPipeline: Processor<
UnistNode,
undefined,
undefined,
HastRoot,
React.JSX.Element
> = unified()
.use(remarkParse)
.use(remarkGemoji as unknown as RemarkPlugin)
.use(remarkBreaks as unknown as RemarkPlugin)
.use(remarkGfm)
.use(remarkRehype as unknown as RemarkRehypePlugin, {
allowDangerousHtml: true,
})
.use(rehypeSanitize as unknown as RehypePlugin)
.use(rehypeHighlight as unknown as RehypePlugin<RehypeHighlightOpts[]>, {
detect: true,
subset: ['text'],
})
.use(rehypeReact, {
...production,
components: {
a: AnchorTag,
blockquote: BlockQuoteTag,
img: ImageTag,
pre: PreTag,
},
});

const Content: React.FC<Props> = ({ markdown }: Props) => {
const [Content, setContent] = useState(<></>);
const theme = React.useContext(ThemeContext);
const [content, setContent] = useState(<></>);

useEffect(() => {
unified()
.use(parse)
.use(gemoji)
.use(html)
.use(remarkRehype)
.use(rehypeReact, {
createElement,
Fragment,
components: {
img: ImageTag,
pre: PreTag,
a: AnchorTag,
blockquote: BlockQuoteTag,
},
})
markdownPipeline
.process(markdown)
.then((file) => {
setContent(file.result);
.then((file) => setContent(file.result))
.catch((err: any) => {
setContent(
<>
<span className="error">{err}</span>
<pre>{markdown}</pre>
</>
);
});
}, [markdown]);

return Content;
return (
<div className={'highlight-theme'} data-theme={theme.mode}>
{content}
</div>
);
};

export default Content;
4 changes: 3 additions & 1 deletion webui/src/components/Label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@ const createStyle = (color: Color, maxWidth?: string) => ({

type Props = {
label: LabelFragment;
inline?: boolean;
maxWidth?: string;
className?: string;
};
function Label({ label, maxWidth, className }: Props) {
function Label({ label, inline, maxWidth, className }: Props) {
return (
<Chip
size={'small'}
label={label.name}
component={inline ? 'span' : 'div'}
className={className}
style={createStyle(label.color, maxWidth)}
/>
Expand Down
2 changes: 1 addition & 1 deletion webui/src/components/Themer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ declare module '@mui/styles/defaultTheme' {
interface DefaultTheme extends Theme {}
}

const ThemeContext = createContext({
export const ThemeContext = createContext({
toggleMode: () => {},
mode: '',
});
Expand Down
1 change: 1 addition & 0 deletions webui/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import App from './App';
import apolloClient from './apollo';
import Themer from './components/Themer';
import { defaultLightTheme, defaultDarkTheme } from './themes/index';
import './themes/highlight-theme.scss';

const root = createRoot(document.getElementById('root') as HTMLElement);
root.render(
Expand Down
4 changes: 2 additions & 2 deletions webui/src/pages/bug/LabelChange.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ function LabelChange({ op }: Props) {
<Author author={op.author} className={classes.author} />
{added.length > 0 && <span> added the </span>}
{added.map((label, index) => (
<Label key={index} label={label} className={classes.label} />
<Label inline key={index} label={label} className={classes.label} />
))}
{added.length > 0 && removed.length > 0 && <span> and</span>}
{removed.length > 0 && <span> removed the </span>}
{removed.map((label, index) => (
<Label key={index} label={label} className={classes.label} />
<Label inline key={index} label={label} className={classes.label} />
))}
<span>
{' '}
Expand Down
13 changes: 13 additions & 0 deletions webui/src/themes/highlight-theme.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@use "sass:meta";

// if highlight.js gets updated to support proper mixins,
// meta.load-css won't be necessary; see https://sass-lang.com/documentation/breaking-changes/import/#nested-imports

.highlight-theme[data-theme="light"],
.highlight-theme:not([data-theme="dark"]) {
@include meta.load-css("~highlight.js/scss/base16/classic-light.scss");
}

.highlight-theme[data-theme="dark"] {
@include meta.load-css("~highlight.js/scss/base16/classic-dark.scss");
}