├── src
├── App
│ ├── utils
│ │ ├── index.js
│ │ ├── url-builder.js
│ │ └── __test__
│ │ │ └── url-builder.test.js
│ ├── libs
│ │ ├── provider
│ │ │ ├── index.js
│ │ │ ├── namespaces.js
│ │ │ ├── url-params.js
│ │ │ ├── reducer.js
│ │ │ └── provider.js
│ │ ├── router
│ │ │ ├── index.js
│ │ │ ├── link.js
│ │ │ ├── __test__
│ │ │ │ └── link.test.js
│ │ │ ├── error-boundary.js
│ │ │ ├── error-page.js
│ │ │ ├── crash-page.js
│ │ │ └── router.js
│ │ ├── layout
│ │ │ ├── header-logo.js
│ │ │ ├── header-links
│ │ │ │ ├── index.js
│ │ │ │ └── link-theme.js
│ │ │ ├── index.js
│ │ │ └── index.module.css
│ │ └── fetcher
│ │ │ └── index.js
│ ├── hooks
│ │ ├── index.js
│ │ ├── use-mobile.js
│ │ └── use-custom-compare-memoize.js
│ ├── components
│ │ ├── icon
│ │ │ ├── index.module.css
│ │ │ └── index.js
│ │ ├── containers
│ │ │ ├── container.js
│ │ │ ├── content
│ │ │ │ ├── index.js
│ │ │ │ └── index.module.css
│ │ │ ├── message.js
│ │ │ ├── base-alert.js
│ │ │ └── loading-result.js
│ │ └── index.js
│ ├── styles
│ │ ├── icons
│ │ │ ├── index.js
│ │ │ ├── fa-icons.js
│ │ │ └── custom-icons.js
│ │ ├── theme
│ │ │ ├── default
│ │ │ │ ├── button.js
│ │ │ │ ├── list.js
│ │ │ │ ├── table.js
│ │ │ │ ├── index.js
│ │ │ │ └── badge.js
│ │ │ ├── index.js
│ │ │ ├── color-scheme.js
│ │ │ ├── variant.js
│ │ │ ├── provider.js
│ │ │ ├── common.js
│ │ │ ├── resolver.js
│ │ │ └── colors
│ │ │ │ └── index.js
│ │ ├── reset.css
│ │ └── common.js
│ ├── assets
│ │ ├── index.js
│ │ └── img
│ │ │ ├── vespa-logo-black.svg
│ │ │ └── vespa-logo-heather.svg
│ ├── pages
│ │ ├── search
│ │ │ ├── search-container
│ │ │ │ ├── index.js
│ │ │ │ └── index.module.css
│ │ │ ├── typography
│ │ │ │ ├── index.js
│ │ │ │ └── index.module.css
│ │ │ ├── link-reference.js
│ │ │ ├── abstract-container
│ │ │ │ ├── abstract
│ │ │ │ │ ├── use-consent.js
│ │ │ │ │ ├── abstract-content.js
│ │ │ │ │ ├── abstract-questions
│ │ │ │ │ │ ├── index.module.css
│ │ │ │ │ │ └── index.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── abstract-feedback.js
│ │ │ │ │ ├── abstract-about.js
│ │ │ │ │ ├── abstract-title.js
│ │ │ │ │ └── abstract-disclaimer.js
│ │ │ │ ├── index.js
│ │ │ │ └── index.module.css
│ │ │ ├── results-container
│ │ │ │ ├── index.module.css
│ │ │ │ ├── results
│ │ │ │ │ ├── index.module.css
│ │ │ │ │ └── index.js
│ │ │ │ └── index.js
│ │ │ ├── search-input
│ │ │ │ ├── index.module.css
│ │ │ │ └── index.js
│ │ │ ├── search-classic.js
│ │ │ ├── index.js
│ │ │ ├── search-sources.js
│ │ │ └── md-parser.js
│ │ ├── home
│ │ │ └── index.js
│ │ ├── md
│ │ │ └── index.js
│ │ └── testcomp
│ │ │ └── index.js
│ └── index.js
└── main.js
├── .prettierrc.cjs
├── .env
├── vespa-search.png
├── public
└── favicon.ico
├── renovate.json
├── jsconfig.json
├── .gitignore
├── postcss.config.js
├── index.html
├── README.md
├── vite.config.js
├── .eslintrc.cjs
├── package.json
├── .github
└── workflows
│ └── deploy.yaml
└── LICENSE
/src/App/utils/index.js:
--------------------------------------------------------------------------------
1 | export * from './url-builder';
2 |
--------------------------------------------------------------------------------
/.prettierrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | singleQuote: true,
3 | };
4 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | VITE_SITE_TITLE=Vespa Search
2 | VITE_ENDPOINT=https://api.search.vespa.ai
3 |
--------------------------------------------------------------------------------
/vespa-search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vespa-engine/vespa-search/HEAD/vespa-search.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vespa-engine/vespa-search/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/App/libs/provider/index.js:
--------------------------------------------------------------------------------
1 | export * from './namespaces';
2 | export * from './provider';
3 |
--------------------------------------------------------------------------------
/src/App/hooks/index.js:
--------------------------------------------------------------------------------
1 | export * from './use-custom-compare-memoize';
2 | export { useMobile } from 'App/hooks/use-mobile.js';
3 |
--------------------------------------------------------------------------------
/src/App/components/icon/index.module.css:
--------------------------------------------------------------------------------
1 | .box {
2 | &[data-disabled='true'] {
3 | pointer-events: none;
4 | opacity: var(--common-opacity);
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/App/styles/icons/index.js:
--------------------------------------------------------------------------------
1 | import customIcons from './custom-icons';
2 | import * as faIcons from './fa-icons';
3 |
4 | export const icons = [...Object.values(faIcons), ...customIcons];
5 |
--------------------------------------------------------------------------------
/src/App/assets/index.js:
--------------------------------------------------------------------------------
1 | export { default as VespaLogoBlack } from 'App/assets/img/vespa-logo-black.svg';
2 | export { default as VespaLogoHeather } from 'App/assets/img/vespa-logo-heather.svg';
3 |
--------------------------------------------------------------------------------
/src/App/styles/theme/default/button.js:
--------------------------------------------------------------------------------
1 | import { Button as MantineButton } from '@mantine/core';
2 |
3 | export const Button = MantineButton.extend({
4 | defaultProps: { variant: 'filled' },
5 | });
6 |
--------------------------------------------------------------------------------
/src/App/styles/theme/default/list.js:
--------------------------------------------------------------------------------
1 | import { List as MantineList } from '@mantine/core';
2 |
3 | export const List = MantineList.extend({
4 | styles: {
5 | itemWrapper: { display: 'inline' },
6 | },
7 | });
8 |
--------------------------------------------------------------------------------
/src/App/styles/theme/default/table.js:
--------------------------------------------------------------------------------
1 | import { Table as MantineTable } from '@mantine/core';
2 |
3 | export const Table = MantineTable.extend({
4 | defaultProps: { verticalSpacing: 'sm' },
5 | styles: {},
6 | });
7 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "local>vespa-engine/renovate-config"
5 | ],
6 | "prHourlyLimit": 20,
7 | "prConcurrentLimit": 20
8 | }
9 |
--------------------------------------------------------------------------------
/src/App/libs/router/index.js:
--------------------------------------------------------------------------------
1 | export { Router, Redirect } from './router';
2 | export { Link } from './link';
3 | export { ErrorPage } from './error-page';
4 | export { ErrorBoundary } from './error-boundary';
5 | export { CrashPage } from './crash-page.js';
6 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import App from 'App';
4 |
5 | ReactDOM.createRoot(document.getElementById('root')).render(
6 |
7 |
8 | ,
9 | );
10 |
--------------------------------------------------------------------------------
/src/App/styles/theme/default/index.js:
--------------------------------------------------------------------------------
1 | export { Badge } from 'App/styles/theme/default/badge';
2 | export { Button } from 'App/styles/theme/default/button';
3 | export { List } from 'App/styles/theme/default/list';
4 | export { Table } from 'App/styles/theme/default/table';
5 |
--------------------------------------------------------------------------------
/src/App/styles/theme/index.js:
--------------------------------------------------------------------------------
1 | export { common } from 'App/styles/theme/common.js';
2 | export { resolver } from 'App/styles/theme/resolver.js';
3 | export { ThemeProvider } from 'App/styles/theme/provider.js';
4 | export { ColorScheme } from 'App/styles/theme/color-scheme.js';
5 |
--------------------------------------------------------------------------------
/src/App/hooks/use-mobile.js:
--------------------------------------------------------------------------------
1 | import { useMediaQuery } from '@mantine/hooks';
2 | import { breakpoints } from 'App/styles/common.js';
3 |
4 | export function useMobile() {
5 | return useMediaQuery(`(max-width: ${breakpoints.sm})`, null, {
6 | getInitialValueInEffect: false,
7 | });
8 | }
9 |
--------------------------------------------------------------------------------
/src/App/styles/theme/color-scheme.js:
--------------------------------------------------------------------------------
1 | import { useMantineColorScheme } from '@mantine/core';
2 | import { useHotkeys } from '@mantine/hooks';
3 |
4 | export function ColorScheme() {
5 | const { toggleColorScheme } = useMantineColorScheme();
6 | useHotkeys([['mod+J', toggleColorScheme]]);
7 | }
8 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "forceConsistentCasingInFileNames": true,
5 | "noFallthroughCasesInSwitch": true,
6 | "resolveJsonModule": true,
7 | "isolatedModules": true,
8 | "noEmit": true,
9 | "jsx": "react-jsx",
10 | "baseUrl": "src"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/App/pages/search/search-container/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Container } from 'App/components/index.js';
3 | import classNames from 'App/pages/search/search-container/index.module.css';
4 |
5 | export function SearchContainer(props) {
6 | return ;
7 | }
8 |
--------------------------------------------------------------------------------
/src/App/pages/search/typography/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Stack } from '@mantine/core';
3 | import Classnames from 'App/pages/search/typography/index.module.css';
4 |
5 | export function Typography(props) {
6 | const { typography } = Classnames;
7 | return ;
8 | }
9 |
--------------------------------------------------------------------------------
/src/App/pages/search/search-container/index.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | gap: var(--mantine-spacing-md);
3 |
4 | grid-template-columns:
5 | minmax(0, var(--search-result-width))
6 | minmax(0, var(--search-abstract-width));
7 |
8 | @media (max-width: $mantine-breakpoint-sm) {
9 | grid-template-columns: revert;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/App/components/containers/container.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box } from '@mantine/core';
3 | import { mergeStyles } from 'App/styles/common';
4 |
5 | export function Container({ style, ...props }) {
6 | return (
7 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 | build
15 |
16 | # Editor directories and files
17 | .vscode/*
18 | !.vscode/extensions.json
19 | .idea
20 | .DS_Store
21 | *.suo
22 | *.ntvs*
23 | *.njsproj
24 | *.sln
25 | *.sw?
26 |
--------------------------------------------------------------------------------
/src/App/pages/search/link-reference.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useSearchContext } from 'App/libs/provider';
3 | import { Link } from 'App/libs/router';
4 |
5 | export function LinkReference({ token }) {
6 | const selectHit = useSearchContext((ctx) => ctx.selectHit);
7 | return (
8 | selectHit(parseInt(token.text))}>{token.text}
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/App/pages/search/abstract-container/abstract/use-consent.js:
--------------------------------------------------------------------------------
1 | import { useLocalStorage } from '@mantine/hooks';
2 |
3 | export function useConsent() {
4 | const [value, setValue] = useLocalStorage({
5 | key: 'consent',
6 | getInitialValueInEffect: false,
7 | });
8 | return {
9 | value: value === 'true',
10 | setValue: (value) => setValue(value.toString()),
11 | };
12 | }
13 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | 'postcss-preset-mantine': {},
4 | 'postcss-simple-vars': {
5 | variables: {
6 | 'mantine-breakpoint-xs': '36em',
7 | 'mantine-breakpoint-sm': '48em',
8 | 'mantine-breakpoint-md': '62em',
9 | 'mantine-breakpoint-lg': '75em',
10 | 'mantine-breakpoint-xl': '88em',
11 | },
12 | },
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/src/App/components/index.js:
--------------------------------------------------------------------------------
1 | export { Container } from 'App/components/containers/container.js';
2 | export { Content } from 'App/components/containers/content';
3 | export { Error } from 'App/components/containers/base-alert.js';
4 | export { Icon } from 'App/components/icon';
5 | export { LoadingResult } from 'App/components/containers/loading-result.js';
6 | export { Message } from 'App/components/containers/message.js';
7 |
--------------------------------------------------------------------------------
/src/App/pages/search/abstract-container/abstract/abstract-content.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Stack } from '@mantine/core';
3 | import { useSearchContext } from 'App/libs/provider/index.js';
4 | import { parseMarkdown } from 'App/pages/search/md-parser.js';
5 |
6 | export function AbstractContent() {
7 | const summary = useSearchContext((ctx) => ctx.summary.raw);
8 | return {parseMarkdown(summary)} ;
9 | }
10 |
--------------------------------------------------------------------------------
/src/App/pages/search/abstract-container/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ScrollArea, Stack } from '@mantine/core';
3 | import classNames from 'App/pages/search/abstract-container/index.module.css';
4 |
5 | export function AbstractContainer(props) {
6 | return (
7 |
8 |
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vespa Search
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/App/components/containers/content/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box, Stack } from '@mantine/core';
3 | import classNames from 'App/components/containers/content/index.module.css';
4 |
5 | export function Content({ withBorder, selected, ...props }) {
6 | const { box } = classNames;
7 | return (
8 |
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/App/components/containers/message.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box } from '@mantine/core';
3 | import { mergeStyles } from 'App/styles/common';
4 |
5 | export function Message({ style, ...props }) {
6 | return (
7 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/App/components/containers/base-alert.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Alert } from '@mantine/core';
3 | import { Icon, Container } from '..';
4 |
5 | function BaseAlert({ message, icon, color, ...props }) {
6 | return (
7 |
8 | } color={color} {...props}>
9 | {message}
10 |
11 |
12 | );
13 | }
14 |
15 | export const Error = (props) => ;
16 |
--------------------------------------------------------------------------------
/src/App/pages/search/abstract-container/abstract/abstract-questions/index.module.css:
--------------------------------------------------------------------------------
1 | .item {
2 | border: 1px solid var(--subtle-border-and-separator-blue);
3 | background-color: var(--app-background);
4 | border-radius: var(--mantine-radius-xl);
5 | padding: var(--mantine-spacing-sm);
6 | margin-bottom: var(--mantine-spacing-xs);
7 |
8 | @mixin hover {
9 | border-color: var(--hovered-ui-element-border-blue);
10 | }
11 | }
12 |
13 | .link {
14 | &:hover {
15 | text-decoration: none;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/App/components/containers/content/index.module.css:
--------------------------------------------------------------------------------
1 | .box {
2 | position: relative;
3 |
4 | &[data-with-border='true'] {
5 | border: 1px solid var(--subtle-border-and-separator);
6 | border-radius: var(--mantine-radius-xs);
7 | }
8 |
9 | &[data-selected='true'] {
10 | border-color: var(--ui-element-border-and-focus-blue);
11 |
12 | @mixin hover {
13 | border-color: var(--hovered-ui-element-border-blue);
14 | }
15 | }
16 |
17 | @mixin hover {
18 | border-color: var(--hovered-ui-element-border);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/App/pages/search/results-container/index.module.css:
--------------------------------------------------------------------------------
1 | .scrollArea {
2 | height: var(--search-scrollarea-height);
3 |
4 | @media (max-width: $mantine-breakpoint-sm) {
5 | height: calc(
6 | 67vh - var(--app-shell-header-height, 0px) -
7 | (2 * var(--mantine-spacing-md))
8 | );
9 | }
10 | }
11 |
12 | .stack {
13 | max-width: calc(var(--search-result-width) - (4 * var(--mantine-spacing-md)));
14 |
15 | @media (max-width: $mantine-breakpoint-sm) {
16 | max-width: calc(100vw - (2 * var(--mantine-spacing-md)));
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/App/hooks/use-custom-compare-memoize.js:
--------------------------------------------------------------------------------
1 | import { isEqual } from 'lodash';
2 | import { useCallback, useRef } from 'react';
3 |
4 | export function useCustomCompareMemoize(deps, depsEqual = isEqual) {
5 | const ref = useRef();
6 | if (!ref.current || !depsEqual(ref.current, deps)) ref.current = deps;
7 | return ref.current;
8 | }
9 |
10 | export function useCustomCompareCallback(callback, deps, depsEqual) {
11 | // eslint-disable-next-line react-hooks/exhaustive-deps
12 | return useCallback(callback, useCustomCompareMemoize(deps, depsEqual));
13 | }
14 |
--------------------------------------------------------------------------------
/src/App/libs/layout/header-logo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Image, useComputedColorScheme } from '@mantine/core';
3 | import { Link } from 'react-router-dom';
4 | import { VespaLogoBlack, VespaLogoHeather } from 'App/assets';
5 |
6 | export function HeaderLogo() {
7 | const computedColorScheme = useComputedColorScheme('light');
8 | const logo =
9 | computedColorScheme === 'dark' ? VespaLogoHeather : VespaLogoBlack;
10 | return (
11 |
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/src/App/components/containers/loading-result.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Skeleton, Stack } from '@mantine/core';
3 | import { Content } from 'App/components';
4 |
5 | export function LoadingResult() {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/App/pages/search/results-container/results/index.module.css:
--------------------------------------------------------------------------------
1 | .titleResult {
2 | color: var(--high-contrast-text);
3 | }
4 |
5 | .titleResult:hover .iconExternal {
6 | visibility: revert;
7 | }
8 |
9 | .iconExternal {
10 | visibility: hidden;
11 | }
12 |
13 | .spoilerControl {
14 | font-size: var(--mantine-font-size-sm);
15 | color: var(--high-contrast-text);
16 | background-color: var(--ui-element-background);
17 | text-align: center;
18 | width: 100%;
19 |
20 | @mixin hover {
21 | text-decoration: none;
22 | background-color: var(--hovered-ui-element-background);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/App/styles/theme/default/badge.js:
--------------------------------------------------------------------------------
1 | import { Badge as MantineBadge } from '@mantine/core';
2 |
3 | export const Badge = MantineBadge.extend({
4 | vars: (theme, { variant, color }) => {
5 | const _color = color || theme.primaryColor;
6 | if (variant === 'light') {
7 | return {
8 | root: {
9 | color: `var(--low-contrast-text-${_color})`,
10 | background: `var(--ui-element-background-${_color})`,
11 | hover: `var(--hovered-ui-element-background-${_color})`,
12 | },
13 | };
14 | }
15 | },
16 | defaultProps: {},
17 | styles: {},
18 | });
19 |
--------------------------------------------------------------------------------
/src/App/pages/search/search-input/index.module.css:
--------------------------------------------------------------------------------
1 | .input {
2 | overflow-y: hidden;
3 |
4 | &[data-expanded='true'][data-suggestions='true'] {
5 | border-radius: var(--mantine-radius-lg) var(--mantine-radius-lg) 0 0;
6 |
7 | &:focus,
8 | &:focus-within {
9 | border-bottom-color: transparent;
10 | }
11 | }
12 | }
13 |
14 | .dropdown {
15 | border-bottom-left-radius: var(--mantine-radius-lg);
16 | border-bottom-right-radius: var(--mantine-radius-lg);
17 | border-color: var(--solid-background-green);
18 | border-top: none;
19 | overflow: hidden;
20 | margin-top: -10px;
21 | }
22 |
--------------------------------------------------------------------------------
/src/App/styles/theme/variant.js:
--------------------------------------------------------------------------------
1 | import { defaultVariantColorsResolver, parseThemeColor } from '@mantine/core';
2 |
3 | export const variantColorResolver = (input) => {
4 | const defaultResolvedColors = defaultVariantColorsResolver(input);
5 | const parsedColor = parseThemeColor({
6 | color: input.color || input.theme.primaryColor,
7 | theme: input.theme,
8 | });
9 |
10 | if (parsedColor.isThemeColor && input.variant === 'filled') {
11 | return {
12 | ...defaultResolvedColors,
13 | color: 'var(--vespa-color-rock)',
14 | hoverColor: 'var(--vespa-color-rock)',
15 | };
16 | }
17 |
18 | return defaultResolvedColors;
19 | };
20 |
--------------------------------------------------------------------------------
/src/App/styles/icons/fa-icons.js:
--------------------------------------------------------------------------------
1 | export {
2 | faDocker,
3 | faGithub,
4 | faLinux,
5 | faPython,
6 | faSlack,
7 | } from '@fortawesome/free-brands-svg-icons';
8 | export {
9 | faAdd,
10 | faBug,
11 | faCloud,
12 | faSun,
13 | faMoon,
14 | faArrowRight,
15 | faMagnifyingGlass,
16 | faBook,
17 | faBlog,
18 | faThumbsUp,
19 | faThumbsDown,
20 | faVial,
21 | faExpand,
22 | faExternalLink,
23 | faExternalLinkSquare,
24 | faCode,
25 | faCheck,
26 | } from '@fortawesome/free-solid-svg-icons';
27 | export {
28 | faClipboard,
29 | faClock as faClockRegular,
30 | faFileLines,
31 | faCircleQuestion,
32 | } from '@fortawesome/free-regular-svg-icons';
33 |
--------------------------------------------------------------------------------
/src/App/pages/search/results-container/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ScrollArea, Stack } from '@mantine/core';
3 | import classNames from 'App/pages/search/results-container/index.module.css';
4 | import { useMobile } from 'App/hooks';
5 |
6 | export function ResultsContainer({ viewportRef, ...props }) {
7 | const isMobile = useMobile();
8 |
9 | return (
10 |
11 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/App/libs/provider/namespaces.js:
--------------------------------------------------------------------------------
1 | export const ALL_NAMESPACES = Object.freeze([
2 | { id: 'all', name: 'All', icon: 'check' },
3 | { id: 'open-p', name: 'Documentation', icon: 'book' },
4 | { id: 'cloud-p', name: 'Cloud Documentation', icon: 'cloud' },
5 | { id: 'vespaapps-p', name: 'Sample Apps', icon: 'vial' },
6 | { id: 'blog-p', name: 'Blog', icon: 'blog' },
7 | { id: 'pyvespa-p', name: 'PyVespa', icon: 'fab-python' },
8 | { id: 'code-p', name: 'Sample Schemas', icon: 'code' },
9 | ]);
10 |
11 | export const NAMESPACES_BY_ID = Object.freeze(
12 | ALL_NAMESPACES.reduce(
13 | (object, namespace) => ({ ...object, [namespace.id]: namespace }),
14 | {},
15 | ),
16 | );
17 |
--------------------------------------------------------------------------------
/src/App/libs/router/link.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link as RouterLink } from 'react-router-dom';
3 |
4 | export const isInternalLink = (link) => {
5 | if (!link) return false;
6 | return !/^[a-z]+:\/\//.test(link);
7 | };
8 |
9 | export function Link({ to, api = false, ...props }) {
10 | const internal = !api && isInternalLink(to);
11 | if (!props.download && to && internal)
12 | return ;
13 |
14 | const fixedProps = Object.assign(
15 | to ? { href: (api ? window.config.api : '') + to } : {},
16 | to && !internal && { target: '_blank', rel: 'noopener noreferrer' },
17 | props,
18 | );
19 | return ;
20 | }
21 |
--------------------------------------------------------------------------------
/src/App/pages/search/abstract-container/index.module.css:
--------------------------------------------------------------------------------
1 | .scrollArea {
2 | height: var(--search-scrollarea-height);
3 |
4 | @media (max-width: $mantine-breakpoint-sm) {
5 | height: calc(33vh - (1 * var(--mantine-spacing-md)));
6 | background-color: var(--ui-element-background-green);
7 | padding-top: var(--mantine-spacing-sm);
8 | padding-left: var(--mantine-spacing-sm);
9 | padding-right: var(--mantine-spacing-sm);
10 | }
11 | }
12 |
13 | .stack {
14 | max-width: calc(
15 | var(--search-abstract-width) - (1 * var(--mantine-spacing-md))
16 | );
17 |
18 | @media (max-width: $mantine-breakpoint-sm) {
19 | max-width: calc(100vw - (2 * var(--mantine-spacing-md)));
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/App/libs/router/__test__/link.test.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest';
2 | import { isInternalLink } from '../link';
3 |
4 | test('external links', () => {
5 | expect(isInternalLink('http://www.vg.no')).toBeFalsy();
6 | expect(isInternalLink('gopher://gopher.floodgap.com/1/world')).toBeFalsy();
7 | expect(
8 | isInternalLink('slack://channel?team=T025DU6HX&id=C6KT1FC9L'),
9 | ).toBeFalsy();
10 | });
11 |
12 | test('invalid links', () => {
13 | expect(isInternalLink()).toBeFalsy();
14 | expect(isInternalLink(null)).toBeFalsy();
15 | expect(isInternalLink('')).toBeFalsy();
16 | });
17 |
18 | test('internal links', () => {
19 | expect(isInternalLink('/search')).toBeTruthy();
20 | expect(isInternalLink('/')).toBeTruthy();
21 | });
22 |
--------------------------------------------------------------------------------
/src/App/styles/reset.css:
--------------------------------------------------------------------------------
1 | #root {
2 | height: 100%;
3 | isolation: isolate;
4 | }
5 |
6 | html {
7 | height: 100%;
8 | }
9 |
10 | * {
11 | margin: 0;
12 | }
13 |
14 | *,
15 | *::before,
16 | *::after {
17 | box-sizing: border-box;
18 | }
19 |
20 | body {
21 | -webkit-font-smoothing: antialiased;
22 | height: 100%;
23 | }
24 |
25 | img,
26 | picture,
27 | video,
28 | canvas,
29 | svg {
30 | display: block;
31 | }
32 |
33 | input,
34 | button,
35 | textarea,
36 | select {
37 | font: inherit;
38 | }
39 |
40 | p,
41 | h1,
42 | h2,
43 | h3,
44 | h4,
45 | h5,
46 | h6 {
47 | overflow-wrap: break-word;
48 | }
49 |
50 | a {
51 | color: var(--vespa-color-anchor);
52 | cursor: pointer;
53 | text-decoration: none;
54 | &:hover {
55 | text-decoration: underline;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/App/pages/search/typography/index.module.css:
--------------------------------------------------------------------------------
1 | .typography {
2 | color: var(--low-contrast-text);
3 | font-size: var(--mantine-font-size-sm);
4 |
5 | & ul,
6 | & ol {
7 | & li {
8 | font-size: var(--mantine-font-size-sm);
9 | }
10 | }
11 |
12 | & blockquote {
13 | font-size: var(--mantine-font-size-sm);
14 | border-top-right-radius: var(--mantine-radius-sm);
15 | border-bottom-right-radius: var(--mantine-radius-sm);
16 | color: var(--low-contrast-text);
17 | border-left: rem(5) solid var(--mantine-color-default-border);
18 |
19 | & cite {
20 | display: block;
21 | font-size: var(--mantine-font-size-sm);
22 | margin-top: var(--mantine-spacing-xs);
23 | overflow: hidden;
24 | text-overflow: ellipsis;
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/App/pages/home/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Container, Group, Space } from '@mantine/core';
3 | import { SearchInput } from 'App/pages/search/search-input/index.js';
4 | import { Icon } from 'App/components/index.js';
5 | import { Link } from 'App/libs/router/index.js';
6 |
7 | export function Home() {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | Browse documentation
21 |
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/App/pages/search/search-classic.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Anchor, Group } from '@mantine/core';
3 | import { useSearchContext } from 'App/libs/provider/index.js';
4 | import { Icon } from 'App/components/index.js';
5 | import { useMobile } from 'App/hooks';
6 |
7 | export function SearchClassic() {
8 | const query = useSearchContext((ctx) => ctx.query);
9 | const isMobile = useMobile();
10 |
11 | return (
12 |
13 |
20 |
21 |
22 | classic search
23 |
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/src/App/libs/layout/header-links/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Group } from '@mantine/core';
3 | import { LinkTheme } from 'App/libs/layout/header-links/link-theme';
4 | import { Icon } from 'App/components';
5 | import { Link } from 'App/libs/router';
6 |
7 | export function HeaderLinks() {
8 | return (
9 |
10 |
11 |
17 |
18 |
19 |
25 |
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/App/libs/layout/header-links/link-theme.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | ActionIcon,
4 | Tooltip,
5 | useComputedColorScheme,
6 | useMantineColorScheme,
7 | } from '@mantine/core';
8 | import { Icon } from 'App/components';
9 |
10 | export function LinkTheme() {
11 | const { toggleColorScheme } = useMantineColorScheme();
12 | const computedColorScheme = useComputedColorScheme('light');
13 | const isDarkMode = computedColorScheme === 'dark';
14 | const color = isDarkMode ? 'yellow' : 'var(--header-links)';
15 | const iconName = isDarkMode ? 'sun' : 'moon';
16 |
17 | return (
18 |
19 | toggleColorScheme()}
21 | variant="transparent"
22 | color={color}
23 | >
24 |
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | 
4 |
5 | # Vespa Search
6 |
7 | This is a Vespa Application for searching and exploring documentation, blogs,
8 | sample applications and other resources useful when working on Vespa.
9 |
10 |
11 |
12 |
13 |
14 | Install and start:
15 |
16 | yarn install
17 | yarn dev
18 |
19 | Alternatively, use Docker to start it without installing node:
20 |
21 | docker run -v `pwd`:/w -w /w --publish 3000:3000 node sh -c 'yarn install && yarn dev --host'
22 |
23 | When started, open [http://127.0.0.1:3000/](http://127.0.0.1:3000/).
24 |
25 | ## License
26 |
27 | Code licensed under the Apache 2.0 license. See [LICENSE](LICENSE) for terms.
28 |
--------------------------------------------------------------------------------
/src/App/styles/common.js:
--------------------------------------------------------------------------------
1 | import { em, rem } from '@mantine/core';
2 |
3 | // maximum width for a content container
4 | export const maxWidth = 1920;
5 |
6 | // default border radius for all elements
7 | export const borderRadius = rem(2);
8 |
9 | // default opacity for disabled elements
10 | export const opacity = 0.34;
11 |
12 | // Default font weights for all text elements
13 | export const fontWeightLight = 300;
14 | export const fontWeightRegular = 400;
15 | export const fontWeightBold = 600;
16 |
17 | export const breakpoints = {
18 | xs: em(576),
19 | sm: em(768),
20 | md: em(992),
21 | lg: em(1200),
22 | xl: em(1400),
23 | };
24 |
25 | export const mergeStyles = (a = {}, b = {}) => {
26 | if (typeof a !== 'function' && typeof b !== 'function') return { ...a, ...b };
27 | return (theme) => ({
28 | ...(typeof a === 'function' ? a(theme) : a),
29 | ...(typeof b === 'function' ? b(theme) : b),
30 | });
31 | };
32 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import fs from 'fs/promises';
3 | import { defineConfig } from 'vite';
4 | import react from '@vitejs/plugin-react';
5 |
6 | // https://vitejs.dev/config/
7 | export default defineConfig({
8 | build: {
9 | sourcemap: 'hidden',
10 | },
11 | esbuild: {
12 | loader: 'jsx',
13 | include: /src\/.*\.jsx?$/,
14 | exclude: [],
15 | },
16 | optimizeDeps: {
17 | esbuildOptions: {
18 | plugins: [
19 | {
20 | name: 'load-js-files-as-jsx',
21 | setup(build) {
22 | build.onLoad({ filter: /src\/.*\.js$/ }, async (args) => ({
23 | loader: 'jsx',
24 | contents: await fs.readFile(args.path, 'utf8'),
25 | }));
26 | },
27 | },
28 | ],
29 | },
30 | },
31 | plugins: [react()],
32 | resolve: {
33 | alias: {
34 | App: path.resolve(__dirname, 'src/App'),
35 | },
36 | },
37 | });
38 |
--------------------------------------------------------------------------------
/src/App/styles/theme/provider.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { library } from '@fortawesome/fontawesome-svg-core';
3 | import { ColorSchemeScript, createTheme, MantineProvider } from '@mantine/core';
4 | import { common, resolver } from 'App/styles/theme';
5 | import { icons } from 'App/styles/icons';
6 | import * as components from 'App/styles/theme/default';
7 | import { variantColorResolver } from 'App/styles/theme/variant';
8 |
9 | const theme = createTheme({
10 | ...common,
11 | components,
12 | variantColorResolver,
13 | });
14 |
15 | export function ThemeProvider({ children }) {
16 | icons.forEach((icon) => library.add(icon));
17 | return (
18 | <>
19 |
20 |
25 | {children}
26 |
27 | >
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/App/libs/router/error-boundary.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export class ErrorBoundary extends React.Component {
4 | constructor(props) {
5 | super(props);
6 | this.state = {};
7 | this.crashPage = props.crashPage;
8 | if (typeof this.crashPage !== 'function')
9 | throw new Error("Required prop 'crashPage' is not a valid React element");
10 | }
11 |
12 | componentDidCatch(exception, errorInfo) {
13 | const error = Object.getOwnPropertyNames(exception).reduce(
14 | (acc, key) => {
15 | acc[key] = exception[key];
16 | return acc;
17 | },
18 | { ...errorInfo },
19 | );
20 | const meta = {
21 | location: window?.location?.href,
22 | time: new Date().toISOString(),
23 | error,
24 | };
25 | this.setState({ error: meta });
26 | }
27 |
28 | render() {
29 | if (this.state.error) return this.crashPage({ error: this.state.error });
30 | return this.props.children;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/App/styles/theme/common.js:
--------------------------------------------------------------------------------
1 | import { rem } from '@mantine/core';
2 |
3 | export const common = {
4 | defaultRadius: 'xs',
5 | cursorType: 'pointer',
6 | fontFamily: 'Inter, sans-serif',
7 | primaryColor: 'green',
8 | spacing: {
9 | xs: rem(5),
10 | sm: rem(8),
11 | md: rem(13),
12 | lg: rem(21),
13 | xl: rem(34),
14 | },
15 | lineHeights: {
16 | xs: '1.62',
17 | sm: '1.62',
18 | md: '1.62',
19 | lg: '1.62',
20 | xl: '1.62',
21 | },
22 | headings: {
23 | fontFamily: 'Inter, sans-serif',
24 | sizes: {
25 | h1: { fontSize: '1.3333rem', lineHeight: 1, fontWeight: 600 },
26 | h2: { fontSize: '1.1875rem', lineHeight: 1, fontWeight: 400 },
27 | h3: { fontSize: '1.1042rem', lineHeight: 1, fontWeight: 400 },
28 | h4: { fontSize: '1.0417rem', lineHeight: 1, fontWeight: 600 },
29 | h5: { fontSize: '1rem', lineHeight: 1, fontWeight: 600 },
30 | h6: { fontSize: '0.9375rem', lineHeight: 1, fontWeight: 600 },
31 | },
32 | },
33 | };
34 |
--------------------------------------------------------------------------------
/src/App/libs/router/error-page.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useLocation } from 'react-router-dom';
3 | import { Text, Title } from '@mantine/core';
4 | import { Message } from 'App/components';
5 |
6 | const getMessage = (code, location) => {
7 | const numberCode =
8 | parseInt(code || new URLSearchParams(location?.search).get('code')) || 404;
9 |
10 | switch (numberCode) {
11 | case 403:
12 | return 'Sorry, you are not authorized to view this page.';
13 | case 404:
14 | return 'Sorry, the page you were looking for does not exist.';
15 | case 500:
16 | return 'Oops... something went wrong.';
17 | default:
18 | return `Unknown error (${code}) - really, I have no idea what is going on here.`;
19 | }
20 | };
21 |
22 | export function ErrorPage({ code }) {
23 | const location = useLocation();
24 | const message = getMessage(code, location);
25 | return (
26 |
27 |
28 |
29 | {message}
30 |
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/App/libs/router/crash-page.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Anchor, Space, Stack, Text, Title } from '@mantine/core';
3 | import { Icon } from 'App/components';
4 |
5 | export function CrashPage({ error }) {
6 | return (
7 |
8 |
9 |
10 | You encountered a bug
11 | This is our fault.
12 |
13 | Not much you can do about it, but here are three suggestions anyway
14 |
15 | Reload page - you never know
16 |
20 | Email us a bug report, please include the information below
21 |
22 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/src/App/libs/layout/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { AppShell } from '@mantine/core';
3 | import { useLocation } from 'react-router-dom';
4 | import classNames from 'App/libs/layout/index.module.css';
5 | import { Container } from 'App/components';
6 | import { HeaderLogo } from 'App/libs/layout/header-logo';
7 | import { HeaderLinks } from 'App/libs/layout/header-links';
8 |
9 | export function Layout({ children }) {
10 | const location = useLocation();
11 | const isHome = location.pathname === `/`;
12 | const withBorder = location.pathname !== `/`;
13 | const { root, header, container } = classNames;
14 | return (
15 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | {children}
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/src/App/libs/provider/url-params.js:
--------------------------------------------------------------------------------
1 | import { isEqual } from 'lodash';
2 | import { NAMESPACES_BY_ID } from 'App/libs/provider/namespaces';
3 |
4 | const DEFAULT_NAMESPACES = Object.freeze(Object.keys(NAMESPACES_BY_ID)).filter(
5 | (id) => id !== 'all',
6 | );
7 |
8 | const qpQuery = 'query';
9 | const qpNamespace = 'namespace';
10 |
11 | export function parseUrlParams(search) {
12 | const urlParams = new URLSearchParams(search);
13 | const query = urlParams.get(qpQuery) ?? '';
14 | const namespaces =
15 | urlParams.get(qpNamespace)?.split(',') ?? DEFAULT_NAMESPACES;
16 | return { query, namespaces };
17 | }
18 |
19 | export function createUrlParams({ query, namespaces }) {
20 | const queryParts = [];
21 | if (query.length > 0)
22 | queryParts.push(`${qpQuery}=${encodeURIComponent(query)}`);
23 | if (namespaces.length > 0 && !isEqual(namespaces, DEFAULT_NAMESPACES)) {
24 | if (namespaces.includes('all')) {
25 | namespaces = namespaces.filter((id) => id !== 'all');
26 | }
27 | queryParts.push(`${qpNamespace}=` + namespaces.join(','));
28 | }
29 | return queryParts.length > 0 ? '?' + queryParts.join('&') : '';
30 | }
31 |
--------------------------------------------------------------------------------
/src/App/utils/url-builder.js:
--------------------------------------------------------------------------------
1 | export function UrlBuilder(url) {
2 | if (url instanceof UrlBuilder) {
3 | this.path = url.path;
4 | this.query = url.query;
5 | } else if (typeof url === 'string' || url instanceof String) {
6 | let [path, query] = url.split('?');
7 | if (!path.startsWith('http') && !path.startsWith('/')) path = '/' + path;
8 |
9 | this.path = path.replace(/\/+$/, '');
10 | this.query = query ? '?' + query : '';
11 | } else {
12 | throw new Error('Unexpected argument (' + typeof url + '): ' + url);
13 | }
14 | }
15 |
16 | UrlBuilder.prototype.add = function (...parts) {
17 | this.path = [
18 | this.path,
19 | ...parts
20 | .map((part) => (typeof part !== 'string' ? part.toString() : part))
21 | .map((part) => part.replace(/^\/+|\/+$/g, '')),
22 | ].join('/');
23 | return this;
24 | };
25 |
26 | UrlBuilder.prototype.queryParam = function (key, val) {
27 | this.query =
28 | (this.query ? this.query + '&' : '?') + key + '=' + encodeURIComponent(val);
29 | return this;
30 | };
31 |
32 | UrlBuilder.prototype.toString = function (withTrailingSlash = false) {
33 | return this.path + (withTrailingSlash ? '/' : '') + this.query;
34 | };
35 |
--------------------------------------------------------------------------------
/src/App/pages/search/abstract-container/abstract/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { AbstractFeedback } from 'App/pages/search/abstract-container/abstract/abstract-feedback.js';
3 | import { Typography } from 'App/pages/search/typography/index.js';
4 | import { AbstractQuestions } from 'App/pages/search/abstract-container/abstract/abstract-questions/index.js';
5 | import { AbstractAbout } from 'App/pages/search/abstract-container/abstract/abstract-about.js';
6 | import { AbstractTitle } from 'App/pages/search/abstract-container/abstract/abstract-title.js';
7 | import { AbstractContent } from 'App/pages/search/abstract-container/abstract/abstract-content.js';
8 | import { AbstractDisclaimer } from 'App/pages/search/abstract-container/abstract/abstract-disclaimer.js';
9 | import { useConsent } from 'App/pages/search/abstract-container/abstract/use-consent.js';
10 |
11 | export function Abstract() {
12 | const { value } = useConsent();
13 |
14 | return (
15 |
16 |
17 | {value ? (
18 | <>
19 |
20 |
21 |
22 |
23 | >
24 | ) : (
25 |
26 | )}
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2021: true,
5 | node: true,
6 | },
7 | extends: [
8 | 'eslint:recommended',
9 | 'plugin:react/recommended',
10 | 'plugin:import/recommended',
11 | 'plugin:prettier/recommended',
12 | ],
13 | overrides: [],
14 | parserOptions: {
15 | ecmaFeatures: {
16 | jsx: true,
17 | },
18 | ecmaVersion: 'latest',
19 | sourceType: 'module',
20 | },
21 | plugins: ['react', 'react-hooks', 'unused-imports'],
22 | rules: {
23 | strict: 0,
24 | 'react/jsx-uses-react': 'error',
25 | 'react/prop-types': 'off',
26 | 'react/display-name': 'off',
27 | 'react-hooks/rules-of-hooks': 'error',
28 | 'react-hooks/exhaustive-deps': [
29 | 'warn',
30 | {
31 | additionalHooks:
32 | 'use(CustomCompare(Callback|Effect)|Cancelable(Layout)?Effect)',
33 | },
34 | ],
35 | 'unused-imports/no-unused-imports': 'error',
36 | 'import/order': ['error', { 'newlines-between': 'never' }],
37 | 'import/no-unassigned-import': ['error', { allow: ['**/*.css'] }],
38 | },
39 | settings: {
40 | react: {
41 | version: 'detect',
42 | },
43 | 'import/resolver': {
44 | alias: {
45 | map: [['App', './src/App']],
46 | extensions: ['.js', '.jsx', '.json', '.css'],
47 | },
48 | },
49 | },
50 | ignorePatterns: ['node_modules/*', 'public/*', '*.css'],
51 | };
52 |
--------------------------------------------------------------------------------
/src/App/components/icon/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box } from '@mantine/core';
3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4 | import classNames from 'App/components/icon/index.module.css';
5 |
6 | function resolveIconName(name, maybeType) {
7 | if (name.startsWith('fa') && name.charAt(3) === '-') {
8 | if (maybeType != null)
9 | throw new Error(
10 | `Invalid usage, cannot set both name with prefix (${name}) and type (${maybeType})`,
11 | );
12 |
13 | const shortType = name.charAt(2);
14 | name = name.substring(4);
15 | switch (shortType) {
16 | case 's':
17 | return `fa-${name} fa-solid`;
18 | case 'b':
19 | return `fa-${name} fa-brands`;
20 | case 'r':
21 | return `fa-${name} fa-regular`;
22 | case 'c':
23 | return `fa-${name} fa-custom`;
24 | default:
25 | throw new Error(`Unknown icon prefix: ${name}`);
26 | }
27 | }
28 | return `fa-${name} fa-${maybeType ?? 'solid'}`;
29 | }
30 |
31 | export function Icon({ name, type, color, disabled, size, ...rest }) {
32 | const icon = resolveIconName(name, type);
33 | const { box } = classNames;
34 | return (
35 | (
40 |
41 | )}
42 | {...rest}
43 | />
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/src/App/pages/search/abstract-container/abstract/abstract-questions/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { List, Stack, Text } from '@mantine/core';
3 | import { useSearchContext } from 'App/libs/provider/index.js';
4 | import { fontWeightBold } from 'App/styles/common.js';
5 | import classNames from 'App/pages/search/abstract-container/abstract/abstract-questions/index.module.css';
6 | import { Icon } from 'App/components/index.js';
7 | import { Link } from 'App/libs/router/index.js';
8 | import { useMobile } from 'App/hooks/index.js';
9 |
10 | export function AbstractQuestions() {
11 | const isMobile = useMobile();
12 | const questions = useSearchContext((ctx) => ctx.summary.questions);
13 | if (!(questions?.length > 0)) return null;
14 |
15 | return (
16 |
17 |
18 | Also try these questions
19 |
20 |
}
23 | type="unordered"
24 | center
25 | >
26 | {questions.map(({ text, url }, i) => (
27 |
28 |
29 |
30 | {text}
31 |
32 |
33 |
34 | ))}
35 |
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/src/App/pages/search/abstract-container/abstract/abstract-feedback.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Button, Group } from '@mantine/core';
3 | import { Get } from 'App/libs/fetcher/index.js';
4 | import { useSearchContext } from 'App/libs/provider/index.js';
5 | import { Icon } from 'App/components/index.js';
6 |
7 | function Action({ icon, name, type, ...props }) {
8 | return (
9 | }
11 | color="var(--low-contrast-text)"
12 | variant="outline"
13 | onClick={() => {}}
14 | radius="xl"
15 | size="xs"
16 | {...props}
17 | >
18 | {name}
19 |
20 | );
21 | }
22 |
23 | export function AbstractFeedback() {
24 | const feedbackUrl = useSearchContext((ctx) => ctx.summary.feedbackUrl);
25 | const [state, setState] = useState(0);
26 | if (!feedbackUrl || feedbackUrl === state) return null;
27 |
28 | const onClick = (url) => {
29 | setState(1);
30 | Get(url).finally(() => setState(feedbackUrl));
31 | };
32 |
33 | return (
34 |
35 | onClick(feedbackUrl + 'good')}
40 | />
41 | onClick(feedbackUrl + 'bad')}
46 | />
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/src/App/pages/search/index.js:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 | import { Group, Stack } from '@mantine/core';
3 | import { Results } from 'App/pages/search/results-container/results/index.js';
4 | import { Abstract } from 'App/pages/search/abstract-container/abstract/index.js';
5 | import { SearchSources } from 'App/pages/search/search-sources';
6 | import { SearchInput } from 'App/pages/search/search-input/index.js';
7 | import { SearchClassic } from 'App/pages/search/search-classic.js';
8 | import { SearchContainer } from 'App/pages/search/search-container/index.js';
9 | import { ResultsContainer } from 'App/pages/search/results-container/index.js';
10 | import { AbstractContainer } from 'App/pages/search/abstract-container/index.js';
11 | import { useMobile } from 'App/hooks/index.js';
12 |
13 | export function Search() {
14 | const viewportRef = useRef(null);
15 | const isMobile = useMobile();
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | viewportRef.current.scrollBy(options)}
29 | />
30 |
31 |
32 |
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/App/index.js:
--------------------------------------------------------------------------------
1 | import 'App/styles/reset.css';
2 |
3 | import '@fontsource/inter/latin-300.css';
4 | import '@fontsource/inter/latin-400.css';
5 | import '@fontsource/inter/latin-500.css';
6 | import '@fontsource/inter/latin-600.css';
7 | import '@fontsource/inter/latin-700.css';
8 |
9 | import '@mantine/core/styles.css';
10 | import '@mantine/code-highlight/styles.css';
11 | import '@mantine/notifications/styles.css';
12 |
13 | import React from 'react';
14 | import { BrowserRouter } from 'react-router-dom';
15 | import { Notifications } from '@mantine/notifications';
16 | import { ColorScheme, ThemeProvider } from 'App/styles/theme';
17 | import { ErrorBoundary, Router, CrashPage } from 'App/libs/router';
18 | import { Layout } from 'App/libs/layout';
19 | import { Home } from 'App/pages/home';
20 | import { Search } from 'App/pages/search';
21 | import { Md } from 'App/pages/md';
22 | import { Testcomp } from 'App/pages/testcomp';
23 | import { SearchContext } from 'App/libs/provider';
24 |
25 | export default function App() {
26 | return (
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/src/App/utils/__test__/url-builder.test.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest';
2 | import { UrlBuilder } from '..';
3 |
4 | test('constructor', () => {
5 | expect(new UrlBuilder('https://domain.tld/my/path/here').toString()).toBe(
6 | 'https://domain.tld/my/path/here',
7 | );
8 |
9 | expect(new UrlBuilder('/my/path/here').toString()).toBe('/my/path/here');
10 |
11 | expect(new UrlBuilder('my/path/here/').toString()).toBe('/my/path/here');
12 |
13 | expect(
14 | new UrlBuilder(
15 | new UrlBuilder('my/path/here/')
16 | .add('test')
17 | .queryParam('key1', 'val1')
18 | .queryParam('key2', 'val2'),
19 | ).toString(),
20 | ).toBe('/my/path/here/test?key1=val1&key2=val2');
21 | });
22 |
23 | test('add', () => {
24 | expect(
25 | new UrlBuilder('/path/').add('/test/').add('/multiple/sub/').toString(),
26 | ).toBe('/path/test/multiple/sub');
27 |
28 | expect(new UrlBuilder('path').add(15).toString()).toBe('/path/15');
29 | });
30 |
31 | test('trailing slash', () => {
32 | expect(new UrlBuilder('/path').add('test').toString(true)).toBe(
33 | '/path/test/',
34 | );
35 |
36 | expect(
37 | new UrlBuilder('/path').add('test').queryParam('key', 'val').toString(true),
38 | ).toBe('/path/test/?key=val');
39 | });
40 |
41 | test('correctly parses query component', () => {
42 | expect(new UrlBuilder('/path/?a=b&c=123').query).toBe('?a=b&c=123');
43 |
44 | expect(
45 | new UrlBuilder('/path/').queryParam('a', 'b').queryParam('c', 123).query,
46 | ).toBe('?a=b&c=123');
47 |
48 | expect(new UrlBuilder('path').query).toBe('');
49 | });
50 |
--------------------------------------------------------------------------------
/src/App/pages/search/search-sources.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 | import { Tabs } from '@mantine/core';
3 | import { useLocation } from 'react-router-dom';
4 | import { ALL_NAMESPACES, useSearchContext } from 'App/libs/provider';
5 | import { Icon } from 'App/components';
6 | import { parseUrlParams } from 'App/libs/provider/url-params';
7 |
8 | function Source({ id, icon, name, selectedTab }) {
9 | const toggleNamespace = useSearchContext((ctx) => ctx.toggleNamespace);
10 | const tabRef = useRef(null);
11 |
12 | useEffect(() => {
13 | if (selectedTab === id) {
14 | tabRef?.current?.scrollIntoView({
15 | behavior: 'smooth',
16 | block: 'center',
17 | });
18 | }
19 | }, [id, selectedTab]);
20 |
21 | return (
22 | toggleNamespace(id)}
25 | leftSection={ }
26 | ref={tabRef}
27 | >
28 | {name}
29 |
30 | );
31 | }
32 |
33 | export function SearchSources() {
34 | const location = useLocation();
35 | const { namespaces } = parseUrlParams(location.search);
36 | const defaultValue = namespaces?.length === 1 ? namespaces[0] : 'all';
37 | return (
38 |
39 |
40 | {ALL_NAMESPACES.map(({ id, name, icon }) => (
41 |
48 | ))}
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/App/pages/search/abstract-container/abstract/abstract-about.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Anchor, Stack, Text } from '@mantine/core';
3 | import { useSearchContext } from 'App/libs/provider/index.js';
4 | import { fontWeightBold } from 'App/styles/common.js';
5 | import { Link } from 'App/libs/router/index.js';
6 | import { useConsent } from 'App/pages/search/abstract-container/abstract/use-consent.js';
7 |
8 | export function AbstractAbout() {
9 | const { setValue } = useConsent();
10 | const hasFinished = useSearchContext(
11 | (ctx) => ctx.summary.feedbackUrl != null,
12 | );
13 | if (!hasFinished) return null;
14 |
15 | return (
16 |
17 |
18 | About the answer
19 |
20 |
21 | This answer is AI-generated, based on your query and search results. By
22 | submitting a query, you agree to share data with OpenAI, governed by
23 | theirs{' '}
24 | Terms of Use{' '}
25 | and{' '}
26 |
27 | Privacy Policy
28 |
29 | . Note that answers may contain inaccuracies or unintended biases and
30 | shouldn't serve as a substitute for professional advice in medical,
31 | legal, financial, or other domains.{' '}
32 |
33 | Click setValue(false)}>here to revoke
34 | showing the answer.
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vespa-search",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite dev --port 3000",
8 | "build": "vite build",
9 | "preview": "vite preview",
10 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0"
11 | },
12 | "dependencies": {
13 | "react": "^18",
14 | "react-dom": "^18"
15 | },
16 | "devDependencies": {
17 | "@fontsource/inter": "^5",
18 | "@fortawesome/fontawesome-svg-core": "6.7.2",
19 | "@fortawesome/free-brands-svg-icons": "6.7.2",
20 | "@fortawesome/free-regular-svg-icons": "6.7.2",
21 | "@fortawesome/free-solid-svg-icons": "6.7.2",
22 | "@fortawesome/react-fontawesome": "0.2.2",
23 | "@mantine/code-highlight": "^7",
24 | "@mantine/core": "^7",
25 | "@mantine/hooks": "^7",
26 | "@mantine/notifications": "^7",
27 | "@types/react": "^18",
28 | "@types/react-dom": "^18",
29 | "@vitejs/plugin-react": "^4",
30 | "clsx": "^2",
31 | "dayjs": "^1",
32 | "eslint": "^8",
33 | "eslint-config-prettier": "^9",
34 | "eslint-import-resolver-alias": "^1",
35 | "eslint-plugin-import": "^2",
36 | "eslint-plugin-prettier": "^5",
37 | "eslint-plugin-react": "^7",
38 | "eslint-plugin-react-hooks": "^4",
39 | "eslint-plugin-unused-imports": "^3",
40 | "file-saver": "^2",
41 | "lodash": "^4",
42 | "marked": "^10.0.0",
43 | "postcss": "^8",
44 | "postcss-preset-mantine": "^1",
45 | "postcss-simple-vars": "^7",
46 | "prettier": "^3",
47 | "react-router-dom": "^6",
48 | "vite": "^5.0.0",
49 | "vitest": "^0",
50 | "zustand": "^4"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yaml:
--------------------------------------------------------------------------------
1 | name: Build & Deploy
2 |
3 | permissions:
4 | contents: read
5 | id-token: write
6 |
7 | on:
8 | workflow_dispatch:
9 | push:
10 | branches:
11 | - main
12 | pull_request:
13 | branches:
14 | - main
15 | schedule:
16 | - cron: "0 0 1 * *"
17 |
18 | jobs:
19 | build:
20 | runs-on: ubuntu-latest
21 |
22 | steps:
23 | - name: Checkout
24 | uses: actions/checkout@v4
25 |
26 | - name: Install dependencies
27 | run: yarn install
28 |
29 | - name: Build project
30 | run: yarn build
31 |
32 | - name: Store build artifact
33 | uses: actions/upload-artifact@v4
34 | if: github.ref_name == github.event.repository.default_branch
35 | with:
36 | name: build
37 | path: dist/
38 |
39 | deploy:
40 | if: github.ref_name == github.event.repository.default_branch
41 |
42 | runs-on: ubuntu-latest
43 |
44 | needs:
45 | - build
46 |
47 | environment:
48 | name: Production
49 | url: ${{ vars.PUBLIC_URL }}
50 |
51 | steps:
52 | - uses: actions/download-artifact@v4
53 | with:
54 | name: build
55 | path: dist/
56 |
57 | - name: Configure AWS credentials
58 | uses: aws-actions/configure-aws-credentials@v4
59 | with:
60 | aws-region: us-east-1
61 | role-to-assume: ${{ vars.IAM_ROLE }}
62 |
63 | - name: Deploy
64 | run: |
65 | aws s3 sync dist/ "s3://${{ vars.S3_BUCKET }}/frontend/" --exclude "*.map" --no-progress
66 | aws cloudfront create-invalidation --distribution-id "${{ vars.CF_DISTRIBUTION_ID }}" --paths "/*"
67 |
--------------------------------------------------------------------------------
/src/App/pages/search/abstract-container/abstract/abstract-title.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Container, Group, Popover, Text, Title } from '@mantine/core';
3 | import { fontWeightLight } from 'App/styles/common.js';
4 | import { Icon } from 'App/components/index.js';
5 | import { useMobile } from 'App/hooks';
6 |
7 | export function AbstractTitle() {
8 | const isMobile = useMobile();
9 |
10 | return (
11 |
12 |
13 | Answer{' '}
14 |
15 | (experimental)
16 |
17 |
18 |
19 |
20 |
25 | What is this?
26 |
27 |
28 |
29 |
30 |
31 | The answer's accuracy relies on your query and the search
32 | outcome. Irrelevant query context might influence results, and
33 | answers vary based on sources used. Answers aren't suitable
34 | for regulatory or legal purposes and shouldn't replace
35 | professional advice in medical, legal, or financial areas.
36 |
37 |
38 |
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/src/App/libs/layout/index.module.css:
--------------------------------------------------------------------------------
1 | .root {
2 | &[data-is-home='true'] {
3 | background-image: radial-gradient(
4 | circle,
5 | transparent 0%,
6 | rgb(251, 252, 253) 99%
7 | ),
8 | repeating-linear-gradient(
9 | 0deg,
10 | rgb(221, 243, 228) 0px,
11 | rgb(221, 243, 228) 1px,
12 | transparent 1px,
13 | transparent 6px
14 | ),
15 | repeating-linear-gradient(
16 | 90deg,
17 | rgb(221, 243, 228) 0px,
18 | rgb(221, 243, 228) 1px,
19 | transparent 1px,
20 | transparent 6px
21 | ),
22 | linear-gradient(90deg, rgb(251, 252, 253), rgb(251, 252, 253));
23 |
24 | @mixin dark {
25 | background-image: radial-gradient(
26 | circle,
27 | transparent 0%,
28 | rgb(21, 23, 24) 99%
29 | ),
30 | repeating-linear-gradient(
31 | 0deg,
32 | rgb(18, 40, 31) 0px,
33 | rgb(18, 40, 31) 1px,
34 | transparent 1px,
35 | transparent 6px
36 | ),
37 | repeating-linear-gradient(
38 | 90deg,
39 | rgb(18, 40, 31) 0px,
40 | rgb(18, 40, 31) 1px,
41 | transparent 1px,
42 | transparent 6px
43 | ),
44 | linear-gradient(90deg, rgb(21, 23, 24), rgb(21, 23, 24));
45 | }
46 | }
47 | }
48 |
49 | .header {
50 | display: flex;
51 | align-items: center;
52 | justify-content: space-between;
53 | padding-left: var(--mantine-spacing-md);
54 | padding-right: var(--mantine-spacing-md);
55 | background: transparent;
56 |
57 | &[data-with-border='true'] {
58 | background: var(--mantine-color-body);
59 | }
60 | }
61 |
62 | .container {
63 | grid-auto-flow: column;
64 | justify-content: space-between;
65 | }
66 |
--------------------------------------------------------------------------------
/src/App/pages/md/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Stack } from '@mantine/core';
3 | import { parseMarkdown } from 'App/pages/search/md-parser';
4 |
5 | const md = `
6 | # Heading1
7 | ## Heading2
8 | ### Heading3
9 | #### Heading4
10 | ##### Heading5
11 | ###### Heading6
12 |
13 | | Syntax | Description |
14 | | ----------- | ----------- |
15 | | Header | Title |
16 | | Paragraph | Text |
17 |
18 | > A block
19 | >
20 |
21 | >> quote with **bold** text
22 |
23 | Here is an ~example~ of a _searcher_ in **Vespa**:
24 |
25 | \`\`\`java
26 | public class MySearcher extends Searcher {
27 | private Query processQuery(Query query) {
28 | // Implement query processing logic here
29 | return query;
30 | }
31 | private Result processResult(Result result) {
32 | // Implement result processing logic here
33 | return result;
34 | }
35 | }
36 | \`\`\`
37 |
38 | In this example, \`\` extends the \`Searcher\` class [relative](./test.html) takes in a \`Query\` object and an \`Execution\` object, which is used to execute the query.
39 | The method then processes the query and [absolute](https://yahoo.com) it using the \`Execution\` object. Finally, it processes the result and returns it.
40 | This is just a basic example, and the actual processing logic will depend on the specific use case. Developers can provide their own searchers and inject them into the query chain to customize the search behavior. [1][2]
41 |
42 | ---
43 |
44 | 1. First item
45 | 1. Second item
46 | 1. Third item
47 | 1. Indented item
48 | 1. Indented item
49 |
50 | ---
51 |
52 | - First item
53 | - Second item
54 | - Third item
55 | - Indented item
56 | - Indented item
57 | `;
58 | export function Md() {
59 | return {parseMarkdown(md)} ;
60 | }
61 |
--------------------------------------------------------------------------------
/src/App/styles/icons/custom-icons.js:
--------------------------------------------------------------------------------
1 | // prettier-ignore
2 | export default [
3 | ['apps', 36, 36, 'M2,3a1,-1 0 0 1 1,-1h4a1,1 0 0 1 1,1v4a-1,1 0 0 1 -1,1h-4a-1,-1 0 0 1 -1,-1v-4zM14,3a1,-1 0 0 1 1,-1h4a1,1 0 0 1 1,1v4a-1,1 0 0 1 -1,1h-4a-1,-1 0 0 1 -1,-1v-4zM26,3a1,-1 0 0 1 1,-1h4a1,1 0 0 1 1,1v4a-1,1 0 0 1 -1,1h-4a-1,-1 0 0 1 -1,-1v-4zM2,15a1,-1 0 0 1 1,-1h4a1,1 0 0 1 1,1v4a-1,1 0 0 1 -1,1h-4a-1,-1 0 0 1 -1,-1v-4zM14,15a1,-1 0 0 1 1,-1h4a1,1 0 0 1 1,1v4a-1,1 0 0 1 -1,1h-4a-1,-1 0 0 1 -1,-1v-4zM26,15a1,-1 0 0 1 1,-1h4a1,1 0 0 1 1,1v4a-1,1 0 0 1 -1,1h-4a-1,-1 0 0 1 -1,-1v-4zM2,27a1,-1 0 0 1 1,-1h4a1,1 0 0 1 1,1v4a-1,1 0 0 1 -1,1h-4a-1,-1 0 0 1 -1,-1v-4zM14,27a1,-1 0 0 1 1,-1h4a1,1 0 0 1 1,1v4a-1,1 0 0 1 -1,1h-4a-1,-1 0 0 1 -1,-1v-4zM26,27a1,-1 0 0 1 1,-1h4a1,1 0 0 1 1,1v4a-1,1 0 0 1 -1,1h-4a-1,-1 0 0 1 -1,-1v-4z'],
4 | ['grid', 18, 18, 'M1,1h4v4h-4v-4ZM1,7h4v4h-4v-4ZM1,13h4v4h-4v-4ZM7,1h4v4h-4v-4ZM7,7h4v4h-4v-4ZM7,13h4v4h-4v-4ZM13,1h4v4h-4v-4ZM13,7h4v4h-4v-4ZM13,13h4v4h-4v-4Z'],
5 | ['grid-classic', 36, 36, 'M2,3a1,-1 0 0 1 1,-1h12a1,1 0 0 1 1,1v12a-1,1 0 0 1 -1,1h-12a-1,-1 0 0 1 -1,-1v-12zM20,3a1,-1 0 0 1 1,-1h12a1,1 0 0 1 1,1v12a-1,1 0 0 1 -1,1h-12a-1,-1 0 0 1 -1,-1v-12zM2,21a1,-1 0 0 1 1,-1h12a1,1 0 0 1 1,1v12a-1,1 0 0 1 -1,1h-12a-1,-1 0 0 1 -1,-1v-12zM20,21a1,-1 0 0 1 1,-1h12a1,1 0 0 1 1,1v12a-1,1 0 0 1 -1,1h-12a-1,-1 0 0 1 -1,-1v-12z'],
6 | ['grid-modern', 36, 36, 'M2,3a1,-1 0 0 1 1,-1h18a1,1 0 0 1 1,1v12a-1,1 0 0 1 -1,1h-18a-1,-1 0 0 1 -1,-1v-12zM26,3a1,-1 0 0 1 1,-1h6a1,1 0 0 1 1,1v12a-1,1 0 0 1 -1,1h-6a-1,-1 0 0 1 -1,-1v-12zM2,21a1,-1 0 0 1 1,-1h6a1,1 0 0 1 1,1v12a-1,1 0 0 1 -1,1h-6a-1,-1 0 0 1 -1,-1v-12zM14,21a1,-1 0 0 1 1,-1h18a1,1 0 0 1 1,1v12a-1,1 0 0 1 -1,1h-18a-1,-1 0 0 1 -1,-1v-12z'],
7 | ].map(([iconName, width, height, path]) => ({
8 | icon: [width, height, [], null, path],
9 | iconName,
10 | prefix: 'fa-custom',
11 | }));
12 |
--------------------------------------------------------------------------------
/src/App/libs/router/router.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Routes, Route, useParams, Navigate } from 'react-router-dom';
3 | import { ErrorPage } from '.';
4 |
5 | const mainTitle = import.meta.env.VITE_SITE_TITLE;
6 |
7 | function TitledRoute({ element, title, default: isDefault, ...props }) {
8 | const params = useParams();
9 | const clone = React.cloneElement(element, Object.assign(props, params));
10 | if (title != null) {
11 | const titleStr = typeof title === 'function' ? title(params) : title;
12 | document.title = titleStr.endsWith(mainTitle)
13 | ? titleStr
14 | : `${titleStr} - ${mainTitle}`;
15 | } else if (isDefault) {
16 | // Reset the title if title is not set and this is a default router
17 | document.title = mainTitle;
18 | }
19 |
20 | return clone;
21 | }
22 |
23 | export function Router({ children }) {
24 | // If there is only one route then this comes as an object.
25 | if (!Array.isArray(children)) children = [children];
26 | children = children
27 | .filter((child) => typeof child === 'object')
28 | .filter(({ props }) => props.enabled ?? true);
29 |
30 | if (!children.some((child) => child.props.default))
31 | children.push( );
32 |
33 | return (
34 |
35 | {children.map(({ props, ...element }, i) => (
36 |
44 | )
45 | }
46 | />
47 | ))}
48 |
49 | );
50 | }
51 |
52 | export function Redirect({ to, replace }) {
53 | return ;
54 | }
55 |
--------------------------------------------------------------------------------
/src/App/pages/search/abstract-container/abstract/abstract-disclaimer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, Container, HoverCard, Stack, Text } from '@mantine/core';
3 | import { Link } from 'App/libs/router/index.js';
4 | import { useConsent } from 'App/pages/search/abstract-container/abstract/use-consent.js';
5 | import { fontWeightBold } from 'App/styles/common.js';
6 |
7 | function DisclaimerDetails() {
8 | return (
9 |
10 |
11 |
12 | privacy notice
13 |
14 |
15 |
16 |
17 |
18 |
19 | Privacy notice
20 |
21 |
22 | By entering a query, you consent to sharing it with OpenAI. The
23 | answer you see is generated by an AI model, using your query and
24 | search results. Please be aware there might be potential
25 | inaccuracies or unintended bias in the answers. The answers are
26 | not a substitute for medical, legal, financial, or other
27 | professional advice. Familiarize yourself with OpenAI's{' '}
28 |
29 | Terms of Use
30 | {' '}
31 | and{' '}
32 |
33 | Privacy Policy
34 |
35 | . Prefer a traditional search? Please check{' '}
36 | docs.search.ai.
37 |
38 |
39 |
40 |
41 |
42 | );
43 | }
44 |
45 | export function AbstractDisclaimer() {
46 | const { setValue } = useConsent();
47 | return (
48 |
49 | setValue(true)}>Show abstract
50 |
51 |
52 | By showing, you consent to share data with OpenAI. The AI-generated
53 | answer may have biases or inaccuracies. See for
54 | more details. For the traditional search{' '}
55 | docs.vespa.ai.
56 |
57 |
58 |
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/src/App/libs/fetcher/index.js:
--------------------------------------------------------------------------------
1 | import { UrlBuilder } from 'App/utils';
2 |
3 | export function Get(url, params) {
4 | return Fetch('GET', url, params);
5 | }
6 |
7 | function xhrFetch(url, { body, method, ...params }, onProgress) {
8 | return new Promise(function (resolve, reject) {
9 | const xhr = new XMLHttpRequest();
10 | xhr.open(method, url);
11 | Object.entries(params).forEach(([key, value]) => {
12 | switch (key) {
13 | case 'credentials': {
14 | xhr.withCredentials = value === 'include';
15 | break;
16 | }
17 | case 'headers': {
18 | Object.entries(value).forEach(([headerKey, headerValue]) =>
19 | xhr.setRequestHeader(headerKey, headerValue),
20 | );
21 | break;
22 | }
23 | default:
24 | throw new Error('Unsupported param for XMLHttpRequest: ' + value);
25 | }
26 | });
27 | xhr.onload = () =>
28 | resolve({
29 | ok: xhr.status >= 200 && xhr.status < 300,
30 | status: xhr.status,
31 | headers: new Headers(
32 | xhr
33 | .getAllResponseHeaders()
34 | .trim()
35 | .split(/[\r\n]+/)
36 | .map((s) => {
37 | const index = s.indexOf(':');
38 | return [s.substring(0, index), s.substring(index + 1)];
39 | }),
40 | ),
41 | text: () => Promise.resolve(xhr.responseText),
42 | json: () => Promise.resolve(JSON.parse(xhr.responseText)),
43 | });
44 | xhr.onerror = (event) => reject(event);
45 | xhr.upload.onprogress = (event) => onProgress(event.loaded / event.total);
46 | xhr.send(body);
47 | });
48 | }
49 |
50 | async function Fetch(
51 | method,
52 | url,
53 | { returnRaw, json, onProgress, ...params } = {},
54 | ) {
55 | if (url instanceof UrlBuilder) url = url.toString();
56 | params.method = method;
57 | if (json) {
58 | if (params.body)
59 | throw new Error("Cannot set both 'json' and 'body' parameters");
60 | params.body = JSON.stringify(json);
61 | params.headers = { 'Content-Type': 'application/json' };
62 | }
63 | return (
64 | (onProgress ? xhrFetch(url, params, onProgress) : fetch(url, params))
65 | // Reject promise if response is not OK
66 | .then((response) => {
67 | if (response.ok) return response;
68 | return response.text().then((text) => {
69 | let message = text;
70 | try {
71 | const json = JSON.parse(text);
72 | if ('message' in json) message = json.message;
73 | } catch (e) {
74 | // not JSON
75 | }
76 | return Promise.reject({ message, code: response.status });
77 | });
78 | })
79 | // automatically return the data if it's a known content type
80 | .then((response) => {
81 | const contentType = response.headers.get('content-type');
82 | if (!contentType || returnRaw) return response;
83 | if (contentType.includes('application/json')) {
84 | return response.json();
85 | } else if (contentType.includes('text/plain')) {
86 | return response.text();
87 | }
88 | return response;
89 | })
90 | );
91 | }
92 |
--------------------------------------------------------------------------------
/src/App/pages/search/results-container/results/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 | import { Badge, Group, Spoiler, Stack, Text, Title } from '@mantine/core';
3 | import { NAMESPACES_BY_ID, useSearchContext } from 'App/libs/provider/index.js';
4 | import { parseMarkdown } from 'App/pages/search/md-parser.js';
5 | import { Content, Error, Icon, LoadingResult } from 'App/components/index.js';
6 | import { Typography } from 'App/pages/search/typography/index.js';
7 | import { Link } from 'App/libs/router/index.js';
8 | import classNames from 'App/pages/search/results-container/results/index.module.css';
9 |
10 | function Result({
11 | refId,
12 | title,
13 | content,
14 | base_uri,
15 | path,
16 | namespace,
17 | scrollBy,
18 | }) {
19 | const isSelected = useSearchContext((ctx) => ctx.selectedHit === refId);
20 | const ref = useRef();
21 | const titleLink = base_uri + path;
22 | const namespaceMeta = NAMESPACES_BY_ID[namespace];
23 | const { titleResult, iconExternal, spoilerControl } = classNames;
24 |
25 | useEffect(() => {
26 | if (!isSelected || !ref.current) return;
27 | const position = ref.current.getBoundingClientRect().top - 85;
28 | scrollBy({ top: position, behavior: 'smooth' });
29 | }, [ref, isSelected, scrollBy]);
30 |
31 | return (
32 |
33 |
41 |
42 |
43 |
50 | [{refId}] {title}{' '}
51 |
57 |
58 | {namespaceMeta && (
59 | }
61 | variant="light"
62 | size="xs"
63 | >
64 | {namespaceMeta.name}
65 |
66 | )}
67 |
68 |
69 | {parseMarkdown(content, { baseUrl: titleLink })}
70 |
71 |
72 |
73 |
74 | );
75 | }
76 |
77 | export function Results({ scrollBy }) {
78 | const { loading, error, hits } = useSearchContext((ctx) => ctx.hits);
79 |
80 | if (loading) return ;
81 | if (error) return ;
82 |
83 | return hits.length === 0 ? (
84 | No matches
85 | ) : (
86 |
87 | {hits.map((child, i) => (
88 |
94 | ))}
95 |
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/src/App/assets/img/vespa-logo-black.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
26 |
28 |
30 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/App/assets/img/vespa-logo-heather.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
29 |
31 |
33 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/App/styles/theme/resolver.js:
--------------------------------------------------------------------------------
1 | import { opacity } from 'App/styles/common';
2 | import { dark, light } from 'App/styles/theme/colors';
3 |
4 | const properties = [
5 | '--app-background-',
6 | '--subtle-background-',
7 | '--ui-element-background-',
8 | '--hovered-ui-element-background-',
9 | '--selected-ui-element-background-',
10 | '--subtle-border-and-separator-',
11 | '--ui-element-border-and-focus-',
12 | '--hovered-ui-element-border-',
13 | '--solid-background-',
14 | '--hovered-solid-background-',
15 | '--low-contrast-text-',
16 | '--high-contrast-text-',
17 | ];
18 |
19 | // prettier-ignore
20 | const cssVariablesOverrides = (colors) => {
21 | const result = {};
22 | for (let colorName in colors) {
23 | result['--mantine-color-text'] = 'var(--high-contrast-text-sage)';
24 | result['--mantine-color-body'] = 'var(--app-background-sage)';
25 | result['--mantine-color-error'] = 'var(--solid-background-red)';
26 | result['--mantine-color-placeholder'] = 'var(--hovered-ui-element-border-sage)';
27 | result['--mantine-color-anchor'] = 'var(--vespa-color-anchor)';
28 | result['--mantine-color-dimmed'] = 'var(--solid-background-gray)';
29 | result[`--mantine-color-${colorName}-filled`] = `var(--solid-background-${colorName})`;
30 | result[`--mantine-color-${colorName}-filled-hover`] = `var(--hovered-solid-background-${colorName})`;
31 | result[`--mantine-color-${colorName}-light`] = `var(--ui-element-background-${colorName})`;
32 | result[`--mantine-color-${colorName}-light-hover`] = `var(--hovered-ui-element-background-${colorName})`;
33 | result[`--mantine-color-${colorName}-light-color`] = `var(--solid-background-${colorName})`;
34 | result[`--mantine-color-${colorName}-outline`] = `var(--solid-background-${colorName})`;
35 | result[`--mantine-color-${colorName}-outline-hover`] = `var(--subtle-background-${colorName})`;
36 | }
37 | return result;
38 | };
39 |
40 | const cssVariablesResolver = (color) => {
41 | const result = {};
42 | Object.keys(color).forEach((key) => {
43 | color[key].forEach((value, index) => {
44 | result[properties[index] + key] = value;
45 | });
46 | });
47 | return result;
48 | };
49 |
50 | const cssVariablesDefaults = (color) => {
51 | const result = {};
52 | properties.forEach((property) => {
53 | const trimmedProperty = property.slice(0, -1);
54 | result[trimmedProperty] = `var(${property}${color})`;
55 | });
56 | return result;
57 | };
58 |
59 | export const resolver = (theme) => ({
60 | variables: {
61 | '--common-opacity': opacity,
62 | '--search-result-width': '67vw',
63 | '--search-abstract-width': '33vw',
64 | '--search-scrollarea-height':
65 | 'calc(100vh - var(--app-shell-header-height, 0px) - (2 * var(--mantine-spacing-md)))',
66 | // primary colors overrides
67 | '--mantine-primary-color-filled': `var(--solid-background-${theme.primaryColor})`,
68 | '--mantine-primary-color-filled-hover': `var(--hovered-solid-background-${theme.primaryColor})`,
69 | '--mantine-primary-color-light': `var(--ui-element-background-${theme.primaryColor})`,
70 | '--mantine-primary-color-light-hover': `var(--hovered-ui-element-background-${theme.primaryColor})`,
71 | '--mantine-primary-color-light-color': `var(--low-contrast-text-${theme.primaryColor})`,
72 | // brand colors
73 | '--vespa-color-rock': '#2E2F27',
74 | '--vespa-color-heather': '#61D790',
75 | },
76 | light: {
77 | ...cssVariablesResolver(light),
78 | ...cssVariablesOverrides(light),
79 | ...cssVariablesDefaults('sage'),
80 | '--vespa-color-anchor': '#0246C9',
81 | '--header-links': 'var(--mantine-color-black)',
82 | },
83 | dark: {
84 | ...cssVariablesResolver(dark),
85 | ...cssVariablesOverrides(dark),
86 | ...cssVariablesDefaults('sage'),
87 | '--vespa-color-anchor': '#5E93FB',
88 | '--header-links': 'var(--vespa-color-heather)',
89 | },
90 | });
91 |
--------------------------------------------------------------------------------
/src/App/libs/provider/reducer.js:
--------------------------------------------------------------------------------
1 | import { isEqual, shuffle, sortedUniq } from 'lodash';
2 | import { UrlBuilder } from 'App/utils';
3 | import { createUrlParams } from 'App/libs/provider/url-params';
4 | import { parseTokens } from 'App/pages/search/md-parser';
5 | import { ALL_NAMESPACES } from 'App/libs/provider/namespaces';
6 |
7 | const initialState = Object.freeze({ query: '', namespaces: [] });
8 |
9 | export function createStore(set) {
10 | const reset = () => apply({}, setFilters({}, initialState));
11 | return {
12 | ...reset(),
13 | setFilters: fn(set, setFilters),
14 | setQuery: fn(set, setQuery),
15 | setNamespaces: fn(set, setNamespaces),
16 | toggleNamespace: fn(set, toggleNamespace),
17 | summaryAppend: fn(set, summaryAppend),
18 | summaryComplete: fn(set, summaryComplete),
19 | setHits: fn(set, setHits),
20 | selectHit: fn(set, selectHit),
21 | reset,
22 | };
23 | }
24 |
25 | function setFilters(state, filters) {
26 | return Object.entries(filters).reduce((result, [key, value]) => {
27 | switch (key) {
28 | case 'query':
29 | return setQuery(result, value);
30 | case 'namespaces':
31 | return setNamespaces(result, value);
32 | default:
33 | throw new Error(`Unknown filter '${key}'`);
34 | }
35 | }, state);
36 | }
37 |
38 | function setQuery(state, query) {
39 | return isEqual(state.query, query) ? state : { ...state, query };
40 | }
41 |
42 | function setNamespaces(state, namespaces) {
43 | if (isEqual(state.namespaces, namespaces)) return state;
44 | return { ...state, namespaces };
45 | }
46 |
47 | function toggleNamespace(state, namespace) {
48 | if (namespace === 'all') {
49 | return setNamespaces(
50 | state,
51 | ALL_NAMESPACES.map(({ id }) => id),
52 | );
53 | }
54 | return setNamespaces(state, [namespace]);
55 | }
56 |
57 | function summaryAppend(state, summary) {
58 | const raw = (state.summary.raw + summary).replace(' ', '\n');
59 | return { ...state, summary: { raw } };
60 | }
61 |
62 | function summaryComplete(state) {
63 | const refs = parseTokens(state.summary.raw)
64 | .filter(({ type }) => type === 'ref')
65 | .map(({ text }) => state.hits.hits?.[parseInt(text) - 1])
66 | .filter((hit) => hit != null);
67 | const questions = shuffle(
68 | sortedUniq(refs.flatMap((hit) => hit.fields?.questions ?? []).sort()),
69 | )
70 | .filter((query) => query.toLowerCase() !== state.query.toLowerCase())
71 | .slice(0, 5)
72 | .map((query) => ({
73 | text: query,
74 | url: createUrlParams({ query, namespaces: state.namespaces }),
75 | }));
76 | const docIds = sortedUniq(refs.flatMap((hit) => hit.id).sort()).join(',');
77 | const feedbackUrl = new UrlBuilder(import.meta.env.VITE_ENDPOINT)
78 | .add('search')
79 | .queryParam('query', state.query)
80 | .queryParam('abstract', state.summary.raw)
81 | .queryParam('docids', docIds)
82 | .queryParam('queryProfile', 'llmsearch')
83 | .queryParam('reason', '')
84 | .toString(true);
85 | return {
86 | ...state,
87 | summary: { ...state.summary, questions, feedbackUrl },
88 | };
89 | }
90 |
91 | function setHits(state, hits) {
92 | return { ...state, hits };
93 | }
94 |
95 | function selectHit(state, selectedHit) {
96 | if (isEqual(state.selectedHit, selectedHit)) return state;
97 | return { ...state, selectedHit };
98 | }
99 |
100 | function fn(set, mapper) {
101 | return (input) => set((state) => apply(state, mapper(state, input)));
102 | }
103 |
104 | function apply(state, result) {
105 | // Reset hits and summary if the query has changed
106 | if (state.query !== result.query || state.namespaces !== result.namespaces) {
107 | result.hits = { loading: true };
108 | result.summary = { raw: '' };
109 | }
110 |
111 | // Unset selectedHit if the hits have changed
112 | if (state.hits !== result.hits) result.selectedHit = null;
113 |
114 | return Object.freeze(result);
115 | }
116 |
--------------------------------------------------------------------------------
/src/App/libs/provider/provider.js:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect, useRef } from 'react';
2 | import { useNavigate, useLocation } from 'react-router-dom';
3 | import { create } from 'zustand';
4 | import { useConsent } from 'App/pages/search/abstract-container/abstract/use-consent.js';
5 | import { UrlBuilder } from 'App/utils';
6 | import { Get } from 'App/libs/fetcher';
7 | import { createStore } from 'App/libs/provider/reducer';
8 | import { parseUrlParams, createUrlParams } from 'App/libs/provider/url-params';
9 |
10 | export const useSearchContext = create(createStore);
11 | const endpoint = import.meta.env.VITE_ENDPOINT;
12 |
13 | export function SearchContext() {
14 | const location = useLocation();
15 | const navigate = useNavigate();
16 | const queryRef = useRef(null);
17 | const { value: abstractConsent } = useConsent();
18 | const [
19 | query,
20 | namespaces,
21 | setFilters,
22 | setHits,
23 | summaryAppend,
24 | summaryComplete,
25 | ] = useSearchContext((ctx) => [
26 | ctx.query,
27 | ctx.namespaces,
28 | ctx.setFilters,
29 | ctx.setHits,
30 | ctx.summaryAppend,
31 | ctx.summaryComplete,
32 | ]);
33 |
34 | // Every time the URL changes, update the state
35 | useLayoutEffect(() => {
36 | if (location.search === queryRef.current) return;
37 | setFilters(parseUrlParams(location.search));
38 | queryRef.current = location.search;
39 | }, [setFilters, location.search]);
40 |
41 | // Every time the query/namespaces are changed, update the URL
42 | useLayoutEffect(() => {
43 | const queryParams = createUrlParams({ query, namespaces });
44 | if (queryRef.current == null || queryParams === queryRef.current) return;
45 |
46 | queryRef.current = queryParams;
47 | navigate(query ? '/search' + queryParams : '/');
48 | }, [navigate, query, namespaces]);
49 |
50 | useLayoutEffect(() => {
51 | if (query.length === 0) return;
52 | const filters = namespaces.map((n) => `+namespace:${n}`).join(' ');
53 |
54 | let cancelled = false;
55 |
56 | // If the user has not consented to the abstract yet, use regular search
57 | if (!abstractConsent) {
58 | const searchUrl = new UrlBuilder(endpoint)
59 | .add('search')
60 | .queryParam('query', query)
61 | .queryParam('filters', filters)
62 | .queryParam('queryProfile', 'llmsearch')
63 | .toString(true);
64 | Get(searchUrl)
65 | .then(
66 | (result) =>
67 | !cancelled && setHits({ hits: result.root.children ?? [] }),
68 | )
69 | .catch((error) => !cancelled && setHits({ error }));
70 | return () => {
71 | cancelled = true;
72 | };
73 | }
74 |
75 | // However, if the user has consented, use RAG search
76 | const streamUrl = new UrlBuilder(endpoint)
77 | .add('sse')
78 | .queryParam('query', query)
79 | .queryParam('filters', filters)
80 | .queryParam('queryProfile', 'ragsearch')
81 | .queryParam('llm.includeHits', 'true')
82 | .toString(true);
83 | const source = new EventSource(streamUrl);
84 | const onToken = (e) => summaryAppend(JSON.parse(e.data).token);
85 | const onHits = (e) => {
86 | if (!cancelled) {
87 | const result = JSON.parse(e.data);
88 | setHits({ hits: result.root.children ?? [] });
89 | }
90 | };
91 | const onError = () => summaryComplete() || source.close();
92 | source.addEventListener('token', onToken);
93 | source.addEventListener('hits', onHits);
94 | source.addEventListener('error', onError);
95 | return () => {
96 | cancelled = true;
97 | source.close();
98 | source.removeEventListener('token', onToken);
99 | source.removeEventListener('hits', onHits);
100 | source.removeEventListener('error', onError);
101 | };
102 | }, [
103 | query,
104 | namespaces,
105 | setHits,
106 | summaryAppend,
107 | summaryComplete,
108 | abstractConsent,
109 | ]);
110 |
111 | return null;
112 | }
113 |
--------------------------------------------------------------------------------
/src/App/pages/search/search-input/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react';
2 | import {
3 | ActionIcon,
4 | Combobox,
5 | Group,
6 | Text,
7 | Textarea,
8 | useCombobox,
9 | } from '@mantine/core';
10 | import classNames from 'App/pages/search/search-input/index.module.css';
11 | import { useSearchContext } from 'App/libs/provider';
12 | import { UrlBuilder } from 'App/utils';
13 | import { Get } from 'App/libs/fetcher';
14 | import { Icon } from 'App/components';
15 | import { Link } from 'App/libs/router';
16 | import { useMobile } from 'App/hooks';
17 |
18 | export function SearchInput({ size = 'md', autofocus = false }) {
19 | const [query, filters, setQuery] = useSearchContext((ctx) => [
20 | ctx.query,
21 | ctx.namespaces.map((n) => `+namespace:${n}`).join(' '),
22 | ctx.setQuery,
23 | ]);
24 | const [value, setValue] = useState(query);
25 | const [suggestions, setSuggestions] = useState([]);
26 | const inputRef = useRef(null);
27 | const combobox = useCombobox();
28 | const isMobile = useMobile();
29 | const { input, dropdown } = classNames;
30 |
31 | // Update search input if we go back/forward in history
32 | useEffect(() => setValue(query), [query]);
33 |
34 | useEffect(() => {
35 | let cancelled = false;
36 | if (value.length === 0) {
37 | setSuggestions([]);
38 | return;
39 | }
40 |
41 | Get(
42 | new UrlBuilder(import.meta.env.VITE_ENDPOINT)
43 | .add('suggest')
44 | .queryParam('query', value)
45 | .queryParam('filters', filters)
46 | .queryParam('queryProfile', 'suggest')
47 | .toString(true),
48 | )
49 | .then(
50 | (response) =>
51 | !cancelled &&
52 | setSuggestions(
53 | (response?.root?.children ?? []).map((item) => ({
54 | value: item.fields.term,
55 | type: item.fields.type,
56 | url: item.fields.url,
57 | })),
58 | ),
59 | )
60 | .catch(() => !cancelled && setSuggestions([]));
61 |
62 | return () => (cancelled = false);
63 | }, [filters, value]);
64 |
65 | const onSubmit = ({ value, url }) => {
66 | inputRef.current?.blur();
67 | url ? window.open(url, '_blank').focus() : setQuery(value);
68 | };
69 |
70 | return (
71 |
143 | );
144 | }
145 |
--------------------------------------------------------------------------------
/src/App/pages/search/md-parser.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Lexer, walkTokens } from 'marked';
3 | import {
4 | Blockquote,
5 | Code,
6 | Divider,
7 | Image,
8 | List,
9 | Table,
10 | Text,
11 | Title,
12 | } from '@mantine/core';
13 | import { CodeHighlight } from '@mantine/code-highlight';
14 | import { Link } from 'App/libs/router';
15 | import { LinkReference } from 'App/pages/search/link-reference';
16 | import { fontWeightBold } from 'App/styles/common';
17 |
18 | const refRegex = /^\[([0-9]+)]+/;
19 | const extensions = Object.freeze({
20 | inline: [
21 | (src) => {
22 | const match = refRegex.exec(src);
23 | if (match) return { type: 'ref', raw: match[0], text: match[1] };
24 | },
25 | ],
26 | });
27 |
28 | const convertTokens = ({ tokens }, urlResolver) =>
29 | tokens.map((token, i) => convert(token, `${token.type}-${i}`, urlResolver));
30 |
31 | // Java string hash code
32 | function hashCode(str) {
33 | let h = 0;
34 | for (let i = 0; i < str.length; i++) {
35 | h = (h * 31 + (str.charCodeAt(i) & 0xff)) | 0;
36 | }
37 | return h;
38 | }
39 |
40 | function resolveUrl(url, options) {
41 | if (options.baseUrl)
42 | try {
43 | return new URL(url, options.baseUrl).href;
44 | } catch (err) {
45 | return undefined;
46 | }
47 | return url.includes('://') ? url : undefined;
48 | }
49 |
50 | // https://github.com/markedjs/marked/blob/7c1e114f9f7949ba4033366582d2a4ddf09e85af/src/Tokenizer.js
51 | function convert(token, key, options) {
52 | switch (token.type) {
53 | case 'code':
54 | return (
55 |
61 | );
62 | case 'blockquote':
63 | return (
64 |
65 | {convertTokens(token, options)}
66 |
67 | );
68 | case 'heading':
69 | return (
70 |
71 | {convertTokens(token, options)}
72 |
73 | );
74 | case 'hr':
75 | return ;
76 | case 'list':
77 | return (
78 |
83 | {token.items.map((item, i) => (
84 | {convertTokens(item, options)}
85 | ))}
86 |
87 | );
88 | case 'table':
89 | return (
90 |
103 |
104 |
105 | {token.header.map((cell, i) => (
106 | {convertTokens(cell, options)}
107 | ))}
108 |
109 |
110 |
111 | {token.rows.map((row, i) => (
112 |
113 | {row.map((cell, j) => (
114 | {convertTokens(cell, options)}
115 | ))}
116 |
117 | ))}
118 |
119 |
120 | );
121 |
122 | case 'strong':
123 | return (
124 |
125 | {convertTokens(token, options)}
126 |
127 | );
128 | case 'em':
129 | return (
130 |
131 | {convertTokens(token, options)}
132 |
133 | );
134 | case 'codespan':
135 | return (
136 | {token.raw.substring(1, token.raw.length - 1)}
137 | );
138 | case 'br':
139 | return '\n';
140 | case 'del':
141 | return (
142 |
143 | {convertTokens(token, options)}
144 |
145 | );
146 | case 'link': {
147 | const to = resolveUrl(token.href, options);
148 | if (!to) return convertTokens(token, options);
149 | return (
150 |
151 | {convertTokens(token, options)}
152 |
153 | );
154 | }
155 | case 'image':
156 | return (
157 |
162 | );
163 | case 'paragraph':
164 | return (
165 |
166 | {convertTokens(token, options)}
167 |
168 | );
169 | case 'text':
170 | return token.tokens ? convertTokens(token, options) : token.raw;
171 | case 'ref':
172 | return ['[', , ']'];
173 | default:
174 | case 'html':
175 | case 'space':
176 | return token.raw;
177 | }
178 | }
179 |
180 | export function parseMarkdown(src, options = {}) {
181 | try {
182 | const opt = { extensions, gfm: true };
183 | const tokens = Lexer.lex(src, opt);
184 | return convertTokens({ tokens }, options);
185 | } catch (e) {
186 | console.error(e);
187 | return src;
188 | }
189 | }
190 |
191 | export function parseTokens(src) {
192 | try {
193 | const opt = { extensions, gfm: true };
194 | const tokens = Lexer.lex(src, opt);
195 | return walkTokens(tokens, (t) => t);
196 | } catch (e) {
197 | console.error(e);
198 | return src;
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/src/App/styles/theme/colors/index.js:
--------------------------------------------------------------------------------
1 | // prettier-ignore
2 | export const light = {
3 | sage: ['#fbfdfc', '#f7f9f8', '#eef1f0', '#e6e9e8', '#dfe2e0', '#d7dad9', '#cbcfcd', '#b8bcba', '#868e8b', '#7c8481', '#5f6563', '#1a211e'],
4 | gray: ['#fcfcfc', '#f9f9f9', '#f1f1f1', '#ebebeb', '#e4e4e4', '#dddddd', '#d4d4d4', '#bbbbbb', '#8d8d8d', '#808080', '#646464', '#202020'],
5 | mauve: ['#fdfcfd', '#faf9fb', '#f3f1f5', '#eceaef', '#e6e3e9', '#dfdce3', '#d5d3db', '#bcbac7', '#8e8c99', '#817f8b', '#65636d', '#211f26'],
6 | bronze: ['#fdfcfc', '#fdf8f6', '#f8f1ee', '#f2e8e4', '#eaddd7', '#e0cec7', '#d1b9b0', '#bfa094', '#a18072', '#947467', '#7d5e54', '#43302b'],
7 | brown: ['#fefdfc', '#fcf9f6', '#f8f1ea', '#f4e9dd', '#efddcc', '#e8cdb5', '#ddb896', '#d09e72', '#ad7f58', '#9e7352', '#815e46', '#3e332e'],
8 | yellow: ['#fdfdf9', '#fffbe0', '#fff8c6', '#fcf3af', '#f7ea9b', '#ecdd85', '#dac56e', '#c9aa45', '#fbe32d', '#f9da10', '#775f28', '#473b1f'],
9 | amber: ['#fefdfb', '#fff9ed', '#fff3d0', '#ffecb7', '#ffe0a1', '#f5d08c', '#e4bb78', '#d6a35c', '#ffc53d', '#ffba1a', '#915930', '#4f3422'],
10 | orange: ['#fefcfb', '#fff8f4', '#ffedd5', '#ffe0bb', '#ffd3a4', '#ffc291', '#ffaa7d', '#ed8a5c', '#f76808', '#ed5f00', '#99543a', '#582d1d'],
11 | tomato: ['#fffcfc', '#fff8f7', '#fff0ee', '#ffe6e2', '#fdd8d3', '#fac7be', '#f3b0a2', '#ea9280', '#e54d2e', '#d84224', '#c33113', '#5c271f'],
12 | red: ['#fffcfc', '#fff7f7', '#ffefef', '#ffe5e5', '#fdd8d8', '#f9c6c6', '#f3aeaf', '#eb9091', '#e5484d', '#d93d42', '#c62a2f', '#641723'],
13 | ruby: ['#fffcfd', '#fff7f9', '#feeff3', '#fde5ea', '#fad8e0', '#f5c7d1', '#eeafbc', '#e592a2', '#e54666', '#da3a5c', '#ca244d', '#64172b'],
14 | crimson: ['#fffcfd', '#fff7fb', '#feeff6', '#fce5f0', '#f9d8e7', '#f4c6db', '#edadc8', '#e58fb1', '#e93d82', '#dc3175', '#cb1d63', '#621639'],
15 | pink: ['#fffcfe', '#fff7fc', '#feeef8', '#fce5f3', '#f9d8ec', '#f3c6e2', '#ecadd4', '#e38ec3', '#d6409f', '#cd3093', '#c41c87', '#651249'],
16 | grape: ['#fefcff', '#fff8ff', '#fceffc', '#f9e5f9', '#f3d9f4', '#ebc8ed', '#dfafe3', '#cf91d8', '#ab4aba', '#a43cb4', '#9c2bad', '#53195d'],
17 | purple: ['#fefcfe', '#fdfaff', '#f9f1fe', '#f3e7fc', '#eddbf9', '#e3ccf4', '#d3b4ed', '#be93e4', '#8e4ec6', '#8445bc', '#793aaf', '#402060'],
18 | violet: ['#fdfcfe', '#fbfaff', '#f5f2ff', '#ede9fe', '#e4defc', '#d7cff9', '#c4b8f3', '#aa99ec', '#6e56cf', '#644fc1', '#5746af', '#2f265f'],
19 | iris: ['#fdfdff', '#fafaff', '#f3f3ff', '#ebebfe', '#e0e0fd', '#d0d0fa', '#babbf5', '#9b9ef0', '#5b5bd6', '#5353ce', '#4747c2', '#272962'],
20 | indigo: ['#fdfdfe', '#f8faff', '#f0f4ff', '#e6edfe', '#d9e2fc', '#c6d4f9', '#aec0f5', '#8da4ef', '#3e63dd', '#3a5ccc', '#3451b2', '#1f2d5c'],
21 | blue: ['#fbfdff', '#f5faff', '#edf6ff', '#e1f0ff', '#cee7fe', '#b7d9f8', '#96c7f2', '#5eb0ef', '#0091ff', '#0880ea', '#0b68cb', '#113264'],
22 | cyan: ['#fafdfe', '#f2fcfd', '#e7f9fb', '#d8f3f6', '#c4eaef', '#aadee6', '#84cdda', '#3db9cf', '#05a2c2', '#0894b3', '#0c7792', '#0d3c48'],
23 | teal: ['#fafefd', '#f1fcfa', '#e7f9f5', '#d9f3ee', '#c7ebe5', '#afdfd7', '#8dcec3', '#53b9ab', '#12a594', '#0e9888', '#067a6f', '#0d3d38'],
24 | jade: ['#fbfefd', '#effdf6', '#e4faef', '#d7f4e6', '#c6ecdb', '#b0e0cc', '#8fcfb9', '#56ba9f', '#29a383', '#259678', '#1a7a5e', '#1d3b31'],
25 | green: ['#fdfefc', '#f4fbf7', '#e6f6ed', '#d6f1e1', '#c4e8d3', '#adddc1', '#8ecea8', '#5bb982', '#61d790', '#50d386', '#218349', '#193b27'],
26 | grass: ['#fbfefb', '#f3fcf3', '#ebf9eb', '#dff3df', '#ceebcf', '#b7dfba', '#97cf9c', '#65ba75', '#46a758', '#3d9a50', '#297c3b', '#203c25'],
27 | lime: ['#fcfdfa', '#f7fcf0', '#edfada', '#e2f5c4', '#d5edaf', '#c6de99', '#b2ca7f', '#9ab654', '#bdee63', '#b0e64c', '#59682c', '#37401c'],
28 | mint: ['#f9fefd', '#effefa', '#ddfbf3', '#ccf7ec', '#bbeee2', '#a6e1d3', '#87d0bf', '#51bda7', '#86ead4', '#7fe1cc', '#27756a', '#16433c'],
29 | sky: ['#f9feff', '#f1fcff', '#e2f9ff', '#d2f4fd', '#bfebf8', '#a5dced', '#82cae0', '#46b8d8', '#7ce2fe', '#72dbf8', '#256e93', '#19404d'],
30 | };
31 |
32 | // prettier-ignore
33 | export const dark = {
34 | sage: ['#101211', '#171918', '#202221', '#272a29', '#2e3130', '#373b39', '#444947', '#5b625f', '#63706b', '#717d79', '#adb5b2', '#eceeed'],
35 | gray: ['#181818', '#1b1b1b', '#282828', '#303030', '#373737', '#3f3f3f', '#4a4a4a', '#606060', '#6e6e6e', '#818181', '#b1b1b1', '#eeeeee'],
36 | mauve: ['#191719', '#1e1a1e', '#2b272c', '#332f35', '#3a363c', '#423e45', '#4d4951', '#625f69', '#6f6d78', '#82808b', '#b1afb8', '#eeeef0'],
37 | bronze: ['#191514', '#1c1918', '#272220', '#302926', '#382f2c', '#463a35', '#5d4c45', '#8d7266', '#a18072', '#b39283', '#d4b3a5', '#ede0d9'],
38 | brown: ['#191513', '#1e1a17', '#29221d', '#312821', '#3b2f26', '#48392d', '#614c3a', '#937153', '#ad7f58', '#bd926c', '#dbb594', '#f2e1ca'],
39 | yellow: ['#1c1500', '#221a04', '#2c230a', '#342a0e', '#3d3211', '#493d14', '#615119', '#8f7d24', '#fbe32d', '#fcea5c', '#ffee33', '#fff5ad'],
40 | amber: ['#1f1300', '#251804', '#30200b', '#39270f', '#432e12', '#533916', '#6f4d1d', '#a9762a', '#ffc53d', '#ffcb47', '#ffcc4d', '#ffe7b3'],
41 | orange: ['#1f1206', '#271504', '#341c0a', '#3f220d', '#4b2910', '#5d3213', '#7e4318', '#c36522', '#f76808', '#ff802b', '#ffa366', '#ffe0c2'],
42 | tomato: ['#1d1412', '#291612', '#3b1a14', '#471d16', '#532017', '#652318', '#862919', '#ca3416', '#e54d2e', '#f46d52', '#ff8870', '#fbd3cb'],
43 | red: ['#1f1315', '#291618', '#3b191d', '#481a20', '#551c22', '#691d25', '#8c1d28', '#d21e24', '#e5484d', '#f26669', '#ff8589', '#ffd1d9'],
44 | ruby: ['#1f1417', '#2a1519', '#3b181f', '#471a23', '#531b27', '#661d2c', '#8a1e34', '#d01b3f', '#e54666', '#f2657e', '#ff859d', '#fed2e1'],
45 | crimson: ['#1d1418', '#29151d', '#391826', '#441a2b', '#511c31', '#641e3a', '#881f49', '#cf1761', '#e93d82', '#f46396', '#ff85ab', '#fdd3e8'],
46 | pink: ['#1f121b', '#291523', '#37192e', '#411c35', '#4b1f3d', '#5d224a', '#7c2860', '#bc2f88', '#d6409f', '#e45eaf', '#f986c9', '#fdd1ea'],
47 | grape: ['#19141b', '#22141f', '#30152b', '#3a1633', '#43173b', '#551942', '#701a51', '#af1b72', '#d31c86', '#de3e9e', '#f769b5', '#fd9ed1'],
48 | purple: ['#1c141f', '#241526', '#301538', '#36173f', '#3d1947', '#4c1c54', '#641e6b', '#93299f', '#a33cab', '#b44db4', '#c96fcb', '#e7a5e4'],
49 | violet: ['#17151f', '#1c172b', '#271f3f', '#2d254c', '#342a58', '#3d316a', '#4c3e89', '#6654c0', '#6e56cf', '#836add', '#b399ff', '#e2ddfe'],
50 | iris: ['#151521', '#19182d', '#222040', '#27264d', '#2d2c59', '#33336b', '#404089', '#5858c0', '#5b5bd6', '#6f6de2', '#a19eff', '#e0dffe'],
51 | indigo: ['#131620', '#15192d', '#1a2242', '#1e284f', '#202d5c', '#24366e', '#2c438f', '#3b5dce', '#3e63dd', '#5c73e7', '#99a2ff', '#dddffe'],
52 | blue: ['#0f1720', '#0f1b2d', '#11253f', '#122b4c', '#12325a', '#123d6f', '#0f5096', '#1276e2', '#0091ff', '#3cabff', '#6bc1ff', '#c2e5ff'],
53 | cyan: ['#07191d', '#0b1d22', '#0f272e', '#112f37', '#143741', '#17444f', '#1d5b6a', '#28879f', '#05a2c2', '#13b7d8', '#20d0f3', '#b6ecf7'],
54 | teal: ['#091a16', '#091f1a', '#0d2923', '#0f312b', '#123a32', '#16463d', '#1b5e54', '#238b7f', '#12a594', '#0abba4', '#0bd8b6', '#adf0dd'],
55 | jade: ['#081911', '#0b1f16', '#0f291e', '#123124', '#143a2b', '#184635', '#1e5e48', '#238b6f', '#29a383', '#25ba92', '#1fd8a4', '#adf0d4'],
56 | green: ['#0e1511', '#121b16', '#132d1e', '#113b22', '#17492b', '#205737', '#286843', '#2f7c4e', '#61d790', '#81dfa6', '#91e8b5', '#dcf9e8'],
57 | grass: ['#0d1912', '#131d16', '#18281d', '#1b3021', '#1e3926', '#24452d', '#2d5d39', '#428a4f', '#46a758', '#5cbc6e', '#71d083', '#c2f0c2'],
58 | lime: ['#141807', '#181d0c', '#1f2711', '#252f14', '#2c3717', '#36431b', '#485921', '#70862d', '#bdee63', '#c4f042', '#bbd926', '#e3f7ba'],
59 | mint: ['#081917', '#0a1f1d', '#0d2927', '#0e322e', '#103b36', '#134842', '#186057', '#248f7d', '#86ead4', '#95f3d9', '#49dfbe', '#c4f5e1'],
60 | sky: ['#0c1820', '#0d1d26', '#112733', '#132f3d', '#163648', '#1a4358', '#205975', '#2d87b4', '#7ce2fe', '#8ae8ff', '#52d4ff', '#c2f3ff'],
61 | };
62 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/src/App/pages/testcomp/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Accordion,
4 | ActionIcon,
5 | Autocomplete,
6 | Badge,
7 | Button,
8 | Checkbox,
9 | Chip,
10 | CloseButton,
11 | Container,
12 | Divider,
13 | FileInput,
14 | Group,
15 | HoverCard,
16 | Input,
17 | JsonInput,
18 | Rating,
19 | SegmentedControl,
20 | Select,
21 | Skeleton,
22 | Slider,
23 | Space,
24 | Stack,
25 | Switch,
26 | Tabs,
27 | Text,
28 | TextInput,
29 | } from '@mantine/core';
30 | import { CodeHighlight } from '@mantine/code-highlight';
31 | import { Content, Icon } from 'App/components';
32 |
33 | function DemoActionIcon() {
34 | return (
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | );
56 | }
57 |
58 | function DemoButton() {
59 | return (
60 |
61 | }
65 | >
66 | transparent
67 |
68 | }>
69 | subtle
70 |
71 | }>
72 | default
73 |
74 | }>
75 | outline
76 |
77 | }>
78 | filled
79 |
80 | }>
81 | light
82 |
83 | }>
84 | button
85 |
86 |
87 | );
88 | }
89 |
90 | function DemoButtonGradients() {
91 | return (
92 |
93 |
94 | Indigo cyan
95 |
96 |
100 | Lime green
101 |
102 |
106 | Teal blue
107 |
108 |
109 | Orange red
110 |
111 |
115 | Peach
116 |
117 |
118 | );
119 | }
120 |
121 | function DemoCloseButton() {
122 | return (
123 |
124 |
125 |
126 |
127 | );
128 | }
129 |
130 | function DemoAutocomplete() {
131 | return (
132 |
137 | );
138 | }
139 |
140 | function DemoCheckBoxes() {
141 | return (
142 | <>
143 | {}} checked={false} label="Default checkbox" />
144 | {}}
146 | checked={false}
147 | indeterminate
148 | label="Indeterminate checkbox"
149 | />
150 | {}}
152 | checked
153 | indeterminate
154 | label="Indeterminate checked checkbox"
155 | />
156 | {}} checked label="Checked checkbox" />
157 | {}} disabled label="Disabled checkbox" />
158 | {}}
160 | disabled
161 | checked
162 | label="Disabled checked checkbox"
163 | />
164 |
169 | >
170 | );
171 | }
172 |
173 | function DemoChips() {
174 | return (
175 |
176 |
177 | Awesome chip
178 |
179 |
180 | Awesome chip
181 |
182 |
183 | Awesome chip
184 |
185 |
186 | );
187 | }
188 |
189 | function DemoFileInput() {
190 | return ;
191 | }
192 |
193 | function DemoInput() {
194 | return } placeholder="Your email" />;
195 | }
196 |
197 | function DemoJsonInput() {
198 | return (
199 |
207 | );
208 | }
209 |
210 | function DemoRating() {
211 | return ;
212 | }
213 |
214 | function DemoSegmentedControl() {
215 | return (
216 |
220 | );
221 | }
222 |
223 | function DemoSelect() {
224 | return (
225 |
235 | );
236 | }
237 |
238 | function DemoSlider() {
239 | return (
240 | <>
241 |
242 |
249 |
250 | >
251 | );
252 | }
253 |
254 | function DemoSwitch() {
255 | return ;
256 | }
257 |
258 | function DemoTextInput() {
259 | return (
260 | <>
261 |
262 |
263 | >
264 | );
265 | }
266 |
267 | function DemoTabs() {
268 | return (
269 | <>
270 |
271 |
272 |
273 | }>
274 | Gallery
275 |
276 | }>
277 | Messages
278 |
279 | }>
280 | Settings
281 |
282 |
283 |
284 |
285 | Gallery tab content
286 |
287 |
288 |
289 | Messages tab content
290 |
291 |
292 |
293 | Settings tab content
294 |
295 |
296 |
297 | >
298 | );
299 | }
300 |
301 | function DemoAccordion() {
302 | return (
303 |
304 |
305 | Customization
306 |
307 | Colors, fonts, shadows and many other parts are customizable to fit
308 | your design needs
309 |
310 |
311 |
312 | Flexibility
313 |
314 | Configure components appearance and behavior with vast amount of
315 | settings or overwrite any part of component styles
316 |
317 |
318 |
319 | No annoying focus ring
320 |
321 | With new :focus-visible pseudo-class focus ring appears only when user
322 | navigates with keyboard
323 |
324 |
325 |
326 | );
327 | }
328 |
329 | function DemoBadge() {
330 | return (
331 |
332 | light
333 | filled
334 | outline
335 | dot
336 |
337 | );
338 | }
339 |
340 | function DemoBadgeGradients() {
341 | return (
342 |
343 |
344 | Indigo cyan
345 |
346 |
350 | Lime green
351 |
352 |
356 | Teal blue
357 |
358 |
359 | Orange red
360 |
361 |
365 | Peach
366 |
367 |
368 | );
369 | }
370 |
371 | function DemoCodeHighlight() {
372 | const demoCode = `import { Button } from '@mantine/core';
373 |
374 | function Demo() {
375 | return
376 | <>
377 | Hello
378 | // Hello
379 | >
380 | }`;
381 |
382 | return (
383 |
384 |
385 |
386 |
387 | );
388 | }
389 |
390 | function DemoSkeleton() {
391 | return (
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 | );
402 | }
403 |
404 | function DemoMiscs() {
405 | return (
406 |
407 | text
408 | text
409 |
410 | text
411 |
412 |
413 | text
414 |
415 |
416 |
417 |
418 |
419 | Hover to reveal the card
420 |
421 |
422 |
423 | Hover card is revealed when user hovers over target element, it will
424 | be hidden once mouse is not over both target and dropdown elements
425 |
426 |
427 |
428 |
433 | Hover card is revealed when user hovers over target element, it will be
434 | hidden once mouse is not over both target and dropdown elements
435 |
436 |
441 | Hover card is revealed when user hovers over target element, it will be
442 | hidden once mouse is not over both target and dropdown elements
443 |
444 |
449 | Hover card is revealed when user hovers over target element, it will be
450 | hidden once mouse is not over both target and dropdown elements
451 |
452 |
457 | Hover card is revealed when user hovers over target element, it will be
458 | hidden once mouse is not over both target and dropdown elements
459 |
460 |
461 |
467 | getAppBackground
468 | {' '}
469 |
475 | getAppBackground
476 |
477 |
483 | getSubtleBackground
484 | {' '}
485 |
491 | getSubtleBackground
492 |
493 |
499 | getUiElementBackground
500 | {' '}
501 |
507 | getUiElementBackground
508 |
509 |
515 | getHoveredUiElementBackground
516 | {' '}
517 |
523 | getHoveredUiElementBackground
524 |
525 |
531 | getSubtleBorderAndSeparator
532 | {' '}
533 |
539 | getSubtleBorderAndSeparator
540 |
541 |
547 | getUiElementBorderAndFocus
548 | {' '}
549 |
555 | getUiElementBorderAndFocus
556 |
557 |
563 | getSolidBackground
564 | {' '}
565 |
571 | getSolidBackground
572 |
573 |
579 | getHoveredSolidBackground
580 | {' '}
581 |
587 | getHoveredSolidBackground
588 |
589 |
595 | getLowContrastText
596 | {' '}
597 |
603 | getLowContrastText
604 |
605 |
611 | getHighContrastText
612 | {' '}
613 |
619 | getHighContrastText
620 |
621 |
622 | );
623 | }
624 |
625 | export function Testcomp() {
626 | return (
627 |
628 |
629 |
630 |
631 |
632 |
633 |
634 |
635 |
636 |
637 |
638 |
639 |
640 |
641 |
642 |
643 |
644 |
645 |
646 |
647 |
648 |
649 |
650 |
651 |
652 |
653 |
654 | );
655 | }
656 |
--------------------------------------------------------------------------------