diff --git a/.storybook/main.ts b/.storybook/main.ts index ad32d1d1..7428222c 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -9,7 +9,9 @@ const config: StorybookConfig = { "@storybook/addon-vitest", "@storybook/addon-a11y", "@storybook/addon-docs", - "@storybook/addon-mcp" + "@storybook/addon-mcp", + "@github-ui/storybook-addon-performance-panel", + "@chromatic-com/storybook" ], "framework": "@storybook/nextjs-vite", "staticDirs": [ diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 3d60b380..1c8c33dd 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,4 +1,5 @@ import type { Preview } from '@storybook/nextjs-vite'; +import perfPanelPreview from '@github-ui/storybook-addon-performance-panel/preview'; import { INITIAL_VIEWPORTS } from 'storybook/viewport'; import { Title, Subtitle, Description, Primary, Controls, Stories } from '@storybook/addon-docs/blocks'; import '../app/globals.css'; @@ -40,4 +41,11 @@ const preview: Preview = { }, }; -export default preview; +export default { + ...perfPanelPreview, + ...preview, + decorators: [ + ...(perfPanelPreview.decorators ?? []), + ...(preview.decorators ?? []), + ], +} satisfies Preview; diff --git a/app/(blog)/tin-tuc/[slug]/page.tsx b/app/(blog)/tin-tuc/[slug]/page.tsx index 16b3b85e..ece6a877 100644 --- a/app/(blog)/tin-tuc/[slug]/page.tsx +++ b/app/(blog)/tin-tuc/[slug]/page.tsx @@ -5,8 +5,8 @@ import {notFound} from 'next/navigation'; import {MDXRemote} from 'next-mdx-remote/rsc'; import {getAllPosts, getPostBySlug, getRelatedPosts} from '@/lib/mdx'; import {RelatedPosts} from '@/components/blog/related-posts'; -import {OwButton} from '@/components/ow-ui/ow-button'; -import {OwBadge} from '@/components/ow-ui/ow-badge'; +import {OwButton} from '@/components/owui/ow-button'; +import {OwBadge} from '@/components/owui/ow-badge'; import {SidebarRelatedCards} from '@/components/blog/sidebar-related-cards'; import {makeMdxComponents} from '@/components/blog/mdx-components'; import {remarkAutoLink} from '@/lib/remark-auto-link'; diff --git a/app/(blog)/tin-tuc/category/[category]/page.tsx b/app/(blog)/tin-tuc/category/[category]/page.tsx index d906d656..5d8dc5a9 100644 --- a/app/(blog)/tin-tuc/category/[category]/page.tsx +++ b/app/(blog)/tin-tuc/category/[category]/page.tsx @@ -4,7 +4,7 @@ import {ROUTES} from '@/lib/routes'; // import {PostList} from '@/components/blog/post-list'; // import {CategoryFilter} from '@/components/blog/category-filter'; import {BlogPageShell} from '@/components/layout/blog-page-shell'; -import {OwPostList} from '@/components/ow-ui/ow-post-list'; +import {OwPostList} from '@/components/owui/ow-post-list'; import {buildCollectionPageMeta} from '@/lib/page-meta/collection'; import {SITE_NAME} from '@/lib/page-meta/title'; diff --git a/app/(blog)/tin-tuc/page.tsx b/app/(blog)/tin-tuc/page.tsx index 1f43a9db..8bb9c4b5 100644 --- a/app/(blog)/tin-tuc/page.tsx +++ b/app/(blog)/tin-tuc/page.tsx @@ -6,8 +6,8 @@ import {ROUTES} from '@/lib/routes'; import {BlogPageShell} from '@/components/layout/blog-page-shell'; import {SITE_NAME} from '@/lib/page-meta/title'; import {buildCollectionPageMeta} from '@/lib/page-meta/collection'; -import {OwFeaturedPosts} from '@/components/ow-ui/ow-featured-posts'; -import {OwPostList} from '@/components/ow-ui/ow-post-list'; +import {OwFeaturedPosts} from '@/components/owui/ow-featured-posts'; +import {OwPostList} from '@/components/owui/ow-post-list'; const BREADCRUMB_ITEMS = [ {label: 'Trang chủ', href: '/'}, diff --git a/app/(marketing)/(about)/ve-openwallet/page.tsx b/app/(marketing)/(about)/ve-openwallet/page.tsx index 38a1b3f4..99135dfc 100644 --- a/app/(marketing)/(about)/ve-openwallet/page.tsx +++ b/app/(marketing)/(about)/ve-openwallet/page.tsx @@ -1,6 +1,6 @@ import type {Metadata} from 'next'; import Link from 'next/link'; -import {OwButton} from '@/components/ow-ui/ow-button'; +import {OwButton} from '@/components/owui/ow-button'; import {ProsePageShell} from '@/components/layout/prose-page-shell'; import {buildTitle} from '@/lib/page-meta/title'; import {buildBreadcrumbJsonLd} from '@/lib/page-meta/breadcrumb'; diff --git a/app/(marketing)/(legal)/chinh-sach-bao-mat/page.tsx b/app/(marketing)/(legal)/chinh-sach-bao-mat/page.tsx index c3cbf735..364672ad 100644 --- a/app/(marketing)/(legal)/chinh-sach-bao-mat/page.tsx +++ b/app/(marketing)/(legal)/chinh-sach-bao-mat/page.tsx @@ -1,6 +1,6 @@ import type {Metadata} from 'next'; import Link from 'next/link'; -import {OwButton} from '@/components/ow-ui/ow-button'; +import {OwButton} from '@/components/owui/ow-button'; import {ProsePageShell} from '@/components/layout/prose-page-shell'; import {buildTitle} from '@/lib/page-meta/title'; import {buildBreadcrumbJsonLd} from '@/lib/page-meta/breadcrumb'; diff --git a/app/(marketing)/(legal)/dieu-khoan/page.tsx b/app/(marketing)/(legal)/dieu-khoan/page.tsx index 9a226008..955be301 100644 --- a/app/(marketing)/(legal)/dieu-khoan/page.tsx +++ b/app/(marketing)/(legal)/dieu-khoan/page.tsx @@ -1,6 +1,6 @@ import type {Metadata} from 'next'; import Link from 'next/link'; -import {OwButton} from '@/components/ow-ui/ow-button'; +import {OwButton} from '@/components/owui/ow-button'; import {ProsePageShell} from '@/components/layout/prose-page-shell'; import {buildTitle} from '@/lib/page-meta/title'; import {buildBreadcrumbJsonLd} from '@/lib/page-meta/breadcrumb'; diff --git a/app/(marketing)/(legal)/mien-tru-trach-nhiem/page.tsx b/app/(marketing)/(legal)/mien-tru-trach-nhiem/page.tsx index b412ae68..411a7a52 100644 --- a/app/(marketing)/(legal)/mien-tru-trach-nhiem/page.tsx +++ b/app/(marketing)/(legal)/mien-tru-trach-nhiem/page.tsx @@ -1,6 +1,6 @@ import type {Metadata} from 'next'; import Link from 'next/link'; -import {OwButton} from '@/components/ow-ui/ow-button'; +import {OwButton} from '@/components/owui/ow-button'; import {ProsePageShell} from '@/components/layout/prose-page-shell'; import {buildTitle} from '@/lib/page-meta/title'; import {buildBreadcrumbJsonLd} from '@/lib/page-meta/breadcrumb'; diff --git a/app/(marketing)/(persona)/linh-vuc/page.tsx b/app/(marketing)/(persona)/linh-vuc/page.tsx index f6b0db2a..591cff05 100644 --- a/app/(marketing)/(persona)/linh-vuc/page.tsx +++ b/app/(marketing)/(persona)/linh-vuc/page.tsx @@ -5,8 +5,8 @@ import {MarketingPageShell} from '@/components/layout/marketing-page-shell'; import {buildCollectionPageMeta} from '@/lib/page-meta/collection'; import {buildTitle, SECTION_TITLES} from '@/lib/page-meta/title'; import Link from 'next/link'; -import {OwButton} from '@/components/ow-ui/ow-button'; -import {OwWobbleCard} from '@/components/ow-ui/ow-wobble-card'; +import {OwButton} from '@/components/owui/ow-button'; +import {OwWobbleCard} from '@/components/owui/ow-wobble-card'; export const revalidate = 3600; diff --git a/app/(marketing)/loai-the/page.tsx b/app/(marketing)/loai-the/page.tsx index d5d4552a..bf199aab 100644 --- a/app/(marketing)/loai-the/page.tsx +++ b/app/(marketing)/loai-the/page.tsx @@ -5,7 +5,7 @@ import {buildCollectionPageMeta} from '@/lib/page-meta/collection'; import {buildTitle, SECTION_TITLES} from '@/lib/page-meta/title'; import {ROUTES} from '@/lib/routes'; import {CARD_TYPE_LABELS, CARD_TYPE_HEX, CARD_TYPE_ICON, CARD_TYPE_SLUGS} from '@/lib/card-model'; -import {OwWobbleCard} from '@/components/ow-ui/ow-wobble-card'; +import {OwWobbleCard} from '@/components/owui/ow-wobble-card'; import type {CardType} from '@/lib/api'; import {getBanks, getCards} from '@/lib/api'; import {getPersonaTopCards} from '@/lib/persona-top-cards'; diff --git a/app/(marketing)/ngan-hang/page.tsx b/app/(marketing)/ngan-hang/page.tsx index 0aa67237..82f24c8e 100644 --- a/app/(marketing)/ngan-hang/page.tsx +++ b/app/(marketing)/ngan-hang/page.tsx @@ -1,6 +1,6 @@ import type {Metadata} from 'next'; import {getBanks} from '@/lib/api'; -import {OwBankRow} from '@/components/ow-ui/ow-bank-row'; +import {OwBankRow} from '@/components/owui/ow-bank-row'; import {buildCollectionPageMeta} from '@/lib/page-meta/collection'; import {MarketingPageShell} from '@/components/layout/marketing-page-shell'; import {ROUTES} from '@/lib/routes'; diff --git a/app/(marketing)/the/[slug]/page.tsx b/app/(marketing)/the/[slug]/page.tsx index 766f5548..825a62ae 100644 --- a/app/(marketing)/the/[slug]/page.tsx +++ b/app/(marketing)/the/[slug]/page.tsx @@ -4,7 +4,7 @@ import {cn} from '@/lib/utils'; import {getBank, getCard, getCards, getRelatedCards} from '@/lib/api'; import {CardModel} from '@/lib/card-model'; import {ChatContextSetter} from '@/components/chat/chat-context-setter'; -import {OwCardImage} from '@/components/ow-ui/ow-card-image'; +import {OwCardImage} from '@/components/owui/ow-card-image'; import {Breadcrumbs} from '@/components/layout/breadcrumbs'; import {buildCardPageMeta} from '@/lib/page-meta/card'; import {buildTitle, SECTION_TITLES} from '@/lib/page-meta/title'; @@ -12,7 +12,7 @@ import {CardDetailHeader} from '@/components/cards/detail/card-detail-header'; import {CardDetailBillingCycle} from '@/components/cards/detail/card-detail-billing-cycle'; import {CardDetailFees} from '@/components/cards/detail/card-detail-fees'; import {CardDetailOtherFees} from '@/components/cards/detail/card-detail-other-fees'; -import {OwSourceList} from '@/components/ow-ui/ow-source-list'; +import {OwSourceList} from '@/components/owui/ow-source-list'; import {CardDetailLastModified} from '@/components/cards/detail/card-detail-last-modified'; import {CardDetailRelated} from '@/components/cards/detail/card-detail-related'; import {CardDetailCompare} from '@/components/cards/detail/card-detail-compare'; diff --git a/app/(marketing)/the/page.tsx b/app/(marketing)/the/page.tsx index 8ab47214..eff9cd56 100644 --- a/app/(marketing)/the/page.tsx +++ b/app/(marketing)/the/page.tsx @@ -5,7 +5,7 @@ import {CardsGrid} from '@/components/cards/cards-grid'; import {buildCollectionPageMeta} from '@/lib/page-meta/collection'; import {MarketingPageShell} from '@/components/layout/marketing-page-shell'; import {OpenOwieButton} from '@/components/chat/open-owie-button'; -import {OwBadge, OwBadges} from '@/components/ow-ui/ow-badge'; +import {OwBadge, OwBadges} from '@/components/owui/ow-badge'; import {PersonaModel} from '@/lib/persona-model'; import {ROUTES} from '@/lib/routes'; import {buildTitle, SECTION_TITLES} from '@/lib/page-meta/title'; diff --git a/app/error.tsx b/app/error.tsx index b4432584..3cd4ae99 100644 --- a/app/error.tsx +++ b/app/error.tsx @@ -2,8 +2,8 @@ import Link from 'next/link'; import {IconAlertTriangle, IconWifi} from '@tabler/icons-react'; -import {OwLogo} from '@/components/ow-ui/ow-logo'; -import {OwButton} from "@/components/ow-ui/ow-button"; +import {OwLogo} from '@/components/owui/ow-logo'; +import {OwButton} from "@/components/owui/ow-button"; function isApiConnectionError(error: Error): boolean { const msg = error.message.toLowerCase(); diff --git a/app/layout.tsx b/app/layout.tsx index c1c00d69..0dc544ed 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -8,7 +8,7 @@ import {OverlayScrollbarsBody} from '@/components/layout/overlay-scrollbars-body import {PreviewBanner} from '@/components/layout/preview-banner'; import {cn} from '@/lib/utils'; import {TooltipProvider} from '@/components/ui/tooltip'; -import {OwOwieFab} from '@/components/ow-ui/ow-owie-fab'; +import {OwOwieFab} from '@/components/owui/ow-owie-fab'; import {ChatProvider} from '@/components/chat/chat-provider'; import {ChatPanel} from '@/components/chat/chat-panel'; import "./globals.css"; diff --git a/app/not-found.tsx b/app/not-found.tsx index 0f6b61a2..09f5b8f0 100644 --- a/app/not-found.tsx +++ b/app/not-found.tsx @@ -1,8 +1,8 @@ import Link from 'next/link'; import { IconError404 } from '@tabler/icons-react'; import { SoSanh404Redirect } from '@/components/layout/so-sanh-404-redirect'; -import { OwLogo } from '@/components/ow-ui/ow-logo'; -import {OwButton} from '@/components/ow-ui/ow-button'; +import { OwLogo } from '@/components/owui/ow-logo'; +import {OwButton} from '@/components/owui/ow-button'; export default function NotFound() { return ( diff --git a/components/assistant-ui/attachment.tsx b/components/assistant-ui/attachment.tsx index c6e02fb9..e8355fd8 100644 --- a/components/assistant-ui/attachment.tsx +++ b/components/assistant-ui/attachment.tsx @@ -22,7 +22,7 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/assistant-ui/avatar"; -import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; +import {OwTooltipIconButton} from "@/components/owui/ow-tooltip-icon-button"; import { cn } from "@/lib/utils"; const useFileSrc = (file: File | undefined) => { @@ -174,13 +174,13 @@ const AttachmentUI: FC = () => { const AttachmentRemove: FC = () => { return ( - - + ); }; @@ -208,7 +208,7 @@ export const ComposerAttachments: FC = () => { export const ComposerAddAttachment: FC = () => { return ( - { aria-label="Add Attachment" > - + ); }; diff --git a/components/assistant-ui/composer.tsx b/components/assistant-ui/composer.tsx new file mode 100644 index 00000000..257bd2b1 --- /dev/null +++ b/components/assistant-ui/composer.tsx @@ -0,0 +1,189 @@ +import {ModelSelector} from "@/components/assistant-ui/model-selector"; +import {CHAT_MODELS, getDefaultModel, getVisibleModels} from "@/lib/chat/models"; +import {getChatPrefs, setChatPref} from "@/lib/chat/chat-prefs"; +import {ContextDisplay} from "@/components/assistant-ui/context-display"; +import {MovingBorder} from "@/components/phucbm/moving-border"; +import {OwTooltipIconButton} from "@/components/owui/ow-tooltip-icon-button"; +import {Button} from "@/components/ui/button"; +import {AuiIf, ComposerPrimitive, useAuiState} from "@assistant-ui/react"; +import {ArrowUpIcon, SquareIcon} from "lucide-react"; +import {type FC, useEffect, useRef, useState} from "react"; +import {getContextPlaceholders, type PageContext} from "@/lib/chat/page-context"; + +type HealthState = { ready: boolean; mcp: boolean; api: boolean; model?: string } | null; + +function useApiReady(): HealthState { + const [health, setHealth] = useState(null); + useEffect(() => { + const check = async () => { + try { + const res = await fetch('/api/health'); + const data = await res.json() as { ready: boolean; mcp: boolean; api: boolean }; + setHealth(data); + } catch { + setHealth({ready: false, mcp: false, api: false}); + } + }; + check(); + const id = setInterval(check, 30_000); + return () => clearInterval(id); + }, []); + return health; +} + +function useContextPlaceholder(pageContext: PageContext | undefined, isRunning: boolean, hasInputValue: boolean) { + const [currentPlaceholder, setCurrentPlaceholder] = useState(''); + const placeholdersRef = useRef([]); + const indexRef = useRef(0); + const hasInputValueRef = useRef(hasInputValue); + + useEffect(() => { + hasInputValueRef.current = hasInputValue; + }, [hasInputValue]); + + useEffect(() => { + const placeholders = getContextPlaceholders(pageContext ?? null); + placeholdersRef.current = placeholders; + + indexRef.current = Math.floor(Math.random() * placeholders.length); + setCurrentPlaceholder(placeholders[indexRef.current]); + + if (isRunning) return; + + const interval = setInterval(() => { + if (hasInputValueRef.current) return; + indexRef.current = (indexRef.current + 1) % placeholders.length; + setCurrentPlaceholder(placeholders[indexRef.current]); + }, 8000); + + return () => clearInterval(interval); + }, [pageContext, isRunning]); + + return currentPlaceholder; +} + +const ComposerAction: FC<{ + health: HealthState; + selectedModelId: string; + onModelChange: (id: string) => void; + contextWindow: number; +}> = ({health, selectedModelId, onModelChange, contextWindow}) => { + const notReady = health !== null && !health.ready; + const unavailableLabel = health && !health.mcp ? "MCP unavailable" : "API unavailable"; + const visibleModels = getVisibleModels().map((m) => ({id: m.id, name: m.label})); + const defaultModelId = getDefaultModel().id; + return ( +
+
+
+ +
+ +
+
+ {notReady && ( + {unavailableLabel} + )} + !s.thread.isRunning}> + + + + + + + s.thread.isRunning}> + + + + +
+
+ ); +}; + +export const Composer: FC<{ pageContext?: PageContext }> = ({pageContext}) => { + const health = useApiReady(); + const defaultModelId = getDefaultModel().id; + const [selectedModelId, setSelectedModelId] = useState(() => { + if (typeof window === 'undefined') return defaultModelId; + return getChatPrefs().modelId ?? defaultModelId; + }); + const contextWindow = CHAT_MODELS.find((m) => m.id === selectedModelId)?.contextWindow ?? 128_000; + const isRunning = useAuiState((s) => s.thread.isRunning); + const inputRef = useRef(null); + const [hasInputValue, setHasInputValue] = useState(false); + + useEffect(() => { + setChatPref('modelId', selectedModelId); + }, [selectedModelId]); + + const currentPlaceholder = useContextPlaceholder(pageContext, isRunning, hasInputValue); + + const composerShell = ( +
+ setHasInputValue(e.target.value.trim() !== '')} + /> + +
+ ); + + return ( + + {isRunning ? ( + + {composerShell} + + ) : ( + composerShell + )} + {health?.model && ( +

+ {health.model} +

+ )} +
+ ); +}; diff --git a/components/assistant-ui/markdown-text.tsx b/components/assistant-ui/markdown-text.tsx index 350ae52c..7daee192 100644 --- a/components/assistant-ui/markdown-text.tsx +++ b/components/assistant-ui/markdown-text.tsx @@ -13,7 +13,7 @@ import {type FC, memo, useState} from "react"; import {CheckIcon, CopyIcon} from "lucide-react"; import Link from "next/link"; -import {TooltipIconButton} from "@/components/assistant-ui/tooltip-icon-button"; +import {OwTooltipIconButton} from "@/components/owui/ow-tooltip-icon-button"; import {cn} from "@/lib/utils"; const MarkdownTextImpl = () => { @@ -40,10 +40,10 @@ const CodeHeader: FC = ({ language, code }) => { {language} - + {!isCopied && } {isCopied && } - + ); }; diff --git a/components/assistant-ui/message.tsx b/components/assistant-ui/message.tsx new file mode 100644 index 00000000..f17fa88d --- /dev/null +++ b/components/assistant-ui/message.tsx @@ -0,0 +1,288 @@ +import {MarkdownText} from "@/components/assistant-ui/markdown-text"; +import {OwTypingBubble} from "@/components/owai/ow-typing-bubble"; +import { + Reasoning, + ReasoningContent, + ReasoningRoot, + ReasoningText, + ReasoningTrigger, +} from "@/components/assistant-ui/reasoning"; +import {ToolGroupContent, ToolGroupRoot, ToolGroupTrigger} from "@/components/assistant-ui/tool-group"; +import {ToolFallback} from "@/components/assistant-ui/tool-fallback"; +import {OwTooltipIconButton} from "@/components/owui/ow-tooltip-icon-button"; +import {Button} from "@/components/ui/button"; +import {cn} from "@/lib/utils"; +import { + ActionBarMorePrimitive, + ActionBarPrimitive, + AuiIf, + BranchPickerPrimitive, + ComposerPrimitive, + ErrorPrimitive, + getMcpAppFromToolPart, + MessagePrimitive, + useAuiState, +} from "@assistant-ui/react"; +import { + CheckIcon, + ChevronLeftIcon, + ChevronRightIcon, + CopyIcon, + DownloadIcon, + MoreHorizontalIcon, + PencilIcon, + RefreshCwIcon, + ThumbsDownIcon, + ThumbsUpIcon, +} from "lucide-react"; +import {type FC} from "react"; + +export const MessageError: FC = () => { + return ( + + + + + + ); +}; + +export const AssistantMessage: FC = () => { + const ACTION_BAR_PT = "pt-1.5"; + const ACTION_BAR_HEIGHT = `min-h-7.5 ${ACTION_BAR_PT}`; + const isRunning = useAuiState((s) => s.message.status?.type === "running"); + const hasText = useAuiState((s) => s.message.parts.some((p) => p.type === "text" && "text" in p && !!(p as {text: string}).text)); + + return ( + + +
+ { + if (part.type === "reasoning") + return ["group-chainOfThought", "group-reasoning"]; + if (part.type === "tool-call") { + if (getMcpAppFromToolPart(part)) return null; + return ["group-chainOfThought", "group-tool"]; + } + return null; + }} + > + {({part, children}) => { + switch (part.type) { + case "group-chainOfThought": + return
{children}
; + case "group-reasoning": { + const running = part.status.type === "running"; + return ( + + + + {children} + + + ); + } + case "group-tool": + return ( + + + {children} + + ); + case "text": + return ; + case "reasoning": + return ; + case "tool-call": + return part.toolUI ?? ; + default: + return null; + } + }} +
+ +
+ +
+ + +
+
+ ); +}; + +const AssistantActionBar: FC = () => { + return ( + + + + s.message.isCopied}> + + + !s.message.isCopied}> + + + + + + + + + + + + + + + + + + + + Helpful + + + + + + Not helpful + + + + + + + Export as Markdown + + + + + + ); +}; + +export const UserMessage: FC = () => { + return ( + +
+
+ +
+
+ +
+
+ + +
+ ); +}; + +const UserActionBar: FC = () => { + return ( + + + + + + + + ); +}; + +export const EditComposer: FC = () => { + return ( + + + +
+ + + + + + +
+
+
+ ); +}; + +export const BranchPicker: FC = ({className, ...rest}) => { + return ( + + + + + + + + / + + + + + + + + ); +}; diff --git a/components/assistant-ui/thread-welcome.tsx b/components/assistant-ui/thread-welcome.tsx new file mode 100644 index 00000000..58c065bb --- /dev/null +++ b/components/assistant-ui/thread-welcome.tsx @@ -0,0 +1,58 @@ +import {Button} from "@/components/ui/button"; +import {ThreadPrimitive} from "@assistant-ui/react"; +import {type FC} from "react"; + +const SUGGESTIONS = [ + { + title: 'Đi siêu thị 5tr/tháng nên mở thẻ nào?', + prompt: 'Mỗi tháng đi siêu thị 5 triệu thì nên mở thẻ nào?', + }, + { + title: 'Thẻ ghi nợ nào có ưu đãi hoàn tiền?', + prompt: 'Thẻ ghi nợ nào có ưu đãi hoàn tiền?', + }, + { + title: 'Mua vé máy bay, khách sạn thường xuyên thì nên mở thẻ nào?', + prompt: 'Mua vé máy bay, khách sạn thường xuyên thì nên mở thẻ nào?', + }, +]; + +const ThreadSuggestions: FC = () => { + return ( +
+ {SUGGESTIONS.map((s) => ( +
+ + + +
+ ))} +
+ ); +}; + +export const ThreadWelcome: FC = () => { + return ( +
+
+
+

+ Chào bạn, +

+

+ Owie là trợ lý ảo giải đáp những câu hỏi về thẻ. +

+
+
+ +
+ ); +}; diff --git a/components/assistant-ui/thread.tsx b/components/assistant-ui/thread.tsx index 70df8c0c..510d3823 100644 --- a/components/assistant-ui/thread.tsx +++ b/components/assistant-ui/thread.tsx @@ -1,73 +1,11 @@ -import {MarkdownText} from "@/components/assistant-ui/markdown-text"; -import {OwTypingBubble} from "@/components/ow-ui/ow-typing-bubble"; -import {ModelSelector} from "@/components/assistant-ui/model-selector"; -import {CHAT_MODELS, getDefaultModel, getVisibleModels} from "@/lib/chat/models"; -import {getChatPrefs, setChatPref} from "@/lib/chat/chat-prefs"; -import {ContextDisplay} from "@/components/assistant-ui/context-display"; -import {MovingBorder} from "@/components/phucbm/moving-border"; -import { - Reasoning, - ReasoningContent, - ReasoningRoot, - ReasoningText, - ReasoningTrigger, -} from "@/components/assistant-ui/reasoning"; -import {ToolGroupContent, ToolGroupRoot, ToolGroupTrigger,} from "@/components/assistant-ui/tool-group"; -import {ToolFallback} from "@/components/assistant-ui/tool-fallback"; -import {TooltipIconButton} from "@/components/assistant-ui/tooltip-icon-button"; -import {Button} from "@/components/ui/button"; -import {cn} from "@/lib/utils"; +import {Composer} from "@/components/assistant-ui/composer"; +import {AssistantMessage, EditComposer, UserMessage} from "@/components/assistant-ui/message"; +import {ThreadWelcome} from "@/components/assistant-ui/thread-welcome"; +import {OwTooltipIconButton} from "@/components/owui/ow-tooltip-icon-button"; import {useChatContext} from "@/components/chat/chat-provider"; -import { - ActionBarMorePrimitive, - ActionBarPrimitive, - AuiIf, - BranchPickerPrimitive, - ComposerPrimitive, - ErrorPrimitive, - getMcpAppFromToolPart, - MessagePrimitive, - ThreadPrimitive, - useAuiState, -} from "@assistant-ui/react"; -import { - ArrowDownIcon, - ArrowUpIcon, - CheckIcon, - ChevronLeftIcon, - ChevronRightIcon, - CopyIcon, - DownloadIcon, - MoreHorizontalIcon, - PencilIcon, - RefreshCwIcon, - SquareIcon, - ThumbsDownIcon, - ThumbsUpIcon, -} from "lucide-react"; -import {type FC, useEffect, useRef, useState} from "react"; -import {getContextPlaceholders, type PageContext} from "@/lib/chat/page-context"; - -type HealthState = { ready: boolean; mcp: boolean; api: boolean; model?: string } | null; - -function useApiReady(): HealthState { - const [health, setHealth] = useState(null); - useEffect(() => { - const check = async () => { - try { - const res = await fetch('/api/health'); - const data = await res.json() as { ready: boolean; mcp: boolean; api: boolean }; - setHealth(data); - } catch { - setHealth({ready: false, mcp: false, api: false}); - } - }; - check(); - const id = setInterval(check, 30_000); - return () => clearInterval(id); - }, []); - return health; -} +import {AuiIf, MessagePrimitive, ThreadPrimitive, useAuiState} from "@assistant-ui/react"; +import {ArrowDownIcon} from "lucide-react"; +import {type FC, useEffect} from "react"; export const Thread: FC = () => { const {setThreadRunning, pageContext} = useChatContext(); @@ -129,488 +67,13 @@ const ThreadMessage: FC = () => { const ThreadScrollToBottom: FC = () => { return ( - - + ); }; - -const ThreadWelcome: FC = () => { - return ( -
-
-
-

- Chào bạn, -

-

- Owie là trợ lý ảo giải đáp những câu hỏi về thẻ. -

-
-
- -
- ); -}; - -const SUGGESTIONS = [ - { - title: 'Đi siêu thị 5tr/tháng nên mở thẻ nào?', - prompt: 'Mỗi tháng đi siêu thị 5 triệu thì nên mở thẻ nào?', - }, - { - title: 'Thẻ ghi nợ nào có ưu đãi hoàn tiền?', - prompt: 'Thẻ ghi nợ nào có ưu đãi hoàn tiền?', - }, - { - title: 'Mua vé máy bay, khách sạn thường xuyên thì nên mở thẻ nào?', - prompt: 'Mua vé máy bay, khách sạn thường xuyên thì nên mở thẻ nào?', - }, -]; - -const ThreadSuggestions: FC = () => { - return ( -
- {SUGGESTIONS.map((s) => ( -
- - - -
- ))} -
- ); -}; - -/** - * Hook for rotating context-aware placeholders - */ -function useContextPlaceholder(pageContext: PageContext | undefined, isRunning: boolean, hasInputValue: boolean) { - const [currentPlaceholder, setCurrentPlaceholder] = useState(''); - const placeholdersRef = useRef([]); - const indexRef = useRef(0); - const hasInputValueRef = useRef(hasInputValue); - - useEffect(() => { - hasInputValueRef.current = hasInputValue; - }, [hasInputValue]); - - // Initialize and rotate placeholders - useEffect(() => { - const placeholders = getContextPlaceholders(pageContext ?? null); - placeholdersRef.current = placeholders; - - // Pick random initial index - indexRef.current = Math.floor(Math.random() * placeholders.length); - setCurrentPlaceholder(placeholders[indexRef.current]); - - if (isRunning) return; - - // Rotate every 8 seconds - const interval = setInterval(() => { - if (hasInputValueRef.current) return; // Stop rotation if input has value - indexRef.current = (indexRef.current + 1) % placeholders.length; - setCurrentPlaceholder(placeholders[indexRef.current]); - }, 8000); - - return () => clearInterval(interval); - }, [pageContext, isRunning]); - - return currentPlaceholder; -} - -const Composer: FC<{ pageContext?: PageContext }> = ({pageContext}) => { - const health = useApiReady(); - const defaultModelId = getDefaultModel().id; - const [selectedModelId, setSelectedModelId] = useState(() => { - if (typeof window === 'undefined') return defaultModelId; - return getChatPrefs().modelId ?? defaultModelId; - }); - const contextWindow = CHAT_MODELS.find((m) => m.id === selectedModelId)?.contextWindow ?? 128_000; - const isRunning = useAuiState((s) => s.thread.isRunning); - const inputRef = useRef(null); - const [hasInputValue, setHasInputValue] = useState(false); - - useEffect(() => { - setChatPref('modelId', selectedModelId); - }, [selectedModelId]); - - const currentPlaceholder = useContextPlaceholder(pageContext, isRunning, hasInputValue); - - const composerShell = ( -
- setHasInputValue(e.target.value.trim() !== '')} - /> - -
- ); - - return ( - - {isRunning ? ( - - {composerShell} - - ) : ( - composerShell - )} - {health?.model && ( -

- {health.model} -

- )} -
- ); -}; - -const ComposerAction: FC<{ - health: HealthState; - selectedModelId: string; - onModelChange: (id: string) => void; - contextWindow: number -}> = ({health, selectedModelId, onModelChange, contextWindow}) => { - const notReady = health !== null && !health.ready; - const unavailableLabel = health && !health.mcp ? "MCP unavailable" : "API unavailable"; - const visibleModels = getVisibleModels().map((m) => ({id: m.id, name: m.label})); - const defaultModelId = getDefaultModel().id; - return ( -
-
-
- -
- -
-
- {notReady && ( - {unavailableLabel} - )} - !s.thread.isRunning}> - - - - - - - s.thread.isRunning}> - - - - -
-
- ); -}; - -const MessageError: FC = () => { - return ( - - - - - - ); -}; - -const AssistantMessage: FC = () => { - // reserves space for action bar and compensates with `-mb` for consistent msg spacing - // keeps hovered action bar from shifting layout (autohide doesn't support absolute positioning well) - // for pt-[n] use -mb-[n + 6] & min-h-[n + 6] to preserve compensation - const ACTION_BAR_PT = "pt-1.5"; - const ACTION_BAR_HEIGHT = `min-h-7.5 ${ACTION_BAR_PT}`; - const isRunning = useAuiState((s) => s.message.status?.type === "running"); - const hasText = useAuiState((s) => s.message.parts.some((p) => p.type === "text" && "text" in p && !!(p as {text: string}).text)); - - return ( - - -
- { - if (part.type === "reasoning") - return ["group-chainOfThought", "group-reasoning"]; - if (part.type === "tool-call") { - if (getMcpAppFromToolPart(part)) return null; - return ["group-chainOfThought", "group-tool"]; - } - return null; - }} - > - {({part, children}) => { - switch (part.type) { - case "group-chainOfThought": - return
{children}
; - case "group-reasoning": { - const running = part.status.type === "running"; - return ( - - - - {children} - - - ); - } - case "group-tool": - return ( - - - {children} - - ); - case "text": - return ; - case "reasoning": - return ; - case "tool-call": - return part.toolUI ?? ; - default: - return null; - } - }} -
- -
- -
- - -
-
- ); -}; - -const AssistantActionBar: FC = () => { - return ( - - - - s.message.isCopied}> - - - !s.message.isCopied}> - - - - - - - - - - - - - - - - - - - - Helpful - - - - - - Not helpful - - - - - - - Export as Markdown - - - - - - ); -}; - -const UserMessage: FC = () => { - return ( - -
-
- -
-
- -
-
- - -
- ); -}; - -const UserActionBar: FC = () => { - return ( - - - - - - - - ); -}; - -const EditComposer: FC = () => { - return ( - - - -
- - - - - - -
-
-
- ); -}; - -const BranchPicker: FC = ({ - className, - ...rest - }) => { - return ( - - - - - - - - / - - - - - - - - ); -}; diff --git a/components/assistant-ui/tooltip-icon-button.tsx b/components/assistant-ui/tooltip-icon-button.tsx deleted file mode 100644 index dd7dbf04..00000000 --- a/components/assistant-ui/tooltip-icon-button.tsx +++ /dev/null @@ -1,45 +0,0 @@ -"use client"; - -import { type ComponentPropsWithRef, forwardRef } from "react"; -import { Slot } from "radix-ui"; - -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; - -export type TooltipIconButtonProps = ComponentPropsWithRef & { - tooltip: string; - side?: "top" | "bottom" | "left" | "right"; -}; - -export const TooltipIconButton = forwardRef< - HTMLButtonElement, - TooltipIconButtonProps ->(({ children, tooltip, side = "bottom", className, ...rest }, ref) => { - return ( - - - - - - {tooltip} - - - ); -}); - -TooltipIconButton.displayName = "TooltipIconButton"; diff --git a/components/blog/mdx-components.tsx b/components/blog/mdx-components.tsx index 4a5a4a4a..8fffdd9e 100644 --- a/components/blog/mdx-components.tsx +++ b/components/blog/mdx-components.tsx @@ -1,6 +1,6 @@ import {slugify} from '@/lib/mdx'; import {cn} from "@/lib/utils"; -import {OwButton} from '@/components/ow-ui/ow-button'; +import {OwButton} from '@/components/owui/ow-button'; function extractText(children: React.ReactNode): string { if (typeof children === 'string') return children; diff --git a/components/blog/related-posts.tsx b/components/blog/related-posts.tsx index 7d179427..c8fff9a8 100644 --- a/components/blog/related-posts.tsx +++ b/components/blog/related-posts.tsx @@ -1,5 +1,5 @@ import type { Post } from '@/lib/mdx'; -import { OwPostList } from '@/components/ow-ui/ow-post-list'; +import { OwPostList } from '@/components/owui/ow-post-list'; interface Props { posts: Post[]; diff --git a/components/cards/card-masonry.tsx b/components/cards/card-masonry.tsx index 267abd3c..2ef8efc5 100644 --- a/components/cards/card-masonry.tsx +++ b/components/cards/card-masonry.tsx @@ -4,7 +4,7 @@ import {useState} from 'react'; import Masonry from 'react-masonry-css'; import type {Card} from '@/lib/api'; import {CardDisplay} from '@/components/cards/variants/card-display'; -import {OwButton} from '@/components/ow-ui/ow-button'; +import {OwButton} from '@/components/owui/ow-button'; const breakpointCols = { default: 5, 1023: 4, 767: 3, 639: 2 }; diff --git a/components/cards/detail/card-detail-cashback.tsx b/components/cards/detail/card-detail-cashback.tsx index 52034358..3f3a7be3 100644 --- a/components/cards/detail/card-detail-cashback.tsx +++ b/components/cards/detail/card-detail-cashback.tsx @@ -3,9 +3,9 @@ import type {Merchant} from '@/lib/api'; import {getIntents, getMerchants} from '@/lib/api'; import {IntentModel} from '@/lib/intent-model'; import type {CardModel} from '@/lib/card-model'; -import {OwAmount} from '@/components/ow-ui/ow-amount'; +import {OwAmount} from '@/components/owui/ow-amount'; import {IconInfoCircle} from '@tabler/icons-react'; -import {OwAlert} from '@/components/ow-ui/ow-alert'; +import {OwAlert} from '@/components/owui/ow-alert'; import {CardDetailSection} from '@/components/cards/detail/card-detail-section'; import {fmtIsoDate, fmtIsoDateLifespan} from '@/lib/utils'; import { diff --git a/components/cards/detail/card-detail-fees.tsx b/components/cards/detail/card-detail-fees.tsx index 918ac0b0..623d7667 100644 --- a/components/cards/detail/card-detail-fees.tsx +++ b/components/cards/detail/card-detail-fees.tsx @@ -1,6 +1,6 @@ import type {Bank, FeeEntry, FeeEntryWithWaiver, FeeWaiver} from '@/lib/api'; import type {CardModel} from '@/lib/card-model'; -import {OwAmount} from '@/components/ow-ui/ow-amount'; +import {OwAmount} from '@/components/owui/ow-amount'; import {CardDetailSection} from '@/components/cards/detail/card-detail-section'; function FeeValue({ entry }: { entry?: FeeEntry | null }) { diff --git a/components/cards/detail/card-detail-header.tsx b/components/cards/detail/card-detail-header.tsx index a77b208c..46ddc436 100644 --- a/components/cards/detail/card-detail-header.tsx +++ b/components/cards/detail/card-detail-header.tsx @@ -3,8 +3,8 @@ import {BankModel} from '@/lib/bank-model'; import {normalizeCardTypes} from '@/lib/api'; import type {CardModel} from '@/lib/card-model'; import {CoBrandDisplay} from '@/components/cards/co-brand-display'; -import {OwBankImage} from '@/components/ow-ui/ow-bank-image'; -import {OwBadge, OwBadges} from '@/components/ow-ui/ow-badge'; +import {OwBankImage} from '@/components/owui/ow-bank-image'; +import {OwBadge, OwBadges} from '@/components/owui/ow-badge'; interface Props { card: CardModel; diff --git a/components/cards/detail/card-detail-intents.tsx b/components/cards/detail/card-detail-intents.tsx index 1c77b90a..348ea216 100644 --- a/components/cards/detail/card-detail-intents.tsx +++ b/components/cards/detail/card-detail-intents.tsx @@ -1,6 +1,6 @@ import {getIntents} from '@/lib/api'; import type {CardModel} from '@/lib/card-model'; -import {OwCardIntentBadges} from '@/components/ow-ui/ow-card-intent-badges'; +import {OwCardIntentBadges} from '@/components/owui/ow-card-intent-badges'; import {CardDetailSection} from '@/components/cards/detail/card-detail-section'; interface Props { diff --git a/components/cards/detail/card-detail-other-fees.tsx b/components/cards/detail/card-detail-other-fees.tsx index 45da4015..15c2a2d9 100644 --- a/components/cards/detail/card-detail-other-fees.tsx +++ b/components/cards/detail/card-detail-other-fees.tsx @@ -1,8 +1,8 @@ import type {Bank, FeeEntry} from '@/lib/api'; import {BankModel} from '@/lib/bank-model'; import type {CardModel} from '@/lib/card-model'; -import {OwAmount} from '@/components/ow-ui/ow-amount'; -import {OwWobbleCard} from '@/components/ow-ui/ow-wobble-card'; +import {OwAmount} from '@/components/owui/ow-amount'; +import {OwWobbleCard} from '@/components/owui/ow-wobble-card'; function NoteLines({ note }: { note: string }) { const lines = note.split('|').map((l) => l.trim()).filter(Boolean); diff --git a/components/cards/detail/card-detail-rank-badges.tsx b/components/cards/detail/card-detail-rank-badges.tsx index 06186ef0..77e25cdf 100644 --- a/components/cards/detail/card-detail-rank-badges.tsx +++ b/components/cards/detail/card-detail-rank-badges.tsx @@ -2,7 +2,7 @@ import Link from 'next/link'; import {getPersonas, getRankedCards} from '@/lib/api'; import type {CardModel} from '@/lib/card-model'; import {PersonaModel} from '@/lib/persona-model'; -import {OwBadge, OwBadges} from '@/components/ow-ui/ow-badge'; +import {OwBadge, OwBadges} from '@/components/owui/ow-badge'; import {CardDetailSection} from '@/components/cards/detail/card-detail-section'; interface RankBadge { diff --git a/components/cards/detail/cashback-calculator.tsx b/components/cards/detail/cashback-calculator.tsx index fbbcc5a7..b473c5b0 100644 --- a/components/cards/detail/cashback-calculator.tsx +++ b/components/cards/detail/cashback-calculator.tsx @@ -2,12 +2,12 @@ import * as React from 'react'; import type {CashbackBenefit, CashbackResult, Merchant} from '@/lib/api'; import {IconAlertTriangle, IconLoader2} from '@tabler/icons-react'; -import {OwRangeSlider} from '@/components/ow-ui/ow-range-slider'; -import {formatOwAmount, OwAmount} from '@/components/ow-ui/ow-amount'; -import {OwCardCashbackRule} from '@/components/ow-ui/ow-card-cashback-rule'; +import {OwRangeSlider} from '@/components/owui/ow-range-slider'; +import {formatOwAmount, OwAmount} from '@/components/owui/ow-amount'; +import {OwCardCashbackRule} from '@/components/owui/ow-card-cashback-rule'; import {IntentModel} from '@/lib/intent-model'; import {CATCHALL_SLUGS} from '@/lib/cashback-utils'; -import {OwAlert} from "@/components/ow-ui/ow-alert"; +import {OwAlert} from "@/components/owui/ow-alert"; export type SerializedIntentMap = Record; export type SerializedMerchantMap = Record; diff --git a/components/cards/variants/card-display.tsx b/components/cards/variants/card-display.tsx index 5d45a862..051663bc 100644 --- a/components/cards/variants/card-display.tsx +++ b/components/cards/variants/card-display.tsx @@ -9,9 +9,9 @@ import {normalizeCardTypes} from '@/lib/api'; import type {Bank} from '@/lib/api'; import {BankModel} from '@/lib/bank-model'; import {CardModel} from '@/lib/card-model'; -import {OwCardImage} from '@/components/ow-ui/ow-card-image'; -import {OwBadge, OwBadges} from '@/components/ow-ui/ow-badge'; -import {OwAmount} from '@/components/ow-ui/ow-amount'; +import {OwCardImage} from '@/components/owui/ow-card-image'; +import {OwBadge, OwBadges} from '@/components/owui/ow-badge'; +import {OwAmount} from '@/components/owui/ow-amount'; import {useCompareList} from '@/lib/use-compare-list'; interface BadgeConfig { diff --git a/components/chat/chat-panel.tsx b/components/chat/chat-panel.tsx index 3ffdcbac..097ca0f3 100644 --- a/components/chat/chat-panel.tsx +++ b/components/chat/chat-panel.tsx @@ -4,7 +4,7 @@ import {useCallback, useEffect, useState} from 'react'; import {useRouter} from 'next/navigation'; import {CheckIcon} from 'lucide-react'; import {cn} from '@/lib/utils'; -import {OwTrafficLights} from '@/components/ow-ui/ow-traffic-lights'; +import {OwTrafficLights} from '@/components/owui/ow-traffic-lights'; import { Select, SelectContent, @@ -20,7 +20,7 @@ import {ChatRuntime} from '@/components/chat/chat-runtime'; import {getUserId} from '@/lib/chat/anonymous-user'; import {type Conversation, createConversation, getLastActiveId, listConversations, setLastActiveId,} from '@/lib/chat/conversation-store'; import Link from 'next/link'; -import {OwLogo} from "@/components/ow-ui/ow-logo"; +import {OwLogo} from "@/components/owui/ow-logo"; import {IconArrowsMaximize, IconHome, IconMinus, IconPlus} from "@tabler/icons-react"; function groupConversations(convos: Conversation[]) { @@ -231,4 +231,4 @@ export function ChatPanel() { ); -} \ No newline at end of file +} diff --git a/components/chat/open-owie-button.tsx b/components/chat/open-owie-button.tsx index 7a1e9915..321b9b4b 100644 --- a/components/chat/open-owie-button.tsx +++ b/components/chat/open-owie-button.tsx @@ -2,8 +2,8 @@ import { useRouter } from 'next/navigation'; import { useChatContext } from '@/components/chat/chat-provider'; -import { OwButton, type OwButtonSize } from '@/components/ow-ui/ow-button'; -import type { OwButtonColor } from '@/components/ow-ui/ow-button'; +import { OwButton, type OwButtonSize } from '@/components/owui/ow-button'; +import type { OwButtonColor } from '@/components/owui/ow-button'; interface Props { label?: string; diff --git a/components/compare/compare-button.tsx b/components/compare/compare-button.tsx index 89d1246c..107370c4 100644 --- a/components/compare/compare-button.tsx +++ b/components/compare/compare-button.tsx @@ -2,7 +2,7 @@ import {IconScale} from '@tabler/icons-react'; import {useCompareList} from '@/lib/use-compare-list'; -import {OwButton} from '@/components/ow-ui/ow-button'; +import {OwButton} from '@/components/owui/ow-button'; interface Props { card: { diff --git a/components/compare/compare-row-defs.tsx b/components/compare/compare-row-defs.tsx index 3465d41f..70ce1265 100644 --- a/components/compare/compare-row-defs.tsx +++ b/components/compare/compare-row-defs.tsx @@ -1,9 +1,9 @@ import type {Card, CompareResult, CompareTableRow as ApiRow} from '@/lib/api'; import {normalizeCardTypes} from '@/lib/api'; -import {OwBankImage} from '@/components/ow-ui/ow-bank-image'; -import {OwBadge, OwBadges} from '@/components/ow-ui/ow-badge'; -import {OwCardIntentBadges} from '@/components/ow-ui/ow-card-intent-badges'; -import {OwAmount} from "@/components/ow-ui/ow-amount"; +import {OwBankImage} from '@/components/owui/ow-bank-image'; +import {OwBadge, OwBadges} from '@/components/owui/ow-badge'; +import {OwCardIntentBadges} from '@/components/owui/ow-card-intent-badges'; +import {OwAmount} from "@/components/owui/ow-amount"; import {formatDueDate} from '@/lib/card-dates'; // ─── Context ────────────────────────────────────────────────────────────────── diff --git a/components/compare/compare-section.tsx b/components/compare/compare-section.tsx index 422cbd92..e3087c7a 100644 --- a/components/compare/compare-section.tsx +++ b/components/compare/compare-section.tsx @@ -8,7 +8,7 @@ import {cn} from '@/lib/utils'; import type {SearchCard} from '@/lib/search-types'; import type {Card, CompareResult} from '@/lib/api'; import {compareCards, getCard} from '@/lib/api'; -import {OwCardImage} from '@/components/ow-ui/ow-card-image'; +import {OwCardImage} from '@/components/owui/ow-card-image'; import {CardSearchInput} from '@/components/compare/card-search-input'; import {CompareTable} from '@/components/compare/compare-table'; import {RecentCompares} from '@/components/compare/recent-compares'; diff --git a/components/compare/recent-compares.tsx b/components/compare/recent-compares.tsx index 24c88b32..8f44b1a6 100644 --- a/components/compare/recent-compares.tsx +++ b/components/compare/recent-compares.tsx @@ -6,7 +6,7 @@ import {IconX} from '@tabler/icons-react'; import {normalizePair, useRecentCompares} from '@/lib/use-recent-compares'; import {useCardSearch} from '@/lib/use-card-search'; import {getTool} from '@/lib/tools'; -import {OwCardImage} from '@/components/ow-ui/ow-card-image'; +import {OwCardImage} from '@/components/owui/ow-card-image'; const cardBattleHref = getTool('Card Battle').href; diff --git a/components/layout/discord-button.tsx b/components/layout/discord-button.tsx index 325c7f77..18111467 100644 --- a/components/layout/discord-button.tsx +++ b/components/layout/discord-button.tsx @@ -1,5 +1,5 @@ import {IconBrandDiscord} from '@tabler/icons-react'; -import {OwButtonHeader} from '@/components/ow-ui/ow-button-header'; +import {OwButtonHeader} from '@/components/owui/ow-button-header'; interface DiscordButtonProps { iconOnly?: boolean; diff --git a/components/layout/footer.tsx b/components/layout/footer.tsx index f98c6fce..132f2782 100644 --- a/components/layout/footer.tsx +++ b/components/layout/footer.tsx @@ -1,6 +1,6 @@ import Image from 'next/image'; import Link from 'next/link'; -import {OwLogo} from '@/components/ow-ui/ow-logo'; +import {OwLogo} from '@/components/owui/ow-logo'; import {TOOLS} from '@/lib/tools'; import {cn} from '@/lib/utils'; import {ROUTES} from '@/lib/routes'; diff --git a/components/layout/header.tsx b/components/layout/header.tsx index dd2dc0c1..116893fc 100644 --- a/components/layout/header.tsx +++ b/components/layout/header.tsx @@ -2,7 +2,7 @@ import {getBanks, getCards, getNetworks, isHybridCard} from '@/lib/api'; import {MobileNav} from './mobile-nav'; import {SearchDialog} from '@/components/search/search-dialog'; import {Nav2} from "@/components/layout/nav2"; -import {OwLogo} from "@/components/ow-ui/ow-logo"; +import {OwLogo} from "@/components/owui/ow-logo"; import {DiscordButton} from "@/components/layout/discord-button"; const NETWORK_TIER_FILTER = 'visa:infinite,visa:signature,mastercard:world-elite,mastercard:world,amex:platinum,jcb:ultimate'; diff --git a/components/layout/logo.tsx b/components/layout/logo.tsx index efb30303..3be3bef8 100644 --- a/components/layout/logo.tsx +++ b/components/layout/logo.tsx @@ -1 +1 @@ -export { OwLogo as Logo } from '@/components/ow-ui/ow-logo'; +export { OwLogo as Logo } from '@/components/owui/ow-logo'; diff --git a/components/marketing/banks-section.tsx b/components/marketing/banks-section.tsx index aa8d4454..32f2fd26 100644 --- a/components/marketing/banks-section.tsx +++ b/components/marketing/banks-section.tsx @@ -1,6 +1,6 @@ import Link from 'next/link'; import {getBanks} from '@/lib/api'; -import {OwBankRow} from '@/components/ow-ui/ow-bank-row'; +import {OwBankRow} from '@/components/owui/ow-bank-row'; import {ROUTES} from '@/lib/routes'; interface Props { diff --git a/components/marketing/card-ranking-table.tsx b/components/marketing/card-ranking-table.tsx index 530a9bd7..beb512cd 100644 --- a/components/marketing/card-ranking-table.tsx +++ b/components/marketing/card-ranking-table.tsx @@ -2,7 +2,7 @@ import {useState} from 'react'; import type {RankedCard} from '@/lib/card-ranker'; -import {OwCardRankedRow} from '@/components/ow-ui/ow-card-ranked-row'; +import {OwCardRankedRow} from '@/components/owui/ow-card-ranked-row'; import {IconInfoCircle} from '@tabler/icons-react'; interface Props { diff --git a/components/marketing/cards-catalog-teaser.tsx b/components/marketing/cards-catalog-teaser.tsx index 27e8b3c2..aca0b794 100644 --- a/components/marketing/cards-catalog-teaser.tsx +++ b/components/marketing/cards-catalog-teaser.tsx @@ -1,7 +1,7 @@ import Link from 'next/link'; import type {Bank, Card} from '@/lib/api'; import {CardDisplay} from '@/components/cards/variants/card-display'; -import {OwButton} from '@/components/ow-ui/ow-button'; +import {OwButton} from '@/components/owui/ow-button'; import {ROUTES} from '@/lib/routes'; interface Props { diff --git a/components/marketing/category-seo-section.tsx b/components/marketing/category-seo-section.tsx index a45a1521..231dbcdf 100644 --- a/components/marketing/category-seo-section.tsx +++ b/components/marketing/category-seo-section.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react'; -import { OwAccordion } from '@/components/ow-ui/ow-accordion'; +import { OwAccordion } from '@/components/owui/ow-accordion'; export function PersonaIntro({intro}: {intro: ReactNode}) { return ( diff --git a/components/marketing/contact-form.tsx b/components/marketing/contact-form.tsx index 28b7bce5..443ea10c 100644 --- a/components/marketing/contact-form.tsx +++ b/components/marketing/contact-form.tsx @@ -1,4 +1,4 @@ -import {OwButton} from '@/components/ow-ui/ow-button'; +import {OwButton} from '@/components/owui/ow-button'; const TO = 'hello@openwallet.vn'; diff --git a/components/marketing/faq-section.tsx b/components/marketing/faq-section.tsx index 4b471938..6a70ce0b 100644 --- a/components/marketing/faq-section.tsx +++ b/components/marketing/faq-section.tsx @@ -1,4 +1,4 @@ -import {OwAccordion} from '@/components/ow-ui/ow-accordion'; +import {OwAccordion} from '@/components/owui/ow-accordion'; import NeuralBackground from "@/components/ui/flow-field-background"; import Link from 'next/link'; diff --git a/components/marketing/hero-section.tsx b/components/marketing/hero-section.tsx index 470775fe..9545bf47 100644 --- a/components/marketing/hero-section.tsx +++ b/components/marketing/hero-section.tsx @@ -2,9 +2,9 @@ import Image from 'next/image'; import Link from 'next/link'; import {IconCircleCheck, IconCreditCard, IconSettings} from '@tabler/icons-react'; import {ROUTES} from '@/lib/routes'; -import {OwBadgeNumberIcon} from '@/components/ow-ui/ow-badge-number-icon'; -import {OwCardImage} from '@/components/ow-ui/ow-card-image'; -import {OwButton} from '@/components/ow-ui/ow-button'; +import {OwBadgeNumberIcon} from '@/components/owui/ow-badge-number-icon'; +import {OwCardImage} from '@/components/owui/ow-card-image'; +import {OwButton} from '@/components/owui/ow-button'; import {OpenOwieButton} from '@/components/chat/open-owie-button'; export function HeroSection({cardCount, bankCount}: { cardCount: number; bankCount: number }) { diff --git a/components/marketing/persona-categories.tsx b/components/marketing/persona-categories.tsx index 74e2e6f0..11dce8c9 100644 --- a/components/marketing/persona-categories.tsx +++ b/components/marketing/persona-categories.tsx @@ -1,5 +1,5 @@ import {PersonaModel} from '@/lib/persona-model'; -import {OwButton} from '@/components/ow-ui/ow-button'; +import {OwButton} from '@/components/owui/ow-button'; function CategoryLink({persona}: {persona: PersonaModel}) { if (!persona.isAvailable()) { diff --git a/components/marketing/recent-posts-section.tsx b/components/marketing/recent-posts-section.tsx index 53e6a5c2..f6bdd7ea 100644 --- a/components/marketing/recent-posts-section.tsx +++ b/components/marketing/recent-posts-section.tsx @@ -1,5 +1,5 @@ import {getAllPosts} from '@/lib/mdx'; -import {OwFeaturedPosts} from '@/components/ow-ui/ow-featured-posts'; +import {OwFeaturedPosts} from '@/components/owui/ow-featured-posts'; export function RecentPostsSection() { const posts = getAllPosts().slice(0, 4); diff --git a/components/marketing/tools-section.tsx b/components/marketing/tools-section.tsx index 0f9d327e..03ef148d 100644 --- a/components/marketing/tools-section.tsx +++ b/components/marketing/tools-section.tsx @@ -1,7 +1,7 @@ import React from 'react'; import Link from 'next/link'; import {ROUTES} from '@/lib/routes'; -import {OwWobbleCard} from '@/components/ow-ui/ow-wobble-card'; +import {OwWobbleCard} from '@/components/owui/ow-wobble-card'; const TOOLS: {name: string; href: string; description: React.ReactNode; color: string}[] = [ { diff --git a/components/match/card-match-finder.tsx b/components/match/card-match-finder.tsx index 8107657e..bf9db76a 100644 --- a/components/match/card-match-finder.tsx +++ b/components/match/card-match-finder.tsx @@ -5,10 +5,10 @@ import {usePathname, useRouter, useSearchParams} from 'next/navigation'; import {getTool} from '@/lib/tools'; import type {Intent, Persona} from '@/lib/api'; import type {RankedCard} from '@/lib/card-ranker'; -import {OwBadge, OwBadges} from '@/components/ow-ui/ow-badge'; -import {OwCardRankedRow} from '@/components/ow-ui/ow-card-ranked-row'; +import {OwBadge, OwBadges} from '@/components/owui/ow-badge'; +import {OwCardRankedRow} from '@/components/owui/ow-card-ranked-row'; import {cn} from "@/lib/utils"; -import {OwRangeSlider} from '@/components/ow-ui/ow-range-slider'; +import {OwRangeSlider} from '@/components/owui/ow-range-slider'; const STORAGE_KEY = 'ow-rec-prefs'; const cardMatchHref = getTool('Card Match').href; diff --git a/components/ow-ui/ow-tooltip.tsx b/components/ow-ui/ow-tooltip.tsx deleted file mode 100644 index 485f9f17..00000000 --- a/components/ow-ui/ow-tooltip.tsx +++ /dev/null @@ -1,91 +0,0 @@ -"use client"; - -import React, {useId} from "react"; -import {cn} from "@/lib/utils"; - -type TooltipAlign = "top" | "bottom"; - -export interface OwTooltipProps { - children: React.ReactNode; - tooltip?: string | React.ReactNode; - tooltipAlign?: TooltipAlign; - className?: string; - classNameTooltip?: string; -} - -const TOOLTIP_STYLES = ` - @keyframes tooltip-enter-top { - from { opacity: 0; transform: translateY(10px) scale(0.9) rotate(0deg); } - to { opacity: 1; transform: translateY(0) scale(1) rotate(var(--tooltip-rotation)); } - } - @keyframes tooltip-enter-bottom { - from { opacity: 0; transform: translateY(-10px) scale(0.9) rotate(0deg); } - to { opacity: 1; transform: translateY(0) scale(1) rotate(var(--tooltip-rotation)); } - } - @keyframes tooltip-exit { - from { opacity: 1; transform: translateY(0) scale(1) rotate(0deg); } - to { opacity: 0; transform: translateY(-8px) scale(0.95); } - } - .tooltip { - opacity: 0; - animation: tooltip-exit 0.15s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; - } - .tooltip-wrapper:hover .tooltip { - opacity: 1; - animation: tooltip-enter-top 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; - } - .tooltip-wrapper[data-tooltip-align="bottom"]:hover .tooltip { - animation: tooltip-enter-bottom 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; - } -`; - -export function OwTooltip({ - tooltip, - tooltipAlign = "top", - className, - classNameTooltip, - children, -}: OwTooltipProps) { - const id = useId(); - - const rotation = React.useMemo( - () => Math.floor(Math.random() * 11) - 5, - [] - ); - - const tooltipStyle = React.useMemo( - () => ({"--tooltip-rotation": `${rotation}deg`} as React.CSSProperties), - [rotation] - ); - - return ( - <> - -
-
- {children} -
- - {tooltip && ( - - )} -
- - ); -} diff --git a/components/ow-ui/ow-typing-bubble.stories.tsx b/components/owai/ow-typing-bubble.stories.tsx similarity index 92% rename from components/ow-ui/ow-typing-bubble.stories.tsx rename to components/owai/ow-typing-bubble.stories.tsx index 9ab1c879..722f28ac 100644 --- a/components/ow-ui/ow-typing-bubble.stories.tsx +++ b/components/owai/ow-typing-bubble.stories.tsx @@ -1,10 +1,10 @@ import type {Meta, StoryObj} from '@storybook/nextjs-vite'; import {OwTypingBubble} from './ow-typing-bubble'; -import {OwStories, OwStorySection} from './ow-story-section'; +import {OwStories, OwStorySection} from "@/components/owui/ow-story-section"; const meta: Meta = { component: OwTypingBubble, - title: 'Assistant UI/OwTypingBubble', + title: 'OW AI/OwTypingBubble', tags: ['autodocs'], parameters: { docs: { diff --git a/components/ow-ui/ow-typing-bubble.tsx b/components/owai/ow-typing-bubble.tsx similarity index 96% rename from components/ow-ui/ow-typing-bubble.tsx rename to components/owai/ow-typing-bubble.tsx index f74cd264..aac54dde 100644 --- a/components/ow-ui/ow-typing-bubble.tsx +++ b/components/owai/ow-typing-bubble.tsx @@ -1,4 +1,4 @@ -import {OwLogo} from "@/components/ow-ui/ow-logo"; +import {OwLogo} from "@/components/owui/ow-logo"; import type {FC} from "react"; export const OwTypingBubble: FC<{ visible?: boolean }> = ({visible = true}) => { diff --git a/components/ow-ui/ow-accordion.stories.tsx b/components/owui/ow-accordion.stories.tsx similarity index 100% rename from components/ow-ui/ow-accordion.stories.tsx rename to components/owui/ow-accordion.stories.tsx diff --git a/components/ow-ui/ow-accordion.tsx b/components/owui/ow-accordion.tsx similarity index 100% rename from components/ow-ui/ow-accordion.tsx rename to components/owui/ow-accordion.tsx diff --git a/components/ow-ui/ow-alert.stories.tsx b/components/owui/ow-alert.stories.tsx similarity index 100% rename from components/ow-ui/ow-alert.stories.tsx rename to components/owui/ow-alert.stories.tsx diff --git a/components/ow-ui/ow-alert.tsx b/components/owui/ow-alert.tsx similarity index 100% rename from components/ow-ui/ow-alert.tsx rename to components/owui/ow-alert.tsx diff --git a/components/ow-ui/ow-amount.stories.tsx b/components/owui/ow-amount.stories.tsx similarity index 100% rename from components/ow-ui/ow-amount.stories.tsx rename to components/owui/ow-amount.stories.tsx diff --git a/components/ow-ui/ow-amount.tsx b/components/owui/ow-amount.tsx similarity index 100% rename from components/ow-ui/ow-amount.tsx rename to components/owui/ow-amount.tsx diff --git a/components/ow-ui/ow-badge-number-icon.stories.tsx b/components/owui/ow-badge-number-icon.stories.tsx similarity index 100% rename from components/ow-ui/ow-badge-number-icon.stories.tsx rename to components/owui/ow-badge-number-icon.stories.tsx diff --git a/components/ow-ui/ow-badge-number-icon.tsx b/components/owui/ow-badge-number-icon.tsx similarity index 100% rename from components/ow-ui/ow-badge-number-icon.tsx rename to components/owui/ow-badge-number-icon.tsx diff --git a/components/ow-ui/ow-badge.stories.tsx b/components/owui/ow-badge.stories.tsx similarity index 100% rename from components/ow-ui/ow-badge.stories.tsx rename to components/owui/ow-badge.stories.tsx diff --git a/components/ow-ui/ow-badge.tsx b/components/owui/ow-badge.tsx similarity index 100% rename from components/ow-ui/ow-badge.tsx rename to components/owui/ow-badge.tsx diff --git a/components/ow-ui/ow-bank-image.stories.tsx b/components/owui/ow-bank-image.stories.tsx similarity index 100% rename from components/ow-ui/ow-bank-image.stories.tsx rename to components/owui/ow-bank-image.stories.tsx diff --git a/components/ow-ui/ow-bank-image.tsx b/components/owui/ow-bank-image.tsx similarity index 100% rename from components/ow-ui/ow-bank-image.tsx rename to components/owui/ow-bank-image.tsx diff --git a/components/ow-ui/ow-bank-row.stories.tsx b/components/owui/ow-bank-row.stories.tsx similarity index 100% rename from components/ow-ui/ow-bank-row.stories.tsx rename to components/owui/ow-bank-row.stories.tsx diff --git a/components/ow-ui/ow-bank-row.tsx b/components/owui/ow-bank-row.tsx similarity index 93% rename from components/ow-ui/ow-bank-row.tsx rename to components/owui/ow-bank-row.tsx index 637639d6..d8270a1a 100644 --- a/components/ow-ui/ow-bank-row.tsx +++ b/components/owui/ow-bank-row.tsx @@ -3,10 +3,10 @@ import Link from 'next/link'; import type {Bank} from '@/lib/api'; import {BankModel} from '@/lib/bank-model'; -import {OwBankImage} from '@/components/ow-ui/ow-bank-image'; -import {OwButton} from '@/components/ow-ui/ow-button'; +import {OwBankImage} from '@/components/owui/ow-bank-image'; +import {OwButton} from '@/components/owui/ow-button'; import {IconAffiliateFilled, IconCreditCardFilled} from "@tabler/icons-react"; -import {OwBadge} from "@/components/ow-ui/ow-badge"; +import {OwBadge} from "@/components/owui/ow-badge"; interface Props { bank: Bank; diff --git a/components/ow-ui/ow-button-header.stories.tsx b/components/owui/ow-button-header.stories.tsx similarity index 100% rename from components/ow-ui/ow-button-header.stories.tsx rename to components/owui/ow-button-header.stories.tsx diff --git a/components/ow-ui/ow-button-header.tsx b/components/owui/ow-button-header.tsx similarity index 96% rename from components/ow-ui/ow-button-header.tsx rename to components/owui/ow-button-header.tsx index 92d40ad3..361e24ca 100644 --- a/components/ow-ui/ow-button-header.tsx +++ b/components/owui/ow-button-header.tsx @@ -1,7 +1,7 @@ import * as React from "react" import Link from "next/link" import {cn} from "@/lib/utils" -import {OwLogo} from "@/components/ow-ui/ow-logo" +import {OwLogo} from "@/components/owui/ow-logo" type Props = React.ComponentProps<"button"> & { icon?: React.ReactNode diff --git a/components/ow-ui/ow-button.stories.tsx b/components/owui/ow-button.stories.tsx similarity index 100% rename from components/ow-ui/ow-button.stories.tsx rename to components/owui/ow-button.stories.tsx diff --git a/components/ow-ui/ow-button.tsx b/components/owui/ow-button.tsx similarity index 100% rename from components/ow-ui/ow-button.tsx rename to components/owui/ow-button.tsx diff --git a/components/ow-ui/ow-card-cashback-rule.stories.tsx b/components/owui/ow-card-cashback-rule.stories.tsx similarity index 100% rename from components/ow-ui/ow-card-cashback-rule.stories.tsx rename to components/owui/ow-card-cashback-rule.stories.tsx diff --git a/components/ow-ui/ow-card-cashback-rule.tsx b/components/owui/ow-card-cashback-rule.tsx similarity index 99% rename from components/ow-ui/ow-card-cashback-rule.tsx rename to components/owui/ow-card-cashback-rule.tsx index ef9dd8c8..3c8afef7 100644 --- a/components/ow-ui/ow-card-cashback-rule.tsx +++ b/components/owui/ow-card-cashback-rule.tsx @@ -1,8 +1,8 @@ import type {CashbackRule, Merchant, SpendTier} from '@/lib/api'; import {IntentModel} from '@/lib/intent-model'; import {cn} from '@/lib/utils'; -import {OwBadge} from '@/components/ow-ui/ow-badge'; -import {OwAmount} from '@/components/ow-ui/ow-amount'; +import {OwBadge} from '@/components/owui/ow-badge'; +import {OwAmount} from '@/components/owui/ow-amount'; import {CATCHALL_SLUGS} from '@/lib/cashback-utils'; import { IconBuildingStore, diff --git a/components/ow-ui/ow-card-image.stories.tsx b/components/owui/ow-card-image.stories.tsx similarity index 100% rename from components/ow-ui/ow-card-image.stories.tsx rename to components/owui/ow-card-image.stories.tsx diff --git a/components/ow-ui/ow-card-image.tsx b/components/owui/ow-card-image.tsx similarity index 100% rename from components/ow-ui/ow-card-image.tsx rename to components/owui/ow-card-image.tsx diff --git a/components/ow-ui/ow-card-intent-badges.stories.tsx b/components/owui/ow-card-intent-badges.stories.tsx similarity index 100% rename from components/ow-ui/ow-card-intent-badges.stories.tsx rename to components/owui/ow-card-intent-badges.stories.tsx diff --git a/components/ow-ui/ow-card-intent-badges.tsx b/components/owui/ow-card-intent-badges.tsx similarity index 96% rename from components/ow-ui/ow-card-intent-badges.tsx rename to components/owui/ow-card-intent-badges.tsx index fddf329d..bdf53187 100644 --- a/components/ow-ui/ow-card-intent-badges.tsx +++ b/components/owui/ow-card-intent-badges.tsx @@ -3,7 +3,7 @@ import type {Card, Intent} from '@/lib/api'; import {IntentModel} from '@/lib/intent-model'; import {CardModel} from '@/lib/card-model'; -import {OwBadge, OwBadges} from '@/components/ow-ui/ow-badge'; +import {OwBadge, OwBadges} from '@/components/owui/ow-badge'; import {useIntentMap} from '@/lib/intent-map-context'; export interface IntentItem { diff --git a/components/ow-ui/ow-card-ranked-row.stories.tsx b/components/owui/ow-card-ranked-row.stories.tsx similarity index 100% rename from components/ow-ui/ow-card-ranked-row.stories.tsx rename to components/owui/ow-card-ranked-row.stories.tsx diff --git a/components/ow-ui/ow-card-ranked-row.tsx b/components/owui/ow-card-ranked-row.tsx similarity index 97% rename from components/ow-ui/ow-card-ranked-row.tsx rename to components/owui/ow-card-ranked-row.tsx index 633b602d..9975ce6b 100644 --- a/components/ow-ui/ow-card-ranked-row.tsx +++ b/components/owui/ow-card-ranked-row.tsx @@ -4,11 +4,11 @@ import type {RankedCard} from '@/lib/card-ranker'; import {CATCHALL_SLUGS} from '@/lib/card-display-utils'; import {IconAlertTriangle, IconBulb, IconCaretDownFilled, IconCaretUpFilled} from '@tabler/icons-react'; import {CardModel} from '@/lib/card-model'; -import {OwCardImage} from '@/components/ow-ui/ow-card-image'; -import {OwRankBadge} from '@/components/ow-ui/ow-rank-badge'; -import {OwAmount} from '@/components/ow-ui/ow-amount'; -import {OwBadge, OwBadges} from '@/components/ow-ui/ow-badge'; -import {OwCardIntentBadges} from '@/components/ow-ui/ow-card-intent-badges'; +import {OwCardImage} from '@/components/owui/ow-card-image'; +import {OwRankBadge} from '@/components/owui/ow-rank-badge'; +import {OwAmount} from '@/components/owui/ow-amount'; +import {OwBadge, OwBadges} from '@/components/owui/ow-badge'; +import {OwCardIntentBadges} from '@/components/owui/ow-card-intent-badges'; type IntentMap = Map>; diff --git a/components/ow-ui/ow-featured-posts.stories.tsx b/components/owui/ow-featured-posts.stories.tsx similarity index 100% rename from components/ow-ui/ow-featured-posts.stories.tsx rename to components/owui/ow-featured-posts.stories.tsx diff --git a/components/ow-ui/ow-featured-posts.tsx b/components/owui/ow-featured-posts.tsx similarity index 98% rename from components/ow-ui/ow-featured-posts.tsx rename to components/owui/ow-featured-posts.tsx index 7d3fecef..26237264 100644 --- a/components/ow-ui/ow-featured-posts.tsx +++ b/components/owui/ow-featured-posts.tsx @@ -3,7 +3,7 @@ import type {Post} from '@/lib/mdx'; import {resolvePosts} from '@/lib/posts-utils'; import {ROUTES} from '@/lib/routes'; import {OwPostCategoryDate} from './ow-post-category-date'; -import {OwButton} from "@/components/ow-ui/ow-button"; +import {OwButton} from "@/components/owui/ow-button"; function FeaturedGridItem({post}: { post: Post }) { return ( diff --git a/components/ow-ui/ow-logo.stories.tsx b/components/owui/ow-logo.stories.tsx similarity index 100% rename from components/ow-ui/ow-logo.stories.tsx rename to components/owui/ow-logo.stories.tsx diff --git a/components/ow-ui/ow-logo.tsx b/components/owui/ow-logo.tsx similarity index 100% rename from components/ow-ui/ow-logo.tsx rename to components/owui/ow-logo.tsx diff --git a/components/ow-ui/ow-owie-fab.stories.tsx b/components/owui/ow-owie-fab.stories.tsx similarity index 100% rename from components/ow-ui/ow-owie-fab.stories.tsx rename to components/owui/ow-owie-fab.stories.tsx diff --git a/components/ow-ui/ow-owie-fab.tsx b/components/owui/ow-owie-fab.tsx similarity index 94% rename from components/ow-ui/ow-owie-fab.tsx rename to components/owui/ow-owie-fab.tsx index da0b81a6..441609a8 100644 --- a/components/ow-ui/ow-owie-fab.tsx +++ b/components/owui/ow-owie-fab.tsx @@ -3,8 +3,8 @@ import * as React from 'react'; import {usePathname, useRouter} from 'next/navigation'; import {cn} from '@/lib/utils'; -import {OwLogo} from '@/components/ow-ui/ow-logo'; -import {OwTooltip} from '@/components/ow-ui/ow-tooltip'; +import {OwLogo} from '@/components/owui/ow-logo'; +import {OwTooltip} from '@/components/owui/ow-tooltip'; import {useChatContext} from '@/components/chat/chat-provider'; import {MovingBorder} from '@/components/phucbm/moving-border'; diff --git a/components/ow-ui/ow-post-category-date.tsx b/components/owui/ow-post-category-date.tsx similarity index 100% rename from components/ow-ui/ow-post-category-date.tsx rename to components/owui/ow-post-category-date.tsx diff --git a/components/ow-ui/ow-post-list.stories.tsx b/components/owui/ow-post-list.stories.tsx similarity index 100% rename from components/ow-ui/ow-post-list.stories.tsx rename to components/owui/ow-post-list.stories.tsx diff --git a/components/ow-ui/ow-post-list.tsx b/components/owui/ow-post-list.tsx similarity index 100% rename from components/ow-ui/ow-post-list.tsx rename to components/owui/ow-post-list.tsx diff --git a/components/ow-ui/ow-range-slider.stories.tsx b/components/owui/ow-range-slider.stories.tsx similarity index 100% rename from components/ow-ui/ow-range-slider.stories.tsx rename to components/owui/ow-range-slider.stories.tsx diff --git a/components/ow-ui/ow-range-slider.tsx b/components/owui/ow-range-slider.tsx similarity index 97% rename from components/ow-ui/ow-range-slider.tsx rename to components/owui/ow-range-slider.tsx index 11351a73..f764e854 100644 --- a/components/ow-ui/ow-range-slider.tsx +++ b/components/owui/ow-range-slider.tsx @@ -2,7 +2,7 @@ import {Slider, Tooltip} from 'radix-ui'; import {cn} from '@/lib/utils'; -import {OwLogo} from '@/components/ow-ui/ow-logo'; +import {OwLogo} from '@/components/owui/ow-logo'; export interface OwRangeSliderProps { min?: number; diff --git a/components/ow-ui/ow-rank-badge.stories.tsx b/components/owui/ow-rank-badge.stories.tsx similarity index 100% rename from components/ow-ui/ow-rank-badge.stories.tsx rename to components/owui/ow-rank-badge.stories.tsx diff --git a/components/ow-ui/ow-rank-badge.tsx b/components/owui/ow-rank-badge.tsx similarity index 100% rename from components/ow-ui/ow-rank-badge.tsx rename to components/owui/ow-rank-badge.tsx diff --git a/components/ow-ui/ow-source-list.stories.tsx b/components/owui/ow-source-list.stories.tsx similarity index 100% rename from components/ow-ui/ow-source-list.stories.tsx rename to components/owui/ow-source-list.stories.tsx diff --git a/components/ow-ui/ow-source-list.tsx b/components/owui/ow-source-list.tsx similarity index 100% rename from components/ow-ui/ow-source-list.tsx rename to components/owui/ow-source-list.tsx diff --git a/components/ow-ui/ow-story-constants.ts b/components/owui/ow-story-constants.ts similarity index 100% rename from components/ow-ui/ow-story-constants.ts rename to components/owui/ow-story-constants.ts diff --git a/components/ow-ui/ow-story-section.tsx b/components/owui/ow-story-section.tsx similarity index 100% rename from components/ow-ui/ow-story-section.tsx rename to components/owui/ow-story-section.tsx diff --git a/components/owui/ow-tooltip-icon-button.stories.tsx b/components/owui/ow-tooltip-icon-button.stories.tsx new file mode 100644 index 00000000..527fae40 --- /dev/null +++ b/components/owui/ow-tooltip-icon-button.stories.tsx @@ -0,0 +1,86 @@ +import type {Meta, StoryObj} from '@storybook/nextjs-vite'; +import {OwTooltipIconButton} from './ow-tooltip-icon-button'; +import {OwStories, OwStorySection} from './ow-story-section'; +import {Copy, Heart, Share2, Trash2} from 'lucide-react'; + +const meta: Meta = { + component: OwTooltipIconButton, + title: 'OW UI/OwTooltipIconButton', + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: [ + 'Ghost icon button with animated `OwTooltip`. Forwards ref to underlying ` + + ); + } +); + +OwTooltipIconButton.displayName = "OwTooltipIconButton"; diff --git a/components/ow-ui/ow-tooltip.stories.tsx b/components/owui/ow-tooltip.stories.tsx similarity index 96% rename from components/ow-ui/ow-tooltip.stories.tsx rename to components/owui/ow-tooltip.stories.tsx index f26c01fb..74598b41 100644 --- a/components/ow-ui/ow-tooltip.stories.tsx +++ b/components/owui/ow-tooltip.stories.tsx @@ -37,7 +37,7 @@ export const Overview: Story = { - + @@ -65,7 +65,7 @@ export const Top: Story = { export const Bottom: Story = { args: { tooltip: 'Tooltip on bottom', - tooltipAlign: 'bottom', + side: 'bottom', children: , }, }; diff --git a/components/owui/ow-tooltip.tsx b/components/owui/ow-tooltip.tsx new file mode 100644 index 00000000..2593324a --- /dev/null +++ b/components/owui/ow-tooltip.tsx @@ -0,0 +1,95 @@ +"use client"; + +import React, {useId, useState} from "react"; +import {cn} from "@/lib/utils"; + +type TooltipAlign = "top" | "bottom"; + +export interface OwTooltipProps { + children: React.ReactNode; + tooltip?: string | React.ReactNode; + side?: TooltipAlign; + className?: string; + classNameTooltip?: string; +} + +const TOOLTIP_KEYFRAMES = ` + @keyframes tooltip-enter-top { + from { opacity: 0; transform: translateY(10px) scale(0.9) rotate(0deg); } + to { opacity: 1; transform: translateY(0) scale(1) rotate(var(--tooltip-rotation)); } + } + @keyframes tooltip-enter-bottom { + from { opacity: 0; transform: translateY(-10px) scale(0.9) rotate(0deg); } + to { opacity: 1; transform: translateY(0) scale(1) rotate(var(--tooltip-rotation)); } + } + @keyframes tooltip-exit { + from { opacity: 1; transform: translateY(0) scale(1) rotate(0deg); } + to { opacity: 0; transform: translateY(-8px) scale(0.95); } + } + .ow-tooltip-bubble { + animation: tooltip-exit 0.15s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; + } + .ow-tooltip-trigger:hover .ow-tooltip-bubble, + .ow-tooltip-trigger:focus-within .ow-tooltip-bubble { + animation: tooltip-enter-top 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; + } + .ow-tooltip-trigger[data-tooltip-align="bottom"]:hover .ow-tooltip-bubble, + .ow-tooltip-trigger[data-tooltip-align="bottom"]:focus-within .ow-tooltip-bubble { + animation: tooltip-enter-bottom 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; + } +`; + +export function OwTooltip({ + tooltip, + side = "top", + className, + classNameTooltip, + children, + }: OwTooltipProps) { + const id = useId(); + const [rotation] = useState(() => Math.floor(Math.random() * 11) - 5); + const [hasInteracted, setHasInteracted] = useState(false); + + const child = React.Children.only(children) as React.ReactElement>; + + const trigger = React.cloneElement(child, { + ...child.props, + className: cn("ow-tooltip-trigger relative", child.props.className, className), + "aria-describedby": tooltip ? id : undefined, + "data-tooltip-align": side, + onMouseEnter: (e: React.MouseEvent) => { + setHasInteracted(true); + child.props.onMouseEnter?.(e); + }, + onFocus: (e: React.FocusEvent) => { + setHasInteracted(true); + child.props.onFocus?.(e); + }, + } as React.HTMLAttributes); + + return ( + <> + {React.cloneElement(trigger, {}, [ + ...React.Children.toArray(trigger.props.children), + tooltip && hasInteracted ? ( + + + + + ) : null, + ])} + + ); +} diff --git a/components/ow-ui/ow-traffic-lights.stories.tsx b/components/owui/ow-traffic-lights.stories.tsx similarity index 100% rename from components/ow-ui/ow-traffic-lights.stories.tsx rename to components/owui/ow-traffic-lights.stories.tsx diff --git a/components/ow-ui/ow-traffic-lights.tsx b/components/owui/ow-traffic-lights.tsx similarity index 97% rename from components/ow-ui/ow-traffic-lights.tsx rename to components/owui/ow-traffic-lights.tsx index 307c29ec..93b5c1c4 100644 --- a/components/ow-ui/ow-traffic-lights.tsx +++ b/components/owui/ow-traffic-lights.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import {cn} from '@/lib/utils'; -import {OwTooltip} from '@/components/ow-ui/ow-tooltip'; +import {OwTooltip} from '@/components/owui/ow-tooltip'; export type TrafficLightButton = { id: string; diff --git a/components/ow-ui/ow-wobble-card.stories.tsx b/components/owui/ow-wobble-card.stories.tsx similarity index 100% rename from components/ow-ui/ow-wobble-card.stories.tsx rename to components/owui/ow-wobble-card.stories.tsx diff --git a/components/ow-ui/ow-wobble-card.tsx b/components/owui/ow-wobble-card.tsx similarity index 100% rename from components/ow-ui/ow-wobble-card.tsx rename to components/owui/ow-wobble-card.tsx diff --git a/components/ow-ui/typography.stories.tsx b/components/owui/typography.stories.tsx similarity index 100% rename from components/ow-ui/typography.stories.tsx rename to components/owui/typography.stories.tsx diff --git a/components/search/search-trigger.tsx b/components/search/search-trigger.tsx index a8c399db..15aaafaa 100644 --- a/components/search/search-trigger.tsx +++ b/components/search/search-trigger.tsx @@ -1,6 +1,6 @@ import {IconSearch} from '@tabler/icons-react'; import {cn} from '@/lib/utils'; -import {OwButtonHeader} from '@/components/ow-ui/ow-button-header'; +import {OwButtonHeader} from '@/components/owui/ow-button-header'; interface SearchTriggerProps { onClick: () => void; diff --git a/components/wallet/add/card-selection-step.tsx b/components/wallet/add/card-selection-step.tsx index a19c2806..16bd4e9d 100644 --- a/components/wallet/add/card-selection-step.tsx +++ b/components/wallet/add/card-selection-step.tsx @@ -1,6 +1,6 @@ import { IconCheck } from '@tabler/icons-react'; import { getBankImageUrl, type Bank, type Card } from '@/lib/api'; -import {OwCardImage} from '@/components/ow-ui/ow-card-image'; +import {OwCardImage} from '@/components/owui/ow-card-image'; export function CardSelectionStep({ bank, diff --git a/components/wallet/card-detail-form.tsx b/components/wallet/card-detail-form.tsx index 282e62eb..d92391d3 100644 --- a/components/wallet/card-detail-form.tsx +++ b/components/wallet/card-detail-form.tsx @@ -12,14 +12,14 @@ import { appDb } from '@/lib/app-db'; import { type Card, type Bank } from '@/lib/api'; import type { WalletCard, CreditAccount, CardStatus } from '@/lib/db'; import type { AppWallet } from '@/lib/app-db'; -import {OwCardImage} from '@/components/ow-ui/ow-card-image'; +import {OwCardImage} from '@/components/owui/ow-card-image'; import { FormField } from '@/components/ui/form-field'; import { CreditPoolSelector, type PoolSelection } from './credit-pool-selector'; import { MoveToWalletPicker } from './move-to-wallet-picker'; import { useWalletDb, useActiveWallet } from '@/providers/wallet-db-provider'; import { useLiveQuery } from 'dexie-react-hooks'; import posthog from 'posthog-js'; -import { OwAmount } from '@/components/ow-ui/ow-amount'; +import { OwAmount } from '@/components/owui/ow-amount'; // ─── Constants ──────────────────────────────────────────────────────────────── diff --git a/components/wallet/payment-row.tsx b/components/wallet/payment-row.tsx index 91e2d4bf..4d5ed234 100644 --- a/components/wallet/payment-row.tsx +++ b/components/wallet/payment-row.tsx @@ -3,7 +3,7 @@ import {CardModel} from '@/lib/card-model'; import type {WalletCard} from '@/lib/db'; import {cn} from '@/lib/utils'; import {CardTimeline, CardTimelineSummary} from './card-timeline'; -import {OwCardImage} from '@/components/ow-ui/ow-card-image'; +import {OwCardImage} from '@/components/owui/ow-card-image'; import React from "react"; /** diff --git a/components/wallet/wallet-card-badges.tsx b/components/wallet/wallet-card-badges.tsx index b00caaa2..88e488d2 100644 --- a/components/wallet/wallet-card-badges.tsx +++ b/components/wallet/wallet-card-badges.tsx @@ -1,5 +1,5 @@ import { DashedBadge } from '@/components/ui/dashed-badge'; -import { OwAmount } from '@/components/ow-ui/ow-amount'; +import { OwAmount } from '@/components/owui/ow-amount'; import type { WalletCard } from '@/lib/db'; import type { Card } from '@/lib/api'; import type { CreditBadge } from './wallet-card-row'; diff --git a/components/wallet/wallet-card-row.tsx b/components/wallet/wallet-card-row.tsx index 8dca6073..604403be 100644 --- a/components/wallet/wallet-card-row.tsx +++ b/components/wallet/wallet-card-row.tsx @@ -4,7 +4,7 @@ import Link from 'next/link'; import { cn } from '@/lib/utils'; import { type Bank, type Card } from '@/lib/api'; import { getMyCardUrl } from '@/lib/routes'; -import {OwCardImage} from '@/components/ow-ui/ow-card-image'; +import {OwCardImage} from '@/components/owui/ow-card-image'; import type { CardStatus, WalletCard } from '@/lib/db'; import { WalletCardBadges } from './wallet-card-badges'; diff --git a/package.json b/package.json index 325bdd38..631d3b8b 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,8 @@ "zustand": "^5.0.13" }, "devDependencies": { + "@chromatic-com/storybook": "^5.2.1", + "@github-ui/storybook-addon-performance-panel": "^1.1.4", "@inquirer/prompts": "^8.5.2", "@storybook/addon-a11y": "^10.4.1", "@storybook/addon-docs": "^10.4.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c08a35d..3390b444 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -174,6 +174,12 @@ importers: specifier: ^5.0.13 version: 5.0.13(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) devDependencies: + '@chromatic-com/storybook': + specifier: ^5.2.1 + version: 5.2.1(storybook@10.4.1(@testing-library/dom@10.4.1)(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + '@github-ui/storybook-addon-performance-panel': + specifier: ^1.1.4 + version: 1.1.4(@storybook/icons@2.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@storybook/react@10.4.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.4.1(@testing-library/dom@10.4.1)(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3))(react@19.2.4)(storybook@10.4.1(@testing-library/dom@10.4.1)(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) '@inquirer/prompts': specifier: ^8.5.2 version: 8.5.2(@types/node@20.19.33) @@ -554,6 +560,12 @@ packages: '@blazediff/core@1.9.1': resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==} + '@chromatic-com/storybook@5.2.1': + resolution: {integrity: sha512-z6I7NJk/0VngA64y5TNYaB4Hc2X8+90n4op6lBt9PvWk5TmIlFLDqdX33rlrwbNRkkYijVgA/wO04rVYXi5Mlg==} + engines: {node: '>=20.0.0', yarn: '>=1.22.18'} + peerDependencies: + storybook: ^0.0.0-0 || ^10.1.0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 || ^10.5.0-0 || ^10.6.0-0 + '@codemirror/autocomplete@6.20.0': resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==} @@ -814,6 +826,19 @@ packages: '@floating-ui/vue@1.1.9': resolution: {integrity: sha512-BfNqNW6KA83Nexspgb9DZuz578R7HT8MZw1CfK9I6Ah4QReNWEJsXWHN+SdmOVLNGmTPDi+fDT535Df5PzMLbQ==} + '@github-ui/storybook-addon-performance-panel@1.1.4': + resolution: {integrity: sha512-clVeSoS1BM2z+nHffOXpnA1J034XALEDfJez6yg7TS7Clfn8eBjgw5FwLjfMvM+kWeG2nwfP6LujRMvJDqj+yg==} + peerDependencies: + '@storybook/icons': ^2.0.0 + '@storybook/react': ^10.0.0 + react: ^19 + storybook: ^10 + peerDependenciesMeta: + '@storybook/react': + optional: true + react: + optional: true + '@grpc/grpc-js@1.14.4': resolution: {integrity: sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==} engines: {node: '>=12.10.0'} @@ -1291,6 +1316,9 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@neoconfetti/react@1.0.0': + resolution: {integrity: sha512-klcSooChXXOzIm+SE5IISIAn3bYzYfPjbX7D7HoqZL84oAfgREeSg5vSIaSFH+DaGzzvImTyWe1OyrJ67vik4A==} + '@next/env@16.0.0': resolution: {integrity: sha512-s5j2iFGp38QsG1LWRQaE2iUY3h1jc014/melHFfLdrsMJPqxqDQwWNwyQTcNoUSGZlCVZuM7t7JDMmSyRilsnA==} @@ -3735,6 +3763,21 @@ packages: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} + chromatic@16.10.0: + resolution: {integrity: sha512-nFsztmnu7rFiGafUJgXvLUNpqmRylz92eNvzBoJNTKKQj4EQUyxznwnfpf1dTs7hXtWD8JwcH92jADydaHA1sw==} + hasBin: true + peerDependencies: + '@chromatic-com/cypress': ^0.*.* || ^1.0.0 + '@chromatic-com/playwright': ^0.*.* || ^1.0.0 + '@chromatic-com/vitest': ^0.*.* || ^1.0.0 + peerDependenciesMeta: + '@chromatic-com/cypress': + optional: true + '@chromatic-com/playwright': + optional: true + '@chromatic-com/vitest': + optional: true + cjs-module-lexer@2.2.0: resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} @@ -7056,6 +7099,18 @@ snapshots: '@blazediff/core@1.9.1': {} + '@chromatic-com/storybook@5.2.1(storybook@10.4.1(@testing-library/dom@10.4.1)(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': + dependencies: + '@neoconfetti/react': 1.0.0 + chromatic: 16.10.0 + jsonfile: 6.2.0 + storybook: 10.4.1(@testing-library/dom@10.4.1)(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + strip-ansi: 7.1.2 + transitivePeerDependencies: + - '@chromatic-com/cypress' + - '@chromatic-com/playwright' + - '@chromatic-com/vitest' + '@codemirror/autocomplete@6.20.0': dependencies: '@codemirror/language': 6.12.1 @@ -7327,6 +7382,14 @@ snapshots: - '@vue/composition-api' - vue + '@github-ui/storybook-addon-performance-panel@1.1.4(@storybook/icons@2.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@storybook/react@10.4.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.4.1(@testing-library/dom@10.4.1)(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3))(react@19.2.4)(storybook@10.4.1(@testing-library/dom@10.4.1)(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': + dependencies: + '@storybook/icons': 2.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.4.1(@testing-library/dom@10.4.1)(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + optionalDependencies: + '@storybook/react': 10.4.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.4.1(@testing-library/dom@10.4.1)(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + react: 19.2.4 + '@grpc/grpc-js@1.14.4': dependencies: '@grpc/proto-loader': 0.8.1 @@ -7792,6 +7855,8 @@ snapshots: '@tybys/wasm-util': 0.10.2 optional: true + '@neoconfetti/react@1.0.0': {} + '@next/env@16.0.0': {} '@next/env@16.2.6': {} @@ -10515,6 +10580,10 @@ snapshots: check-error@2.1.3: {} + chromatic@16.10.0: + dependencies: + semver: 7.7.4 + cjs-module-lexer@2.2.0: {} class-variance-authority@0.7.1: