💄(frontend) update interlinking ux/ui

Update interlinking to fit the new design.
The notable changes is that we cannot create
a subdoc from the search dropdown.
This commit is contained in:
Anthony LC
2026-04-15 13:11:24 +02:00
parent b3dd8f2e39
commit c20e71e21d
7 changed files with 80 additions and 108 deletions

View File

@@ -52,28 +52,6 @@ test.describe('Doc Create', () => {
).toBeVisible();
});
test('it creates a sub doc from interlinking dropdown', async ({
page,
browserName,
}) => {
const [title] = await createDoc(page, 'my-new-slash-doc', browserName, 1);
await verifyDocName(page, title);
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Link a doc').first().click();
await page
.locator('.quick-search-container')
.getByText('New sub-doc')
.click();
const input = page.getByRole('textbox', { name: 'Document title' });
await expect(input).toHaveText('', { timeout: 10000 });
await expect(
page.locator('.c__tree-view--row-content').getByText('Untitled document'),
).toBeVisible();
});
test('it creates a doc with link "/doc/new/', async ({
page,
browserName,

View File

@@ -1,5 +1,5 @@
import { Command } from 'cmdk';
import { PropsWithChildren, ReactNode, useId, useRef, useState } from 'react';
import { PropsWithChildren, ReactNode, useId, useRef } from 'react';
import { hasChildrens } from '@/utils/children';
@@ -24,6 +24,7 @@ export type QuickSearchData<T> = {
};
export type QuickSearchProps = {
isSelectByDefault?: boolean;
onFilter?: (str: string) => void;
inputValue?: string;
inputContent?: ReactNode;
@@ -36,6 +37,7 @@ export type QuickSearchProps = {
};
export const QuickSearch = ({
isSelectByDefault,
onFilter,
inputContent,
inputValue,
@@ -47,13 +49,6 @@ export const QuickSearch = ({
}: PropsWithChildren<QuickSearchProps>) => {
const ref = useRef<HTMLDivElement | null>(null);
const listId = useId();
/**
* Hack to prevent cmdk from auto-selecting the first element on open
*
* TODO: Find a clean solution to prevent cmdk from auto-selecting
* the first element on open
*/
const [selectedValue, _] = useState('__none__');
return (
<>
@@ -65,7 +60,7 @@ export const QuickSearch = ({
ref={ref}
tabIndex={-1}
disablePointerSelection
value={selectedValue}
value={!isSelectByDefault ? '__none__' : undefined}
>
{showInput && (
<QuickSearchInput

View File

@@ -19,7 +19,13 @@ export const QuickSearchGroup = <T,>({
}: Props<T>) => {
return (
<Box>
<Text as="h2" $weight="700" $size="sm" $margin="none">
<Text
className="--docs--quick-search-group-title"
as="h2"
$weight="700"
$size="sm"
$margin="none"
>
{group.groupName}
</Text>
<Command.Group
@@ -61,7 +67,11 @@ export const QuickSearchGroup = <T,>({
);
})}
{group.emptyString && group.elements.length === 0 && (
<Text $margin={{ left: '2xs', bottom: '3xs' }} $size="sm">
<Text
className="--docs--quick-search-group-empty"
$margin={{ left: '2xs', bottom: '3xs' }}
$size="sm"
>
{group.emptyString}
</Text>
)}

View File

@@ -9,8 +9,7 @@ import { useEffect } from 'react';
import { css } from 'styled-components';
import { validate as uuidValidate } from 'uuid';
import { BoxButton, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { Box, BoxButton, Text } from '@/components';
import SelectedPageIcon from '@/docs/doc-editor/assets/doc-selected.svg';
import { getEmojiAndTitle, useDoc } from '@/docs/doc-management/';
@@ -120,8 +119,6 @@ export const LinkSelected = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [doc?.title, docId, isEditable]);
const { colorsTokens } = useCunninghamTheme();
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(title);
const router = useRouter();
const href = `/docs/${docId}/`;
@@ -154,6 +151,7 @@ export const LinkSelected = ({
onClick={handleClick}
onAuxClick={handleAuxClick}
draggable="false"
$height="28px"
$css={css`
display: inline;
padding: 0.1rem 0.4rem;
@@ -179,18 +177,38 @@ export const LinkSelected = ({
{emoji ? (
<Text $size="16px">{emoji}</Text>
) : (
<SelectedPageIcon width={11.5} color={colorsTokens['brand-400']} />
<SelectedPageIcon
width={11.5}
color="var(--c--contextuals--content--semantic--brand--tertiary)"
/>
)}
<Text
$weight="500"
spellCheck="false"
$size="16px"
$display="inline"
$position="relative"
$css={css`
margin-left: 2px;
`}
>
<Box
className="--docs-interlinking-underline"
as="span"
$height="1px"
$width="100%"
$background="var(--c--contextuals--border--semantic--neutral--tertiary)"
$position="absolute"
$hasTransition
$radius="2px"
$css={css`
left: 0;
bottom: 0px;
`}
/>
<Box as="span" $zIndex="1" $position="relative">
{titleWithoutEmoji}
</Box>
</Text>
</BoxButton>
);

View File

@@ -15,30 +15,21 @@ import {
Card,
Icon,
QuickSearch,
QuickSearchGroup,
QuickSearchItemContent,
Text,
} from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import {
DocsBlockSchema,
DocsInlineContentSchema,
DocsStyleSchema,
} from '@/docs/doc-editor';
import FoundPageIcon from '@/docs/doc-editor/assets/doc-found.svg';
import AddPageIcon from '@/docs/doc-editor/assets/doc-plus.svg';
import {
Doc,
getEmojiAndTitle,
useCreateChildDocTree,
useDocStore,
useTrans,
} from '@/docs/doc-management';
import { Doc, getEmojiAndTitle, useTrans } from '@/docs/doc-management';
import { DocSearchContent, DocSearchTarget } from '@/docs/doc-search';
import { useResponsiveStore } from '@/stores';
const inputStyle = css`
background-color: var(--c--globals--colors--gray-100);
background-color: transparent;
border: none;
outline: none;
color: var(--c--globals--colors--gray-700);
@@ -76,15 +67,12 @@ export const SearchPage = ({
trigger,
updateInlineContent,
}: SearchPageProps) => {
const { colorsTokens } = useCunninghamTheme();
const editor = useBlockNoteEditor<
DocsBlockSchema,
DocsInlineContentSchema,
DocsStyleSchema
>();
const { t } = useTranslation();
const { currentDoc } = useDocStore();
const createChildDoc = useCreateChildDocTree(currentDoc?.id);
const inputRef = useRef<HTMLInputElement>(null);
const [search, setSearch] = useState('');
const { isDesktop } = useResponsiveStore();
@@ -174,11 +162,11 @@ export const SearchPage = ({
<Box
as="span"
className="inline-content"
$background={colorsTokens['gray-100']}
$color="var(--c--globals--colors--gray-700)"
$background="var(--c--contextuals--background--semantic--overlay--primary)"
$color="var(--c--contextuals--content--semantic--neutral--primary)"
$direction="row"
$radius="3px"
$padding="1px"
$padding="2px"
$display="inline-flex"
tabIndex={-1} // Ensure the span is focusable
>
@@ -196,6 +184,7 @@ export const SearchPage = ({
aria-autocomplete="list"
aria-controls={dropdownId}
$padding={{ left: '3px' }}
placeholder={t('mention a sub-doc...')}
$css={inputStyle}
ref={inputRef}
$display="inline-flex"
@@ -229,13 +218,22 @@ export const SearchPage = ({
& .quick-search-container [cmdk-root] {
border-radius: inherit;
background: transparent;
}
`}
>
<QuickSearch showInput={false}>
<QuickSearch showInput={false} isSelectByDefault>
<Card
$css={css`
box-shadow: 0 0 3px 0px var(--c--globals--colors--gray-200);
box-shadow: 0 0 6px 0 rgba(0, 0, 145, 0.1);
border: 1px solid
var(--c--contextuals--border--surface--primary);
background: var(
--c--contextuals--background--surface--primary
);
.quick-search-container & [cmdk-group] {
margin-top: 0 !important;
}
& > div {
margin-top: var(--c--globals--spacings--0);
& [cmdk-group-heading] {
@@ -257,15 +255,27 @@ export const SearchPage = ({
& .--docs--doc-search-item > div {
gap: 0.8rem;
}
& .--docs--quick-search-group-title {
font-size: 12px;
margin: var(--c--globals--spacings--sm);
margin-bottom: var(--c--globals--spacings--xxs);
}
& .--docs--quick-search-group-empty {
margin: var(--c--globals--spacings--sm);
}
}
`}
$margin={{ top: '0.5rem' }}
$margin="sm"
$padding="none"
>
<DocSearchContent
groupName={t('Select a document')}
groupName={t('Link a doc')}
search={search}
target={DocSearchTarget.CURRENT}
parentPath={treeContext?.root?.path}
isSearchNotMandatory
onSelect={(doc) => {
if (!isEditable) {
return;
@@ -343,52 +353,6 @@ export const SearchPage = ({
);
}}
/>
<QuickSearchGroup
group={{
groupName: '',
elements: [],
endActions: [
{
onSelect: createChildDoc,
content: (
<Box
$css={css`
border-top: 1px solid
var(--c--globals--colors--gray-200);
`}
$width="100%"
>
<Box
$direction="row"
$gap="0.4rem"
$align="center"
$padding={{
vertical: '0.5rem',
horizontal: '0.3rem',
}}
$css={css`
&:hover {
background-color: var(
--c--globals--colors--gray-100
);
}
`}
>
<AddPageIcon />
<Text
$size="sm"
$color="var(--c--globals--colors--gray-1000)"
contentEditable={false}
>
{t('New sub-doc')}
</Text>
</Box>
</Box>
),
},
],
}}
/>
</Card>
</QuickSearch>
</Box>

View File

@@ -17,6 +17,7 @@ type DocSearchContentProps = {
search: string;
filterResults?: (doc: Doc) => boolean;
isSearchNotMandatory?: boolean;
onResults?: (results: Doc[]) => void;
onSelect: (doc: Doc) => void;
onLoadingChange?: (loading: boolean) => void;
target?: DocSearchTarget;
@@ -28,6 +29,7 @@ export const DocSearchContent = ({
groupName,
search,
filterResults,
onResults,
onSelect,
onLoadingChange,
renderSearchElement,
@@ -76,8 +78,10 @@ export const DocSearchContent = ({
const elements = search || isSearchNotMandatory ? docs : [];
onResults?.(elements);
setDocsData({
groupName: docs.length > 0 ? groupName : '',
groupName: groupName,
groupKey: 'docs',
elements,
emptyString: t('No document found'),
@@ -109,6 +113,7 @@ export const DocSearchContent = ({
loading,
hasNextPage,
fetchNextPage,
onResults,
]);
useEffect(() => {

View File

@@ -36,6 +36,7 @@ const DocSearchModalGlobal = ({
}: DocSearchModalGlobalProps) => {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [results, setResults] = useState<Doc[]>([]);
const restoreFocus = useFocusStore((state) => state.restoreFocus);
const router = useRouter();
const [search, setSearch] = useState('');
@@ -120,9 +121,10 @@ const DocSearchModalGlobal = ({
)}
{search && (
<DocSearchContent
groupName={t('Select a document')}
groupName={results.length ? t('Select a document') : ''}
search={search}
onSelect={handleSelect}
onResults={setResults}
onLoadingChange={setLoading}
target={
filters.target === DocSearchTarget.CURRENT