+ {/* eslint-disable-next-line no-nested-ternary */}
+ {isChip ? (
+ // CHIP: single pill, favicon on the left inside the pill, text right.
+
+
+ {iconContent}
+
+
+ {label}
+
+
+ ) : isIconOnly ? (
+ // ICON ONLY: just the favicon box, no label.
+
+ {iconContent}
+
+ ) : (
+ // TILE: favicon square + label under (default Chrome new-tab style).
+ <>
+
+ {iconContent}
+
+
+ {label}
+
+ >
+ )}
+
+ {useQuickRemove && (
+
{
+ stop(event);
+ onRemove?.(shortcut);
+ }}
+ onPointerDown={(event) => event.stopPropagation()}
+ className={classNames(
+ 'flex size-5 cursor-pointer items-center justify-center rounded-full bg-text-primary text-surface-invert opacity-0 shadow-2 transition-[opacity,background-color] duration-150 hover:bg-accent-ketchup-default hover:text-white focus-visible:opacity-100 group-hover:opacity-100 motion-reduce:transition-none',
+ actionBtnPositionClass,
+ )}
+ >
+
+
+ )}
+
+ {menuOptions.length > 0 && (
+
+
+ event.stopPropagation()}
+ className={classNames(
+ 'flex size-5 cursor-pointer items-center justify-center rounded-full bg-text-primary text-surface-invert opacity-0 shadow-2 transition-opacity duration-150 focus-visible:opacity-100 group-hover:opacity-100 motion-reduce:transition-none',
+ actionBtnPositionClass,
+ )}
+ >
+
+
+
+ {/* Tile menu only carries 1–2 short labels (Edit / Remove or
+ Hide), so the default 256px action width feels enormous next
+ to a 76px tile. min-w-0 + a sensible 7rem floor lets it size
+ to its content while staying tappable on touch. */}
+
+
+
+
+ )}
+
+ );
+}
diff --git a/packages/shared/src/features/shortcuts/components/WebappShortcutsRow.tsx b/packages/shared/src/features/shortcuts/components/WebappShortcutsRow.tsx
new file mode 100644
index 00000000000..73bb64bf301
--- /dev/null
+++ b/packages/shared/src/features/shortcuts/components/WebappShortcutsRow.tsx
@@ -0,0 +1,155 @@
+import type { ReactElement } from 'react';
+import React, { useEffect, useRef } from 'react';
+import classNames from 'classnames';
+import {
+ closestCenter,
+ DndContext,
+ KeyboardSensor,
+ PointerSensor,
+ useSensor,
+ useSensors,
+} from '@dnd-kit/core';
+import type { DragEndEvent } from '@dnd-kit/core';
+import {
+ horizontalListSortingStrategy,
+ SortableContext,
+ sortableKeyboardCoordinates,
+} from '@dnd-kit/sortable';
+import { useSettingsContext } from '../../../contexts/SettingsContext';
+import { useLogContext } from '../../../contexts/LogContext';
+import { LogEvent, ShortcutsSourceType, TargetType } from '../../../lib/log';
+import { ShortcutTile } from './ShortcutTile';
+import { AddShortcutTile } from './AddShortcutTile';
+import {
+ useDragClickGuard,
+ DRAG_ACTIVATION_DISTANCE_PX,
+} from '../hooks/useDragClickGuard';
+import { DEFAULT_SHORTCUTS_APPEARANCE } from '../types';
+import type { ShortcutsAppearance } from '../types';
+import { useManualShortcutsRow } from '../hooks/useManualShortcutsRow';
+
+interface WebappShortcutsRowProps {
+ className?: string;
+}
+
+// Shares `ShortcutTile` / `useShortcutsManager` with the extension hub so
+// edits and reorders stay in sync. Auto mode is ignored — we don't have
+// topSites permission on the webapp and live browser history doesn't
+// translate across devices anyway.
+export function WebappShortcutsRow({
+ className,
+}: WebappShortcutsRowProps): ReactElement | null {
+ const { flags, showTopSites } = useSettingsContext();
+ const { logEvent } = useLogContext();
+ const manualRow = useManualShortcutsRow();
+ const { shortcuts } = manualRow;
+
+ const enabled = flags?.showShortcutsOnWebapp ?? false;
+ const appearance: ShortcutsAppearance =
+ flags?.shortcutsAppearance ?? DEFAULT_SHORTCUTS_APPEARANCE;
+
+ const loggedRef = useRef(false);
+ useEffect(() => {
+ if (loggedRef.current) {
+ return;
+ }
+ if (!enabled || !showTopSites || shortcuts.length === 0) {
+ return;
+ }
+ loggedRef.current = true;
+ logEvent({
+ event_name: LogEvent.Impression,
+ target_type: TargetType.Shortcuts,
+ extra: JSON.stringify({
+ source: ShortcutsSourceType.Custom,
+ surface: 'webapp',
+ }),
+ });
+ }, [enabled, showTopSites, shortcuts.length, logEvent]);
+
+ const sensors = useSensors(
+ useSensor(PointerSensor, {
+ activationConstraint: { distance: DRAG_ACTIVATION_DISTANCE_PX },
+ }),
+ useSensor(KeyboardSensor, {
+ coordinateGetter: sortableKeyboardCoordinates,
+ }),
+ );
+
+ // Same drag-guard plumbing as `ShortcutLinksHub` — see that file for the
+ // full rationale on post-drag click + native URL-drag suppression.
+ const { armGuard: armDragSuppression, onClickCapture: suppressClickCapture } =
+ useDragClickGuard();
+
+ const suppressNativeDragCapture = (event: React.DragEvent) => {
+ event.preventDefault();
+ };
+
+ const handleDragEnd = (event: DragEndEvent) => {
+ armDragSuppression();
+ const { active, over } = event;
+ if (!over || active.id === over.id) {
+ return;
+ }
+ manualRow.reorderShortcuts(active.id as string, over.id as string);
+ };
+
+ if (!enabled || !showTopSites) {
+ return null;
+ }
+ if (shortcuts.length === 0 && !manualRow.canAdd) {
+ return null;
+ }
+
+ return (
+