(ui) fix retour global ui (#35)

correction retour ui
This commit is contained in:
elvoisin
2025-09-22 09:20:07 +02:00
committed by GitHub
parent 9878498a34
commit b4df44311a
15 changed files with 221 additions and 81 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ and this project adheres to
## [Unreleased]
- 🐛(fix) broken staging css
- ✨(ui) fix retour global ui
### Changed
@@ -71,7 +71,7 @@ export const ToastProvider = ({ children }: ToastProviderProps) => {
<Box
$css={`
position: fixed;
top: 20px;
top: 8px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
@@ -28,7 +28,6 @@ export type QuickSearchProps = {
inputValue?: string;
inputContent?: ReactNode;
showInput?: boolean;
loading?: boolean;
label?: string;
placeholder?: string;
children?: ReactNode;
@@ -39,7 +38,6 @@ export const QuickSearch = ({
onClear,
inputContent,
inputValue,
loading,
showInput = true,
label,
placeholder,
@@ -53,7 +51,6 @@ export const QuickSearch = ({
<Command label={label} shouldFilter={false} ref={ref}>
{showInput && (
<QuickSearchInput
loading={loading}
withSeparator={hasChildrens(children)}
inputValue={inputValue}
onFilter={onFilter}
@@ -1,5 +1,5 @@
import { Command } from 'cmdk';
import { ReactNode } from 'react';
import { ReactNode, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { HorizontalSeparator } from '@/components';
@@ -7,10 +7,8 @@ import { useCunninghamTheme } from '@/cunningham';
import { Box } from '../Box';
import { Icon } from '../Icon';
import { Loader } from '../Loader';
type Props = {
loading?: boolean;
inputValue?: string;
onFilter?: (str: string) => void;
placeholder?: string;
@@ -19,7 +17,6 @@ type Props = {
onClear?: () => void;
};
export const QuickSearchInput = ({
loading,
inputValue,
onFilter,
placeholder,
@@ -30,6 +27,37 @@ export const QuickSearchInput = ({
const { t } = useTranslation();
const { spacingsTokens } = useCunninghamTheme();
const [localValue, setLocalValue] = useState(inputValue || '');
const debouncedFilter = useCallback(
(value: string) => {
let timeoutId: NodeJS.Timeout;
const debounce = () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
onFilter?.(value);
}, 150);
};
debounce();
},
[onFilter],
);
const handleChange = useCallback(
(value: string) => {
setLocalValue(value);
debouncedFilter(value);
},
[debouncedFilter],
);
const handleClear = useCallback(() => {
setLocalValue('');
onClear?.();
}, [onClear]);
if (children) {
return (
<>
@@ -47,12 +75,13 @@ export const QuickSearchInput = ({
$gap={spacingsTokens['2xs']}
$css={`
position: relative;
justify-content: space-between;
border-radius: 4px;
margin: 12px 12px 0 12px;
border: 1px solid #CFD5DE;
background-color: #EEF1F4;
background-color: #eff1f5;
padding: 6px 8px;
&:focus-within {
&:focus-within, &:hover {
border: 2px solid #3E5DE7;
padding: 5px 7px;
}
@@ -69,29 +98,42 @@ export const QuickSearchInput = ({
}
`}
>
{!loading && <Icon iconName="search" $variation="600" />}
{loading && <Loader />}
<Command.Input
aria-label={t('Quick search input')}
value={inputValue}
role="combobox"
placeholder={placeholder ?? t('Search')}
onValueChange={onFilter}
/>
<Icon
iconName="close"
$size="sm"
$variation="600"
$css={`cursor: pointer;
position: absolute;
opacity: ${inputValue && inputValue.length > 0 && onClear ? 1 : 0};
transition: opacity 0.3s cubic-bezier(1, 0, 0, 1);
top: 10px;
right: 10px;
z-index: 1000;`}
onClick={onClear}
<Box $direction="row" $gap="6px">
<Icon iconName="search" $variation="600" />
<Command.Input
aria-label={t('Quick search input')}
value={localValue}
role="combobox"
placeholder={placeholder ?? t('Search')}
onValueChange={handleChange}
/>
</Box>
<Box
onClick={handleClear}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleClear();
}
}}
role="button"
tabIndex={0}
aria-label={t('Clear search')}
/>
$css={`cursor: pointer;
opacity: ${localValue && localValue.length > 0 && onClear ? 1 : 0};
transition: opacity 0.3s cubic-bezier(1, 0, 0, 1);
right: 10px;
border-radius: 4px;
padding: 7px;
background-color: #e4e6ea;
z-index: 1000;
&:focus {
outline: 2px solid #3E5DE7;
outline-offset: 2px;
}`}
>
<Icon iconName="close" $size="15px" $variation="600" />
</Box>
</Box>
</>
);
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { BoxButton, Icon } from '@/components';
import { useResponsiveStore } from '@/stores';
import ProConnectImg from '../assets/button-proconnect.svg';
import { useAuth } from '../hooks';
@@ -11,6 +12,7 @@ import { gotoLogin, gotoLogout } from '../utils';
export const ButtonLogin = () => {
const { t } = useTranslation();
const { authenticated } = useAuth();
const { isDesktop } = useResponsiveStore();
if (!authenticated) {
return (
@@ -29,7 +31,7 @@ export const ButtonLogin = () => {
<Button
onClick={gotoLogout}
color="primary-text"
icon={<Icon iconName="logout" $theme="primary" />}
icon={!isDesktop ? <Icon iconName="logout" $theme="primary" /> : ''}
aria-label={t('Logout')}
className="--docs--button-logout"
>
@@ -564,7 +564,7 @@ export const Chat = ({
$direction="row"
$align="center"
$gap="4px"
className="c__button--neutral"
className="c__button--neutral action-chat-button"
onClick={() => copyToClipboard(message.content)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
@@ -594,7 +594,7 @@ export const Chat = ({
$direction="row"
$align="center"
$gap="4px"
className="c__button--neutral"
className="c__button--neutral action-chat-button"
onClick={() => openSources(message.id)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
@@ -660,7 +660,7 @@ export const Chat = ({
<Box
$css={`
position: relative;
bottom: 20px;
bottom: ${isMobile ? '8px' : '20px'};
margin: auto;
z-index: 1000;
`}
@@ -40,27 +40,57 @@ export const InputChat = ({
}: InputChatProps) => {
const { t } = useTranslation();
const fileInputRef = useRef<HTMLInputElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [isDragActive, setIsDragActive] = useState(false);
const { isDesktop, isMobile } = useResponsiveStore();
const [currentSuggestionIndex, setCurrentSuggestionIndex] = useState(0);
const [isResetting, setIsResetting] = useState(false);
const suggestions = [
'Ask a question',
'Turn this list into bullet points',
'Write a short product description',
'Find recent news about...',
t('Ask a question'),
t('Turn this list into bullet points'),
t('Write a short product description'),
t('Find recent news about...'),
];
useEffect(() => {
if (messagesLength === 0) {
const interval = setInterval(() => {
setCurrentSuggestionIndex((prev) => (prev + 1) % suggestions.length);
setCurrentSuggestionIndex((prev) => {
if (prev === suggestions.length - 1) {
return suggestions.length;
}
return prev + 1;
});
}, 3000);
return () => clearInterval(interval);
}
}, [messagesLength, suggestions.length]);
useEffect(() => {
if (currentSuggestionIndex === suggestions.length) {
const timeout = setTimeout(() => {
setIsResetting(true);
setCurrentSuggestionIndex(0);
setTimeout(() => setIsResetting(false), 50);
}, 500);
return () => clearTimeout(timeout);
}
}, [currentSuggestionIndex, suggestions.length]);
useEffect(() => {
if (textareaRef.current && messagesLength === 0 && status === 'ready') {
textareaRef.current.focus();
}
}, [messagesLength, status]);
useEffect(() => {
if (textareaRef.current && status === 'ready' && !input) {
textareaRef.current.focus();
}
}, [status, input]);
return (
<Box
$css={`
@@ -155,12 +185,13 @@ export const InputChat = ({
border: ${
isDragActive
? '2px dashed var(--c--theme--colors--primary-400)'
: '1px solid var(--c--theme--colors--greyscale-200)'
: '1px solid var(--c--theme--colors--greyscale-100)'
};
position: relative;
`}
>
<textarea
ref={textareaRef}
value={input ?? ''}
name="inputchat-textarea"
onChange={(e) => {
@@ -169,6 +200,7 @@ export const InputChat = ({
textarea.style.height = 'auto';
const newHeight = Math.min(textarea.scrollHeight, 200);
textarea.style.height = `${newHeight}px`;
textarea.focus();
}}
disabled={status !== 'ready'}
rows={1}
@@ -183,7 +215,7 @@ export const InputChat = ({
minHeight: '60px',
maxHeight: '200px',
overflowY: 'auto',
transition: 'all 0.3s ease',
transition: 'all 0.3s cubic-bezier(1, 0, 0, 1)',
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.ctrlKey && !e.shiftKey) {
@@ -191,6 +223,7 @@ export const InputChat = ({
const textarea = e.target as HTMLTextAreaElement;
textarea.style.height = '0';
e.currentTarget.form?.requestSubmit?.();
textarea.focus();
}
}}
/>
@@ -202,28 +235,39 @@ export const InputChat = ({
top: 1rem;
left: 1.5rem;
right: 1.5rem;
height: 1.5rem;
pointer-events: none;
color: var(--c--theme--colors--greyscale-500);
font-size: 1rem;
font-family: inherit;
line-height: 1.5;
overflow: hidden;
`}
>
{suggestions.map((suggestion, index) => (
<Box
key={index}
$css={`
position: absolute;
top: 0;
left: 0;
right: 0;
opacity: ${index === currentSuggestionIndex ? 1 : 0};
transition: opacity ${index === currentSuggestionIndex ? '0.4s' : '0s'} ease;
`}
>
{suggestion}
</Box>
))}
<Box
$css={`
display: flex;
flex-direction: column;
height: ${(suggestions.length + 1) * 100}%;
transform: translateY(-${currentSuggestionIndex * (100 / (suggestions.length + 1))}%);
transition: ${isResetting ? 'none' : 'transform 0.5s cubic-bezier(0.4, 0, 0.2, 1)'};
`}
>
{[...suggestions, suggestions[0]].map((suggestion, index) => (
<Box
key={index}
$css={`
height: calc(100% / ${suggestions.length + 1});
flex-shrink: 0;
white-space: nowrap;
display: flex;
justify-content: flex-start;
`}
>
{suggestion}
</Box>
))}
</Box>
</Box>
)}
@@ -295,13 +339,13 @@ export const InputChat = ({
type="button"
onClick={() => fileInputRef.current?.click()}
aria-label={t('Add attach file')}
className="c__button--neutral"
className="c__button--neutral attach-file-button"
icon={
<Icon
iconName="attach_file"
$theme="greyscale"
$variation="550"
$size="16px"
$size={`${isMobile ? '24px' : '16px'}`}
/>
}
>
@@ -349,7 +393,6 @@ export const InputChat = ({
$css={`
color: ${forceWebSearch ? 'var(--c--theme--colors--primary-600) !important' : 'var(--c--theme--colors--greyscale-600)'}
`}
$size="24px"
/>
}
>
@@ -384,7 +427,7 @@ export const InputChat = ({
</Text>
<Icon
iconName="close"
$variation="800"
$variation="text"
$theme="primary"
$size="md"
$css={`
@@ -15,6 +15,7 @@ export const ButtonTogglePanel = () => {
aria-label={t('Open the header menu')}
color="primary-text"
icon={<Icon $theme="primary" iconName={isPanelOpen ? 'close' : 'menu'} />}
className="mobile-no-focus"
/>
);
};
@@ -67,7 +67,7 @@ export const Header = () => {
color={colorsTokens['primary-text']}
/>
</StyledLink>
{isDesktop && <Feedback />}
<Box className="c__button--feedback">{isDesktop && <Feedback />}</Box>
</Box>
{!isDesktop ? (
<Box $direction="row" $gap={spacingsTokens['sm']}>
@@ -77,6 +77,7 @@ export const Header = () => {
router.push('/');
togglePanel(false);
}}
className="mobile-no-focus"
aria-label={t('New chat')}
color="primary-text"
icon={<NewChatIcon />}
@@ -22,7 +22,7 @@ export const HomeHeader = () => {
const { t } = useTranslation();
const { themeTokens, spacingsTokens, colorsTokens } = useCunninghamTheme();
const logo = themeTokens.logo;
const { isSmallMobile } = useResponsiveStore();
const { isSmallMobile, isDesktop } = useResponsiveStore();
return (
<Box
@@ -70,7 +70,7 @@ export const HomeHeader = () => {
color={colorsTokens['primary-text']}
/>
</Box>
{!isSmallMobile && <Feedback />}
{isDesktop && <Feedback />}
</Box>
{!isSmallMobile && (
<Box $direction="row" $gap="1rem" $align="center">
@@ -32,15 +32,31 @@ export const ConversationItemActions = ({
<>
<DropdownMenu options={options}>
<Box
role="button"
tabIndex={0}
aria-label={t('Conversation actions')}
aria-haspopup="menu"
aria-expanded="false"
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
// Le DropdownMenu gère l'ouverture
}
}}
$css={css`
display: block;
width: 24px;
height: 24px;
padding: 4px;
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: #e1e3e7 !important;
}
&:focus {
outline: 2px solid #3e5de7;
outline-offset: 2px;
}
`}
>
<Icon
@@ -51,6 +67,7 @@ export const ConversationItemActions = ({
$css={css`
font-size: 1rem;
color: var(--c--theme--colors--primary-text-text);
pointer-events: none;
`}
/>
</Box>
@@ -24,7 +24,21 @@ export const LeftPanelContent = () => {
<Feedback />
</Box>
)}
<LeftPanelSearch onSearchChange={setHasSearch} />
<Box
$position="sticky"
$zIndex="100"
$css={`
top: 0;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 1) 0%,
rgba(255, 255, 255, 1) 70%,
rgba(255, 255, 255, 0) 100%
);
`}
>
<LeftPanelSearch onSearchChange={setHasSearch} />
</Box>
{!hasSearch && <LeftPanelConversations />}
</Box>
);
@@ -28,19 +28,11 @@ export const LeftPanelSearch = ({ onSearchChange }: LeftPanelSearchProps) => {
const currentConversationId = params?.id as string;
const {
data,
isFetching,
isRefetching,
isLoading,
fetchNextPage,
hasNextPage,
} = useInfiniteConversations({
const { data, fetchNextPage, hasNextPage } = useInfiniteConversations({
page: 1,
title: search,
});
const loading = isFetching || isRefetching || isLoading;
const handleInputSearch = useDebouncedCallback((value: string) => {
setSearch(value);
onSearchChange?.(value.length > 0);
@@ -71,7 +63,6 @@ export const LeftPanelSearch = ({ onSearchChange }: LeftPanelSearchProps) => {
return (
<QuickSearch
placeholder={t('Search for a chat')}
loading={loading}
onFilter={handleInputSearch}
onClear={handleClear}
inputValue={search}
@@ -58,7 +58,7 @@ export const SimpleConversationItem = ({
aria-describedby="doc-title"
aria-label={conversation.title || t('Untitled conversation')}
$size="sm"
$variation="1000"
$variation="850"
$css={ItemTextCss}
>
{conversation.title || t('Untitled conversation')}
@@ -69,6 +69,11 @@ nextjs-portal {
border: 1px solid rgb(0 0 0 / 16%) !important;
}
.c__button--primary:active,
.c__button--primary.c__button--active {
border: none;
}
.c__button--neutral {
transition: background-color 0.4s !important;
color: var(--c--theme--colors--greyscale-550) !important;
@@ -80,11 +85,26 @@ nextjs-portal {
padding: 4px !important;
padding-right: 9px !important;
border-radius: 4px !important;
@media (width <= 768px) {
padding-right: 4px !important;
}
}
.c__button--neutral:hover,
.c__button--neutral:active,
.c__button--neutral:focus {
.c__button--neutral.research-web-button,
.c__button--neutral.attach-file-button {
padding: 9px !important;
}
.c__button--neutral.action-chat-button span,
.c__button--neutral.research-web-button .c__button__icon span,
.c__button--neutral.attach-file-button .c__button__icon span {
@media (width <= 768px) {
font-size: 19px !important;
}
}
.c__button--neutral:hover {
background-color: #f2f5f4 !important;
}
@@ -139,3 +159,15 @@ ul a:hover {
background-color: var(--c--theme--colors--greyscale-550) !important;
color: var(--c--theme--colors--greyscale-050) !important;
}
@media (width <= 768px) {
.mobile-no-focus:hover {
background-color: transparent !important;
}
}
.c__button--feedback {
@media (width <= 1150px) {
display: none !important;
}
}