Skip to content
43 changes: 43 additions & 0 deletions special-pages/pages/new-tab/app/components/Icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,49 @@ export function CloseSmallIcon(props) {
);
}

/**
* From https://dub.duckduckgo.com/duckduckgo/Icons/blob/Main/Glyphs/16px/Close-Xsmall-16.svg.
* @param {import('preact').JSX.SVGAttributes<SVGSVGElement>} props
*/
export function CloseXsmallIcon(props) {
return (
<svg width="16" height="16" fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
fill="currentColor"
d="M10.433 4.683c.244-.244.64-.244.884 0s.244.64 0 .884L8.884 8l2.433 2.433c.244.244.244.64 0 .884s-.64.244-.884 0L8 8.884l-2.433 2.433c-.244.244-.64.244-.884 0s-.244-.64 0-.884L7.116 8 4.683 5.567c-.244-.244-.244-.64 0-.884s.64-.244.884 0L8 7.116z"
/>
</svg>
);
}

/**
* Colour PDF document glyph, from the Duck.ai frontend (react/src/duck-chat/assets/document-pdf-color-16.svg).
* @param {import('preact').JSX.SVGAttributes<SVGSVGElement>} props
*/
export function DocumentPdfColorIcon(props) {
return (
<svg width="16" height="16" fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
fill="#fff"
d="M10.4736 15.4219H5.52572C3.66751 15.4219 2.16113 13.9155 2.16113 12.0573V3.94271C2.16113 2.0845 3.66751 0.578125 5.52572 0.578125H8.40174C8.84178 0.578125 9.2643 0.750541 9.57871 1.0584L13.3329 4.73438C13.6561 5.05082 13.8382 5.48408 13.8382 5.93639V12.0573C13.8382 13.9155 12.3318 15.4219 10.4736 15.4219Z"
/>
<path
fill="#888"
d="M13.0054 4.69678V13C13.0054 14.1074 12.1074 15.0054 11 15.0054H5C3.89255 15.0054 2.99463 14.1074 2.99463 13V3C2.99463 1.89255 3.89255 0.994629 5 0.994629H9.30322C9.61815 0.994642 9.92038 1.12009 10.1431 1.34277L12.6572 3.85693C12.8799 4.07962 13.0054 4.38185 13.0054 4.69678ZM13.9946 4.69678C13.9946 4.1194 13.7652 3.5655 13.3569 3.15723L10.8428 0.643066C10.4345 0.234797 9.8806 0.00538445 9.30322 0.00537109H5C3.34602 0.00537109 2.00537 1.34602 2.00537 3V13C2.00537 14.654 3.34602 15.9946 5 15.9946H11C12.654 15.9946 13.9946 14.654 13.9946 13V4.69678Z"
/>
<path fill="#F05F2B" d="M9.5 2.125V0.5H10L13.5 4V4.5H11.875C10.5633 4.5 9.5 3.43668 9.5 2.125Z" />
<path
fill="#CC3B0A"
d="M12.8057 4.00537L9.99463 1.19434L9.99463 2.125C9.99463 3.16341 10.8366 4.00537 11.875 4.00537L12.8057 4.00537ZM9.60889 0.00537107C9.99051 0.00537105 10.3566 0.156911 10.6265 0.426758L13.5732 3.37353C13.8431 3.64338 13.9946 4.00949 13.9946 4.39111C13.9946 4.7245 13.7245 4.9946 13.3911 4.99463L11.875 4.99463C10.2901 4.99463 9.00537 3.70994 9.00537 2.125L9.00537 0.608887C9.0054 0.275497 9.2755 0.00539778 9.60889 0.00537107Z"
/>
<path
fill="#CC3B0A"
d="M5.15039 9.5C5.82697 9.5 6.22151 9.9448 6.22168 10.6445C6.22168 11.3395 5.82686 11.7754 5.1543 11.7754H4.58984V13H4.0752V9.5H5.15039ZM8.00293 9.5C8.84982 9.50006 9.44141 10.1701 9.44141 11.25C9.44139 12.3249 8.84981 12.9999 8.00293 13H6.9043V9.5H8.00293ZM12.0752 10.0596H10.6406V11.0352H11.8965V11.5947H10.6406V13H10.124V9.5H12.0752V10.0596ZM7.42871 12.4404H7.97363C8.57728 12.4403 8.88769 11.9949 8.8877 11.25C8.8877 10.5051 8.57729 10.0597 7.97363 10.0596H7.42871V12.4404ZM4.58984 11.2295H5.11328C5.47022 11.2294 5.67773 11.0345 5.67773 10.6445C5.67758 10.2549 5.47008 10.0596 5.11328 10.0596H4.58984V11.2295Z"
/>
</svg>
);
}

/**
* @param {import('preact').JSX.SVGAttributes<SVGSVGElement>} props
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export function AiChatForm({
}
}}
>
{children}
<textarea
ref={textareaRef}
class={styles.textarea}
Expand Down Expand Up @@ -215,7 +216,6 @@ export function AiChatForm({
}}
onClick={emitChange}
/>
{children}
<div tabIndex={-1} class={styles.buttons}>
{toolbarLeft}
<div class={styles.rightButtons}>{toolbarRight}</div>
Expand Down
6 changes: 4 additions & 2 deletions special-pages/pages/new-tab/app/omnibar/components/Tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ const VIEWPORT_PADDING = 8;
* @param {string} props.content
* @param {string} [props.className]
* @param {TooltipPosition} [props.position]
* @param {string} [props.ariaLabel] - names the wrapper so nested controls' labels don't leak into its accessible name.
*/
export function Tooltip({ children, content, className, position = 'above' }) {
export function Tooltip({ children, content, className, position = 'above', ariaLabel }) {
const [rect, setRect] = useState(/** @type {DOMRect | null} */ (null));
const tooltipId = useId();
const containerRef = useRef(/** @type {HTMLDivElement|null} */ (null));
Expand Down Expand Up @@ -55,7 +56,7 @@ export function Tooltip({ children, content, className, position = 'above' }) {
const hide = () => setRect(null);

const handleKeyDown = (e) => {
if (e.key === 'Enter' || e.key === ' ') {
if ((e.key === 'Enter' || e.key === ' ') && e.target === e.currentTarget) {
e.preventDefault();
if (rect) {
hide();
Expand Down Expand Up @@ -95,6 +96,7 @@ export function Tooltip({ children, content, className, position = 'above' }) {
class={cn(styles.container, className)}
role="button"
tabIndex={0}
aria-label={ariaLabel}
aria-describedby={isVisible ? tooltipId : undefined}
onMouseEnter={show}
onMouseLeave={hide}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { h } from 'preact';
import { Tooltip } from '../../Tooltip.js';
import { ChipRemoveButton } from './ChipRemoveButton';
import styles from './AttachmentChip.module.css';

/**
* @typedef {'tab' | 'file' | 'image'} AttachmentKind
*/

/**
* The 116px square attachment card shown above the AI chat prompt. Text chips render a
* title, an optional metadata line and an icon + type row; image chips render a
* full-bleed preview instead.
*
* @param {object} props
* @param {AttachmentKind} props.attachmentKind - marker for tests and per-kind styling.
* @param {import('preact').ComponentChildren} [props.imagePreview] - full-bleed preview; when set, the text rows are not rendered.
* @param {import('preact').ComponentChildren} [props.icon] - leading 16px glyph in the info row.
* @param {string} [props.title]
* @param {string} [props.typeLabel] - attachment kind, e.g. "Webpage" or "PDF".
* @param {string} [props.metadata] - extra line, e.g. domain or file size.
* @param {string} [props.tooltipLabel] - hover preview; defaults to `title`.
* @param {() => void} props.onRemove
* @param {string} props.removeLabel
* @param {number} [props.enteringAnimationDelay] - ms; staggers the pop-in across a row.
*/
export function AttachmentChip({
attachmentKind,
imagePreview,
icon,
title,
typeLabel,
metadata,
tooltipLabel,
onRemove,
removeLabel,
enteringAnimationDelay = 0,
}) {
const tooltip = tooltipLabel || title || '';
return (
<div class={styles.listItem} data-attachment-kind={attachmentKind}>
<span class={styles.enter} style={{ animationDelay: enteringAnimationDelay ? `${enteringAnimationDelay}ms` : undefined }}>
<Tooltip content={tooltip} position="above" className={styles.tooltipContainer} ariaLabel={tooltip}>
<span class={styles.root}>
{imagePreview ? (
<span class={styles.imageFill}>{imagePreview}</span>
) : (
<span class={styles.headerBand}>
<span class={styles.title}>{title}</span>
</span>
)}
{!imagePreview && metadata && <span class={styles.metadata}>{metadata}</span>}
{!imagePreview && (
<span class={styles.info}>
<span class={styles.iconSlot}>
<span class={styles.icon}>{icon}</span>
</span>
<span class={styles.typeLabel}>{typeLabel}</span>
</span>
)}
<span class={styles.removeSlot}>
<ChipRemoveButton onRemove={onRemove} label={removeLabel} />
</span>
</span>
</Tooltip>
</span>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/* A fixed 116px card that never compresses; the row scrolls instead. */
.listItem {
display: flex;
flex: none;
position: relative;
width: 116px;
}

/* Carries the entrance animation instead of the list item, so the item keeps its
* real layout size while the chip pops in and can be scrolled to right away. */
.enter {
display: flex;
flex: 1 1 auto;
min-width: 0;
animation: chipEnter 0.3s cubic-bezier(0.1, 0.77, 0.37, 1.13) backwards;

@media (prefers-reduced-motion) {
animation: none;
}
}

@keyframes chipEnter {
from {
transform: scale(0);
}
}

/* The Tooltip wrapper must fill the item so the card below can use width: 100%. */
.tooltipContainer {
border-radius: 18px;
display: flex;
flex: 1 1 auto;
min-width: 0;
}

/* A 116px square with three rows (title + metadata + icon/type). The icon/type row
* absorbs any slack below the fixed text rows. */
.root {
backdrop-filter: blur(24px);
background-color: var(--ds-color-theme-control-fill-primary);
/* Reserved so the hover/pressed border doesn't shift the contents. */
border: 1px solid transparent;
border-radius: 18px;
box-sizing: border-box;
color: var(--ds-color-theme-text-primary);
display: flex;
flex-direction: column;
height: 116px;
min-width: 0;
overflow: hidden;
position: relative;
width: 100%;
}

/* Hover is gated on a real pointer: on touch it sticks after a tap. */
@media (hover: hover) {
.root:hover {
background-color: var(--ds-color-theme-control-fill-secondary);
border-color: var(--ds-color-theme-container-border-primary);
box-shadow:
0 0.25px 1px 0 var(--ds-color-theme-shadow-secondary),
0 1px 4px 0 var(--ds-color-theme-shadow-primary);
}
}

.root:active {
background-color: var(--ds-color-theme-control-fill-tertiary);
border-color: var(--ds-color-theme-container-border-secondary);
box-shadow:
0 0.25px 1px 0 var(--ds-color-theme-shadow-secondary),
0 1px 4px 0 var(--ds-color-theme-shadow-primary);
}

/* Full-bleed image chips. The preview is absolutely positioned so its own aspect
* ratio can't shape the card. */
.imageFill {
flex: 1 1 auto;
overflow: hidden;
position: relative;
width: 100%;

& > * {
height: 100%;
left: 0;
object-fit: cover;
position: absolute;
top: 0;
width: 100%;
}
}

.headerBand {
box-sizing: border-box;
display: flex;
flex: none;
min-width: 0;
overflow: hidden;
padding: 12px 12px 2px;
width: 100%;
}

.title {
color: var(--ds-color-theme-text-primary);
font-size: 14px;
line-height: 20px;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}

/* Title wraps to two lines, then clamps; the two-line height is reserved so cards
* with short and long titles stay the same height. */
.headerBand .title {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
min-height: 40px;
white-space: normal;
overflow-wrap: anywhere;
}

/* Extra line between the title and the icon/type row: a fixed 24px band with its
* single 16px line centred by the vertical padding. */
.metadata {
box-sizing: border-box;
color: var(--ds-color-theme-text-secondary);
flex: none;
font-size: 12px;
height: 24px;
line-height: 16px;
min-width: 0;
overflow: hidden;
padding: 4px 12px;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}

.info {
align-items: center;
box-sizing: border-box;
display: flex;
flex: 1 1 auto;
gap: 4px;
min-width: 0;
padding: 4px 12px 12px;
width: 100%;
}

.iconSlot {
align-items: center;
display: flex;
flex: none;
height: 16px;
justify-content: center;
}

.icon {
align-items: center;
border-radius: 2px;
color: var(--ds-color-theme-icons-secondary);
display: flex;
flex: none;
height: 16px;
justify-content: center;
overflow: hidden;
width: 16px;

& > img {
height: 16px;
object-fit: cover;
width: 16px;
}
}

.typeLabel {
color: var(--ds-color-theme-text-secondary);
flex: 1 1 auto;
font-size: 12px;
line-height: 20px;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

/* Inset from the corner rather than overhanging it: the card clips its overflow.
* Hidden until hover or focus. A pointer click always hovers the card first, so the
* button is visible by the time it can be clicked; Tab focus reveals it too. */
.removeSlot {
display: flex;
opacity: 0;
position: absolute;
right: 7px;
top: 7px;
transition: opacity 0.15s ease-out;

@media (prefers-reduced-motion) {
transition: none;
}
}

.removeSlot:focus-within,
.root:hover .removeSlot {
opacity: 1;
}
Loading
Loading