From 64af00e702ca4f49f82ccaba0adf159fb27e8e6e Mon Sep 17 00:00:00 2001 From: Rami Bitar Date: Sun, 2 Aug 2026 09:43:55 -0400 Subject: [PATCH] Replace Streamdown with a plugin-free markdown renderer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The assistant only ever renders prose, links and the odd list, but Streamdown's plugin set dragged in shiki, mermaid and katex — and streamdown core declares mermaid as a hard dependency, so dropping the plugins alone would have left it installed. Rendering now runs on the same unified pipeline Streamdown used internally, minus the plugin surface: remark-parse -> remark-gfm -> remark-rehype -> rehype-sanitize -> hast-util-to-jsx-runtime. GFM, streaming repair and the link-safety hook all survive; fenced code renders as plain
. Net -264/+101
packages.

remend now runs only while a part is still streaming. On settled text it
reads `*$89*` as an unclosed italic — the `$` throws off its
closing-delimiter scan — and appends a stray `*` that parses as an empty
bullet. Italic prices are ordinary storefront copy, so that would have
shown up in production.

Route-relative links go through next/link in the same tab; sending a
shopper to a product page in a new tab would strand the conversation
behind it. Only external destinations get the confirmation dialog.

code-block.tsx and tool.tsx were the sole users of shiki and had no
importers, so both are gone.

Also folded in two unrelated changes that were already in the tree: the
loader move out of app/components/ui, and dropping the fade on the
command palette so it opens instantly.

Co-Authored-By: Claude Opus 5 (1M context) 
Claude-Session: https://claude.ai/code/session_01KuySwKLSF4uEPpZjHkdHBP
---
 components/ai-elements/code-block.tsx         |  562 ------
 components/ai-elements/markdown.tsx           |  295 +++
 components/ai-elements/message.tsx            |   31 +-
 components/ai-elements/reasoning.tsx          |   36 +-
 components/ai-elements/tool.tsx               |  173 --
 components/shopify/account-form.tsx           |    2 +-
 components/shopify/cart-drawer.tsx            |    2 +-
 components/shopify/collection-detail.tsx      |    2 +-
 .../product-detail/product-detail-info.tsx    |    2 +-
 components/shopify/products.tsx               |    2 +-
 components/shopify/search-dialog.tsx          |    4 +-
 components/shopify/search-results.tsx         |    2 +-
 components/shopify/shop-pay-button.tsx        |    2 +-
 components/shopify/store-assistant.tsx        |    5 +
 components/ui/command.tsx                     |    9 +-
 {app/components => components}/ui/loader.tsx  |    0
 package.json                                  |   14 +-
 yarn.lock                                     | 1695 +----------------
 18 files changed, 363 insertions(+), 2475 deletions(-)
 delete mode 100644 components/ai-elements/code-block.tsx
 create mode 100644 components/ai-elements/markdown.tsx
 delete mode 100644 components/ai-elements/tool.tsx
 rename {app/components => components}/ui/loader.tsx (100%)

diff --git a/components/ai-elements/code-block.tsx b/components/ai-elements/code-block.tsx
deleted file mode 100644
index bc1441a..0000000
--- a/components/ai-elements/code-block.tsx
+++ /dev/null
@@ -1,562 +0,0 @@
-"use client";
-
-import { Button } from "@/components/ui/button";
-import {
-  Select,
-  SelectContent,
-  SelectItem,
-  SelectTrigger,
-  SelectValue,
-} from "@/components/ui/select";
-import { cn } from "@/lib/utils";
-import { CheckIcon, CopyIcon } from "lucide-react";
-import type { ComponentProps, CSSProperties, HTMLAttributes } from "react";
-import {
-  createContext,
-  memo,
-  useCallback,
-  useContext,
-  useEffect,
-  useMemo,
-  useRef,
-  useState,
-} from "react";
-import type {
-  BundledLanguage,
-  BundledTheme,
-  HighlighterGeneric,
-  ThemedToken,
-} from "shiki";
-import { createHighlighter } from "shiki";
-
-// Shiki uses bitflags for font styles: 1=italic, 2=bold, 4=underline
-// oxlint-disable-next-line eslint(no-bitwise)
-const isItalic = (fontStyle: number | undefined) => fontStyle && fontStyle & 1;
-// oxlint-disable-next-line eslint(no-bitwise)
-const isBold = (fontStyle: number | undefined) => fontStyle && fontStyle & 2;
-const isUnderline = (fontStyle: number | undefined) =>
-  // oxlint-disable-next-line eslint(no-bitwise)
-  fontStyle && fontStyle & 4;
-
-// Transform tokens to include pre-computed keys to avoid noArrayIndexKey lint
-interface KeyedToken {
-  token: ThemedToken;
-  key: string;
-}
-interface KeyedLine {
-  tokens: KeyedToken[];
-  key: string;
-}
-
-const addKeysToTokens = (lines: ThemedToken[][]): KeyedLine[] =>
-  lines.map((line, lineIdx) => ({
-    key: `line-${lineIdx}`,
-    tokens: line.map((token, tokenIdx) => ({
-      key: `line-${lineIdx}-${tokenIdx}`,
-      token,
-    })),
-  }));
-
-// Token rendering component
-const TokenSpan = ({ token }: { token: ThemedToken }) => (
-  
-    {token.content}
-  
-);
-
-// Line number styles using CSS counters
-const LINE_NUMBER_CLASSES = cn(
-  "block",
-  "before:content-[counter(line)]",
-  "before:inline-block",
-  "before:[counter-increment:line]",
-  "before:w-8",
-  "before:mr-4",
-  "before:text-right",
-  "before:text-muted-foreground/50",
-  "before:font-mono",
-  "before:select-none"
-);
-
-// Line rendering component
-const LineSpan = ({
-  keyedLine,
-  showLineNumbers,
-}: {
-  keyedLine: KeyedLine;
-  showLineNumbers: boolean;
-}) => (
-  
-    {keyedLine.tokens.length === 0
-      ? "\n"
-      : keyedLine.tokens.map(({ token, key }) => (
-          
-        ))}
-  
-);
-
-// Types
-type CodeBlockProps = HTMLAttributes & {
-  code: string;
-  language: BundledLanguage;
-  showLineNumbers?: boolean;
-};
-
-interface TokenizedCode {
-  tokens: ThemedToken[][];
-  fg: string;
-  bg: string;
-}
-
-interface CodeBlockContextType {
-  code: string;
-}
-
-// Context
-const CodeBlockContext = createContext({
-  code: "",
-});
-
-// Highlighter cache (singleton per language)
-const highlighterCache = new Map<
-  string,
-  Promise>
->();
-
-// Token cache
-const tokensCache = new Map();
-
-// Subscribers for async token updates
-const subscribers = new Map void>>();
-
-const getTokensCacheKey = (code: string, language: BundledLanguage) => {
-  const start = code.slice(0, 100);
-  const end = code.length > 100 ? code.slice(-100) : "";
-  return `${language}:${code.length}:${start}:${end}`;
-};
-
-const getHighlighter = (
-  language: BundledLanguage
-): Promise> => {
-  const cached = highlighterCache.get(language);
-  if (cached) {
-    return cached;
-  }
-
-  const highlighterPromise = createHighlighter({
-    langs: [language],
-    themes: ["github-light", "github-dark"],
-  });
-
-  highlighterCache.set(language, highlighterPromise);
-  return highlighterPromise;
-};
-
-// Create raw tokens for immediate display while highlighting loads
-const createRawTokens = (code: string): TokenizedCode => ({
-  bg: "transparent",
-  fg: "inherit",
-  tokens: code.split("\n").map((line) =>
-    line === ""
-      ? []
-      : [
-          {
-            color: "inherit",
-            content: line,
-          } as ThemedToken,
-        ]
-  ),
-});
-
-// Synchronous highlight with callback for async results
-export const highlightCode = (
-  code: string,
-  language: BundledLanguage,
-  // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-callbacks)
-  callback?: (result: TokenizedCode) => void
-): TokenizedCode | null => {
-  const tokensCacheKey = getTokensCacheKey(code, language);
-
-  // Return cached result if available
-  const cached = tokensCache.get(tokensCacheKey);
-  if (cached) {
-    return cached;
-  }
-
-  // Subscribe callback if provided
-  if (callback) {
-    if (!subscribers.has(tokensCacheKey)) {
-      subscribers.set(tokensCacheKey, new Set());
-    }
-    subscribers.get(tokensCacheKey)?.add(callback);
-  }
-
-  // Start highlighting in background - fire-and-forget async pattern
-  getHighlighter(language)
-    // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then)
-    .then((highlighter) => {
-      const availableLangs = highlighter.getLoadedLanguages();
-      const langToUse = availableLangs.includes(language) ? language : "text";
-
-      const result = highlighter.codeToTokens(code, {
-        lang: langToUse,
-        themes: {
-          dark: "github-dark",
-          light: "github-light",
-        },
-      });
-
-      const tokenized: TokenizedCode = {
-        bg: result.bg ?? "transparent",
-        fg: result.fg ?? "inherit",
-        tokens: result.tokens,
-      };
-
-      // Cache the result
-      tokensCache.set(tokensCacheKey, tokenized);
-
-      // Notify all subscribers
-      const subs = subscribers.get(tokensCacheKey);
-      if (subs) {
-        for (const sub of subs) {
-          sub(tokenized);
-        }
-        subscribers.delete(tokensCacheKey);
-      }
-    })
-    // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then), eslint-plugin-promise(prefer-await-to-callbacks)
-    .catch((error) => {
-      console.error("Failed to highlight code:", error);
-      subscribers.delete(tokensCacheKey);
-    });
-
-  return null;
-};
-
-const CodeBlockBody = memo(
-  ({
-    tokenized,
-    showLineNumbers,
-    className,
-  }: {
-    tokenized: TokenizedCode;
-    showLineNumbers: boolean;
-    className?: string;
-  }) => {
-    const preStyle = useMemo(
-      () => ({
-        backgroundColor: tokenized.bg,
-        color: tokenized.fg,
-      }),
-      [tokenized.bg, tokenized.fg]
-    );
-
-    const keyedLines = useMemo(
-      () => addKeysToTokens(tokenized.tokens),
-      [tokenized.tokens]
-    );
-
-    return (
-      
-        
-          {keyedLines.map((keyedLine) => (
-            
-          ))}
-        
-      
- ); - }, - (prevProps, nextProps) => - prevProps.tokenized === nextProps.tokenized && - prevProps.showLineNumbers === nextProps.showLineNumbers && - prevProps.className === nextProps.className -); - -CodeBlockBody.displayName = "CodeBlockBody"; - -export const CodeBlockContainer = ({ - className, - language, - style, - ...props -}: HTMLAttributes & { language: string }) => ( -
-); - -export const CodeBlockHeader = ({ - children, - className, - ...props -}: HTMLAttributes) => ( -
- {children} -
-); - -export const CodeBlockTitle = ({ - children, - className, - ...props -}: HTMLAttributes) => ( -
- {children} -
-); - -export const CodeBlockFilename = ({ - children, - className, - ...props -}: HTMLAttributes) => ( - - {children} - -); - -export const CodeBlockActions = ({ - children, - className, - ...props -}: HTMLAttributes) => ( -
- {children} -
-); - -export const CodeBlockContent = ({ - code, - language, - showLineNumbers = false, -}: { - code: string; - language: BundledLanguage; - showLineNumbers?: boolean; -}) => { - // Memoized raw tokens for immediate display - const rawTokens = useMemo(() => createRawTokens(code), [code]); - - // Synchronous cache lookup — avoids setState in effect for cached results - const syncTokens = useMemo( - () => highlightCode(code, language) ?? rawTokens, - [code, language, rawTokens] - ); - - // Async highlighting result (populated after shiki loads) - const [asyncTokens, setAsyncTokens] = useState(null); - const asyncKeyRef = useRef({ code, language }); - - // Invalidate stale async tokens synchronously during render - if ( - asyncKeyRef.current.code !== code || - asyncKeyRef.current.language !== language - ) { - asyncKeyRef.current = { code, language }; - setAsyncTokens(null); - } - - useEffect(() => { - let cancelled = false; - - highlightCode(code, language, (result) => { - if (!cancelled) { - setAsyncTokens(result); - } - }); - - return () => { - cancelled = true; - }; - }, [code, language]); - - const tokenized = asyncTokens ?? syncTokens; - - return ( -
- -
- ); -}; - -export const CodeBlock = ({ - code, - language, - showLineNumbers = false, - className, - children, - ...props -}: CodeBlockProps) => { - const contextValue = useMemo(() => ({ code }), [code]); - - return ( - - - {children} - - - - ); -}; - -export type CodeBlockCopyButtonProps = ComponentProps & { - onCopy?: () => void; - onError?: (error: Error) => void; - timeout?: number; -}; - -export const CodeBlockCopyButton = ({ - onCopy, - onError, - timeout = 2000, - children, - className, - ...props -}: CodeBlockCopyButtonProps) => { - const [isCopied, setIsCopied] = useState(false); - const timeoutRef = useRef(0); - const { code } = useContext(CodeBlockContext); - - const copyToClipboard = useCallback(async () => { - if (typeof window === "undefined" || !navigator?.clipboard?.writeText) { - onError?.(new Error("Clipboard API not available")); - return; - } - - try { - if (!isCopied) { - await navigator.clipboard.writeText(code); - setIsCopied(true); - onCopy?.(); - timeoutRef.current = window.setTimeout( - () => setIsCopied(false), - timeout - ); - } - } catch (error) { - onError?.(error as Error); - } - }, [code, onCopy, onError, timeout, isCopied]); - - useEffect( - () => () => { - window.clearTimeout(timeoutRef.current); - }, - [] - ); - - const Icon = isCopied ? CheckIcon : CopyIcon; - - return ( - - ); -}; - -export type CodeBlockLanguageSelectorProps = ComponentProps; - -export const CodeBlockLanguageSelector = ( - props: CodeBlockLanguageSelectorProps -) => + ), + img: ({ alt, ...props }: ComponentProps<"img">) => ( + // biome-ignore lint/nursery/noImgElement: model output, not a known asset + {alt + ), + li: (props: ComponentProps<"li">) =>
  • , + ol: (props: ComponentProps<"ol">) => ( +
      + ), + p: (props: ComponentProps<"p">) => ( +

      + ), + pre: (props: ComponentProps<"pre">) => ( +

      +  ),
      +  strong: (props: ComponentProps<"strong">) => (
      +    
      +  ),
      +  table: (props: ComponentProps<"table">) => (
      +    
      + + + ), + td: (props: ComponentProps<"td">) => ( +
      + ), + th: (props: ComponentProps<"th">) => ( + + ), + ul: (props: ComponentProps<"ul">) => ( +
        + ), +}); + +export type MarkdownProps = Omit, "children"> & { + children: string; + /** True while tokens are still arriving; enables incomplete-syntax repair. */ + isAnimating?: boolean; + linkSafety?: LinkSafetyConfig; +}; + +export const Markdown = memo( + ({ + children, + className, + isAnimating, + linkSafety, + ...props + }: MarkdownProps) => { + const [pending, setPending] = useState(null); + + const handleUntrusted = useCallback((url: string) => setPending(url), []); + + const content = useMemo(() => { + // While streaming, close syntax the model has not finished emitting so a + // partial `**bold` renders as bold rather than as literal asterisks. + const source = repair(children ?? "", isAnimating === true); + const tree = processor.runSync(processor.parse(source)) as Nodes; + + return toJsxRuntime(tree, { + components: buildComponents(linkSafety, handleUntrusted), + Fragment, + jsx, + jsxs, + }); + }, [children, isAnimating, linkSafety, handleUntrusted]); + + return ( +
        *:first-child]:mt-0 [&>*:last-child]:mb-0", + className + )} + {...props} + > + {content} + !open && setPending(null)} + open={pending !== null} + > + + + Leave this site? + + This link points somewhere outside the store. Continue only if + you trust it. + + +

        + {pending} +

        + + + + +
        +
        +
        + ); + }, + (prev, next) => + prev.children === next.children && prev.isAnimating === next.isAnimating +); + +Markdown.displayName = "Markdown"; diff --git a/components/ai-elements/message.tsx b/components/ai-elements/message.tsx index cc0e62c..0906a23 100644 --- a/components/ai-elements/message.tsx +++ b/components/ai-elements/message.tsx @@ -12,23 +12,19 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; -import { cjk } from "@streamdown/cjk"; -import { code } from "@streamdown/code"; -import { math } from "@streamdown/math"; -import { mermaid } from "@streamdown/mermaid"; import type { UIMessage } from "ai"; import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; import type { ComponentProps, HTMLAttributes, ReactElement } from "react"; import { createContext, - memo, useCallback, useContext, useEffect, useMemo, useState, } from "react"; -import { Streamdown } from "streamdown"; + +import { Markdown } from "./markdown"; export type MessageProps = HTMLAttributes & { from: UIMessage["role"]; @@ -319,27 +315,10 @@ export const MessageBranchPage = ({ ); }; -export type MessageResponseProps = ComponentProps; +export type MessageResponseProps = ComponentProps; -const streamdownPlugins = { cjk, code, math, mermaid }; - -export const MessageResponse = memo( - ({ className, ...props }: MessageResponseProps) => ( - *:first-child]:mt-0 [&>*:last-child]:mb-0", - className - )} - plugins={streamdownPlugins} - {...props} - /> - ), - (prevProps, nextProps) => - prevProps.children === nextProps.children && - nextProps.isAnimating === prevProps.isAnimating -); - -MessageResponse.displayName = "MessageResponse"; +// Markdown already memoises on children/isAnimating. +export const MessageResponse = Markdown; export type MessageToolbarProps = ComponentProps<"div">; diff --git a/components/ai-elements/reasoning.tsx b/components/ai-elements/reasoning.tsx index b56adac..b940398 100644 --- a/components/ai-elements/reasoning.tsx +++ b/components/ai-elements/reasoning.tsx @@ -7,10 +7,6 @@ import { CollapsibleTrigger, } from "@/components/ui/collapsible"; import { cn } from "@/lib/utils"; -import { cjk } from "@streamdown/cjk"; -import { code } from "@streamdown/code"; -import { math } from "@streamdown/math"; -import { mermaid } from "@streamdown/mermaid"; import { BrainIcon, ChevronDownIcon } from "lucide-react"; import type { ComponentProps, ReactNode } from "react"; import { @@ -23,8 +19,8 @@ import { useRef, useState, } from "react"; -import { Streamdown } from "streamdown"; +import { Markdown } from "./markdown"; import { Shimmer } from "./shimmer"; interface ReasoningContextValue { @@ -204,21 +200,23 @@ export type ReasoningContentProps = ComponentProps< children: string; }; -const streamdownPlugins = { cjk, code, math, mermaid }; - export const ReasoningContent = memo( - ({ className, children, ...props }: ReasoningContentProps) => ( - - {children} - - ) + ({ className, children, ...props }: ReasoningContentProps) => { + const { isStreaming } = useReasoning(); + + return ( + + {children} + + ); + } ); Reasoning.displayName = "Reasoning"; diff --git a/components/ai-elements/tool.tsx b/components/ai-elements/tool.tsx deleted file mode 100644 index 18c4bb8..0000000 --- a/components/ai-elements/tool.tsx +++ /dev/null @@ -1,173 +0,0 @@ -"use client"; - -import { Badge } from "@/components/ui/badge"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { cn } from "@/lib/utils"; -import type { DynamicToolUIPart, ToolUIPart } from "ai"; -import { - CheckCircleIcon, - ChevronDownIcon, - CircleIcon, - ClockIcon, - WrenchIcon, - XCircleIcon, -} from "lucide-react"; -import type { ComponentProps, ReactNode } from "react"; -import { isValidElement } from "react"; - -import { CodeBlock } from "./code-block"; - -export type ToolProps = ComponentProps; - -export const Tool = ({ className, ...props }: ToolProps) => ( - -); - -export type ToolPart = ToolUIPart | DynamicToolUIPart; - -export type ToolHeaderProps = { - title?: string; - className?: string; -} & ( - | { type: ToolUIPart["type"]; state: ToolUIPart["state"]; toolName?: never } - | { - type: DynamicToolUIPart["type"]; - state: DynamicToolUIPart["state"]; - toolName: string; - } -); - -const statusLabels: Record = { - "approval-requested": "Awaiting Approval", - "approval-responded": "Responded", - "input-available": "Running", - "input-streaming": "Pending", - "output-available": "Completed", - "output-denied": "Denied", - "output-error": "Error", -}; - -const statusIcons: Record = { - "approval-requested": , - "approval-responded": , - "input-available": , - "input-streaming": , - "output-available": , - "output-denied": , - "output-error": , -}; - -export const getStatusBadge = (status: ToolPart["state"]) => ( - - {statusIcons[status]} - {statusLabels[status]} - -); - -export const ToolHeader = ({ - className, - title, - type, - state, - toolName, - ...props -}: ToolHeaderProps) => { - const derivedName = - type === "dynamic-tool" ? toolName : type.split("-").slice(1).join("-"); - - return ( - -
        - - {title ?? derivedName} - {getStatusBadge(state)} -
        - -
        - ); -}; - -export type ToolContentProps = ComponentProps; - -export const ToolContent = ({ className, ...props }: ToolContentProps) => ( - -); - -export type ToolInputProps = ComponentProps<"div"> & { - input: ToolPart["input"]; -}; - -export const ToolInput = ({ className, input, ...props }: ToolInputProps) => ( -
        -

        - Parameters -

        -
        - -
        -
        -); - -export type ToolOutputProps = ComponentProps<"div"> & { - output: ToolPart["output"]; - errorText: ToolPart["errorText"]; -}; - -export const ToolOutput = ({ - className, - output, - errorText, - ...props -}: ToolOutputProps) => { - if (!(output || errorText)) { - return null; - } - - let Output =
        {output as ReactNode}
        ; - - if (typeof output === "object" && !isValidElement(output)) { - Output = ( - - ); - } else if (typeof output === "string") { - Output = ; - } - - return ( -
        -

        - {errorText ? "Error" : "Result"} -

        -
        - {errorText &&
        {errorText}
        } - {Output} -
        -
        - ); -}; diff --git a/components/shopify/account-form.tsx b/components/shopify/account-form.tsx index 47ca830..871ad74 100644 --- a/components/shopify/account-form.tsx +++ b/components/shopify/account-form.tsx @@ -4,7 +4,7 @@ import React, { useState } from 'react'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { Button } from '@/components/ui/button'; -import { Loader } from '@/app/components/ui/loader'; +import { Loader } from '@/components/ui/loader'; export interface AccountFormField { name: string; diff --git a/components/shopify/cart-drawer.tsx b/components/shopify/cart-drawer.tsx index e175d04..81ace6f 100644 --- a/components/shopify/cart-drawer.tsx +++ b/components/shopify/cart-drawer.tsx @@ -3,7 +3,7 @@ import React, { useState } from 'react'; import { useCartStore, redirectToCheckout } from '@/hooks/use-shopify-cart'; import { Button } from '@/components/ui/button'; -import { Loader } from '@/app/components/ui/loader'; +import { Loader } from '@/components/ui/loader'; import { Sheet, SheetContent, diff --git a/components/shopify/collection-detail.tsx b/components/shopify/collection-detail.tsx index 5c588dd..2860d60 100644 --- a/components/shopify/collection-detail.tsx +++ b/components/shopify/collection-detail.tsx @@ -6,7 +6,7 @@ import ProductCard from './product-card'; import ProductFilters, { type ProductFilterFacet } from './product-filters'; import ProductToolbar from './product-toolbar'; import { Button } from '@/components/ui/button'; -import { Loader } from '@/app/components/ui/loader'; +import { Loader } from '@/components/ui/loader'; import { getCollectionProductsPage, type CollectionSortKey, diff --git a/components/shopify/product-detail/product-detail-info.tsx b/components/shopify/product-detail/product-detail-info.tsx index c701d95..aadea71 100644 --- a/components/shopify/product-detail/product-detail-info.tsx +++ b/components/shopify/product-detail/product-detail-info.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Button } from '@/components/ui/button'; -import { Loader } from '@/app/components/ui/loader'; +import { Loader } from '@/components/ui/loader'; import { RiSubtractLine, RiAddLine } from '@remixicon/react'; import ShopPayButton from '@/components/shopify/shop-pay-button'; import type { ProductOption, ProductOptionValue } from '@/hooks/use-shopify-products'; diff --git a/components/shopify/products.tsx b/components/shopify/products.tsx index 9d31568..7c643b3 100644 --- a/components/shopify/products.tsx +++ b/components/shopify/products.tsx @@ -4,7 +4,7 @@ import React, { useState, useEffect } from 'react'; import ProductCard from './product-card'; import { getProductsPage } from '@/hooks/use-shopify-products'; import { Button } from '@/components/ui/button'; -import { Loader } from '@/app/components/ui/loader'; +import { Loader } from '@/components/ui/loader'; interface ProductImage { url: string; diff --git a/components/shopify/search-dialog.tsx b/components/shopify/search-dialog.tsx index 9bb12f1..b4fc655 100644 --- a/components/shopify/search-dialog.tsx +++ b/components/shopify/search-dialog.tsx @@ -11,7 +11,7 @@ import { CommandItem, } from '@/components/ui/command'; import { Button } from '@/components/ui/button'; -import { Loader } from '@/app/components/ui/loader'; +import { Loader } from '@/components/ui/loader'; import { RiSearchLine, RiImageLine, RiCloseLine } from '@remixicon/react'; import { searchSuggestions, @@ -111,7 +111,7 @@ const SearchDialog: React.FC = () => { if (event.key === 'Enter') goToSearchPage(); }} /> -
        +
        {hasQuery && (