@adres/mgda-ui 0.0.0-stage → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/brand-knowledge.json +451 -0
- package/knowledge/accordion.md +135 -0
- package/knowledge/activity-rings.md +123 -0
- package/knowledge/alert.md +184 -0
- package/knowledge/answer-actions.md +217 -0
- package/knowledge/aspect-ratio.md +82 -0
- package/knowledge/avatar.md +158 -0
- package/knowledge/badge.md +172 -0
- package/knowledge/bar-graph.md +167 -0
- package/knowledge/bento-grid.md +115 -0
- package/knowledge/blockquote.md +153 -0
- package/knowledge/box-list.md +177 -0
- package/knowledge/bullet-list.md +128 -0
- package/knowledge/button-group.md +179 -0
- package/knowledge/button.md +231 -0
- package/knowledge/calendar.md +168 -0
- package/knowledge/card.md +321 -0
- package/knowledge/cards-nav.md +170 -0
- package/knowledge/chain-of-thought.md +164 -0
- package/knowledge/chat-container.md +135 -0
- package/knowledge/chat-message.md +224 -0
- package/knowledge/checkbox-card.md +172 -0
- package/knowledge/checkbox.md +111 -0
- package/knowledge/citations.md +241 -0
- package/knowledge/combobox.md +265 -0
- package/knowledge/countdown-pill.md +153 -0
- package/knowledge/cta.md +118 -0
- package/knowledge/data-list.md +173 -0
- package/knowledge/data-table.md +220 -0
- package/knowledge/dialog.md +249 -0
- package/knowledge/drawer.md +236 -0
- package/knowledge/dropdown-menu.md +238 -0
- package/knowledge/dropzone.md +153 -0
- package/knowledge/email-chips-input.md +161 -0
- package/knowledge/empty-state.md +106 -0
- package/knowledge/entity-header.md +166 -0
- package/knowledge/feature-grid.md +120 -0
- package/knowledge/feedback-bar.md +127 -0
- package/knowledge/footer.md +116 -0
- package/knowledge/heading.md +189 -0
- package/knowledge/hero.md +361 -0
- package/knowledge/input-button.md +187 -0
- package/knowledge/input-group.md +185 -0
- package/knowledge/input.md +256 -0
- package/knowledge/inset.md +177 -0
- package/knowledge/kpi-card.md +206 -0
- package/knowledge/list-nav.md +181 -0
- package/knowledge/logo-cloud.md +102 -0
- package/knowledge/media-gallery.md +158 -0
- package/knowledge/message-actions.md +175 -0
- package/knowledge/nav-sidebar.md +225 -0
- package/knowledge/number-stepper.md +159 -0
- package/knowledge/page-header.md +109 -0
- package/knowledge/pagination.md +225 -0
- package/knowledge/popover.md +159 -0
- package/knowledge/pricing.md +132 -0
- package/knowledge/progress.md +156 -0
- package/knowledge/prompt-input.md +337 -0
- package/knowledge/prompt-suggestions.md +168 -0
- package/knowledge/quota-bar.md +160 -0
- package/knowledge/quote.md +116 -0
- package/knowledge/radio-card.md +172 -0
- package/knowledge/radio-group.md +135 -0
- package/knowledge/reasoning.md +142 -0
- package/knowledge/search.md +131 -0
- package/knowledge/section-nav.md +162 -0
- package/knowledge/segmented-control.md +181 -0
- package/knowledge/select-button.md +205 -0
- package/knowledge/select.md +210 -0
- package/knowledge/separator.md +75 -0
- package/knowledge/shimmer-text.md +140 -0
- package/knowledge/sidebar.md +227 -0
- package/knowledge/skeleton.md +142 -0
- package/knowledge/slider.md +133 -0
- package/knowledge/slideshow.md +159 -0
- package/knowledge/source.md +156 -0
- package/knowledge/spinner.md +120 -0
- package/knowledge/stat-strip.md +128 -0
- package/knowledge/stats.md +117 -0
- package/knowledge/step-indicator.md +157 -0
- package/knowledge/switch.md +88 -0
- package/knowledge/table.md +190 -0
- package/knowledge/tabs.md +169 -0
- package/knowledge/testimonials.md +115 -0
- package/knowledge/text-link.md +192 -0
- package/knowledge/text.md +194 -0
- package/knowledge/thinking-bar.md +115 -0
- package/knowledge/thinking-steps.md +140 -0
- package/knowledge/toaster.md +127 -0
- package/knowledge/toggle.md +113 -0
- package/knowledge/tool-call-card.md +241 -0
- package/knowledge/tooltip.md +120 -0
- package/knowledge/top-nav.md +138 -0
- package/knowledge.json +19854 -0
- package/package.json +94 -4
- package/src/components/accordion.stories.tsx +46 -0
- package/src/components/accordion.tsx +74 -0
- package/src/components/activity-rings.stories.tsx +73 -0
- package/src/components/activity-rings.tsx +121 -0
- package/src/components/alert.stories.tsx +43 -0
- package/src/components/alert.tsx +132 -0
- package/src/components/answer-actions.stories.tsx +54 -0
- package/src/components/answer-actions.test.tsx +37 -0
- package/src/components/answer-actions.tsx +313 -0
- package/src/components/aspect-ratio.stories.tsx +32 -0
- package/src/components/aspect-ratio.tsx +5 -0
- package/src/components/avatar.stories.tsx +40 -0
- package/src/components/avatar.tsx +154 -0
- package/src/components/badge.stories.tsx +27 -0
- package/src/components/badge.tsx +63 -0
- package/src/components/bar-graph.stories.tsx +28 -0
- package/src/components/bar-graph.tsx +204 -0
- package/src/components/bento-grid.stories.tsx +23 -0
- package/src/components/bento-grid.tsx +67 -0
- package/src/components/blockquote.stories.tsx +28 -0
- package/src/components/blockquote.tsx +88 -0
- package/src/components/box-list.stories.tsx +25 -0
- package/src/components/box-list.tsx +108 -0
- package/src/components/bullet-list.stories.tsx +22 -0
- package/src/components/bullet-list.tsx +101 -0
- package/src/components/button-group.stories.tsx +31 -0
- package/src/components/button-group.tsx +156 -0
- package/src/components/button.stories.tsx +66 -0
- package/src/components/button.test.ts +17 -0
- package/src/components/button.tsx +139 -0
- package/src/components/calendar.stories.tsx +25 -0
- package/src/components/calendar.tsx +367 -0
- package/src/components/card.stories.tsx +44 -0
- package/src/components/card.tsx +392 -0
- package/src/components/cards-nav.stories.tsx +35 -0
- package/src/components/cards-nav.tsx +115 -0
- package/src/components/chain-of-thought.stories.tsx +67 -0
- package/src/components/chain-of-thought.test.tsx +50 -0
- package/src/components/chain-of-thought.tsx +170 -0
- package/src/components/chat-container.stories.tsx +47 -0
- package/src/components/chat-container.test.tsx +49 -0
- package/src/components/chat-container.tsx +222 -0
- package/src/components/chat-message.stories.tsx +52 -0
- package/src/components/chat-message.test.tsx +36 -0
- package/src/components/chat-message.tsx +163 -0
- package/src/components/checkbox-card.stories.tsx +27 -0
- package/src/components/checkbox-card.tsx +200 -0
- package/src/components/checkbox.stories.tsx +26 -0
- package/src/components/checkbox.tsx +61 -0
- package/src/components/citations.stories.tsx +64 -0
- package/src/components/citations.test.tsx +61 -0
- package/src/components/citations.tsx +220 -0
- package/src/components/combobox.stories.tsx +184 -0
- package/src/components/combobox.test.tsx +82 -0
- package/src/components/combobox.tsx +407 -0
- package/src/components/countdown-pill.stories.tsx +24 -0
- package/src/components/countdown-pill.tsx +129 -0
- package/src/components/cta.stories.tsx +29 -0
- package/src/components/cta.tsx +52 -0
- package/src/components/data-list.stories.tsx +50 -0
- package/src/components/data-list.test.tsx +35 -0
- package/src/components/data-list.tsx +181 -0
- package/src/components/data-table.stories.tsx +29 -0
- package/src/components/data-table.tsx +200 -0
- package/src/components/dialog.stories.tsx +42 -0
- package/src/components/dialog.tsx +200 -0
- package/src/components/drawer.stories.tsx +48 -0
- package/src/components/drawer.tsx +385 -0
- package/src/components/dropdown-menu.stories.tsx +41 -0
- package/src/components/dropdown-menu.tsx +252 -0
- package/src/components/dropzone.stories.tsx +19 -0
- package/src/components/dropzone.tsx +250 -0
- package/src/components/email-chips-input.stories.tsx +135 -0
- package/src/components/email-chips-input.tsx +210 -0
- package/src/components/empty-state.stories.tsx +22 -0
- package/src/components/empty-state.tsx +35 -0
- package/src/components/entity-header.stories.tsx +43 -0
- package/src/components/entity-header.test.tsx +35 -0
- package/src/components/entity-header.tsx +92 -0
- package/src/components/feature-grid.stories.tsx +25 -0
- package/src/components/feature-grid.tsx +87 -0
- package/src/components/feedback-bar.stories.tsx +25 -0
- package/src/components/feedback-bar.test.tsx +24 -0
- package/src/components/feedback-bar.tsx +62 -0
- package/src/components/footer.stories.tsx +23 -0
- package/src/components/footer.tsx +78 -0
- package/src/components/heading.stories.tsx +45 -0
- package/src/components/heading.test.tsx +22 -0
- package/src/components/heading.tsx +77 -0
- package/src/components/hero.stories.tsx +38 -0
- package/src/components/hero.tsx +498 -0
- package/src/components/input-button.stories.tsx +19 -0
- package/src/components/input-button.tsx +112 -0
- package/src/components/input-group.stories.tsx +38 -0
- package/src/components/input-group.tsx +157 -0
- package/src/components/input.stories.tsx +31 -0
- package/src/components/input.tsx +284 -0
- package/src/components/inset.stories.tsx +29 -0
- package/src/components/inset.tsx +70 -0
- package/src/components/kpi-card.stories.tsx +29 -0
- package/src/components/kpi-card.tsx +284 -0
- package/src/components/list-nav.stories.tsx +22 -0
- package/src/components/list-nav.tsx +145 -0
- package/src/components/logo-cloud.stories.tsx +23 -0
- package/src/components/logo-cloud.tsx +42 -0
- package/src/components/media-gallery.stories.tsx +25 -0
- package/src/components/media-gallery.tsx +347 -0
- package/src/components/message-actions.stories.tsx +45 -0
- package/src/components/message-actions.test.tsx +32 -0
- package/src/components/message-actions.tsx +119 -0
- package/src/components/nav-sidebar.stories.tsx +58 -0
- package/src/components/nav-sidebar.test.tsx +54 -0
- package/src/components/nav-sidebar.tsx +462 -0
- package/src/components/number-stepper.stories.tsx +19 -0
- package/src/components/number-stepper.tsx +158 -0
- package/src/components/page-header.stories.tsx +25 -0
- package/src/components/page-header.tsx +64 -0
- package/src/components/pagination.stories.tsx +110 -0
- package/src/components/pagination.test.tsx +46 -0
- package/src/components/pagination.tsx +144 -0
- package/src/components/popover.stories.tsx +26 -0
- package/src/components/popover.tsx +62 -0
- package/src/components/pricing.stories.tsx +42 -0
- package/src/components/pricing.tsx +130 -0
- package/src/components/progress.stories.tsx +22 -0
- package/src/components/progress.tsx +189 -0
- package/src/components/prompt-input.stories.tsx +62 -0
- package/src/components/prompt-input.test.tsx +106 -0
- package/src/components/prompt-input.tsx +343 -0
- package/src/components/prompt-suggestions.stories.tsx +53 -0
- package/src/components/prompt-suggestions.test.tsx +37 -0
- package/src/components/prompt-suggestions.tsx +122 -0
- package/src/components/quota-bar.stories.tsx +19 -0
- package/src/components/quota-bar.tsx +191 -0
- package/src/components/quote.stories.tsx +18 -0
- package/src/components/quote.tsx +43 -0
- package/src/components/radio-card.stories.tsx +27 -0
- package/src/components/radio-card.tsx +225 -0
- package/src/components/radio-group.stories.tsx +32 -0
- package/src/components/radio-group.tsx +68 -0
- package/src/components/reasoning.stories.tsx +50 -0
- package/src/components/reasoning.test.tsx +48 -0
- package/src/components/reasoning.tsx +143 -0
- package/src/components/search.stories.tsx +18 -0
- package/src/components/search.tsx +133 -0
- package/src/components/section-nav.stories.tsx +59 -0
- package/src/components/section-nav.test.tsx +36 -0
- package/src/components/section-nav.tsx +146 -0
- package/src/components/segmented-control.stories.tsx +22 -0
- package/src/components/segmented-control.tsx +301 -0
- package/src/components/select-button.stories.tsx +27 -0
- package/src/components/select-button.tsx +174 -0
- package/src/components/select.stories.tsx +27 -0
- package/src/components/select.tsx +198 -0
- package/src/components/separator.stories.tsx +22 -0
- package/src/components/separator.tsx +23 -0
- package/src/components/shimmer-text.stories.tsx +23 -0
- package/src/components/shimmer-text.test.tsx +35 -0
- package/src/components/shimmer-text.tsx +43 -0
- package/src/components/sidebar.stories.tsx +25 -0
- package/src/components/sidebar.tsx +234 -0
- package/src/components/skeleton.stories.tsx +22 -0
- package/src/components/skeleton.tsx +96 -0
- package/src/components/slider.stories.tsx +20 -0
- package/src/components/slider.tsx +176 -0
- package/src/components/slideshow.stories.tsx +30 -0
- package/src/components/slideshow.tsx +291 -0
- package/src/components/source.stories.tsx +32 -0
- package/src/components/source.test.tsx +40 -0
- package/src/components/source.tsx +161 -0
- package/src/components/spinner.stories.tsx +19 -0
- package/src/components/spinner.tsx +76 -0
- package/src/components/stat-strip.stories.tsx +36 -0
- package/src/components/stat-strip.test.tsx +25 -0
- package/src/components/stat-strip.tsx +85 -0
- package/src/components/stats.stories.tsx +24 -0
- package/src/components/stats.tsx +83 -0
- package/src/components/step-indicator.stories.tsx +18 -0
- package/src/components/step-indicator.test.tsx +18 -0
- package/src/components/step-indicator.tsx +286 -0
- package/src/components/switch.stories.tsx +26 -0
- package/src/components/switch.tsx +32 -0
- package/src/components/table.stories.tsx +38 -0
- package/src/components/table.tsx +220 -0
- package/src/components/tabs.stories.tsx +25 -0
- package/src/components/tabs.tsx +276 -0
- package/src/components/testimonials.stories.tsx +22 -0
- package/src/components/testimonials.tsx +109 -0
- package/src/components/text-link.stories.tsx +47 -0
- package/src/components/text-link.test.tsx +18 -0
- package/src/components/text-link.tsx +88 -0
- package/src/components/text.stories.tsx +49 -0
- package/src/components/text.tsx +105 -0
- package/src/components/thinking-bar.stories.tsx +19 -0
- package/src/components/thinking-bar.test.tsx +27 -0
- package/src/components/thinking-bar.tsx +64 -0
- package/src/components/thinking-steps.stories.tsx +44 -0
- package/src/components/thinking-steps.test.tsx +42 -0
- package/src/components/thinking-steps.tsx +92 -0
- package/src/components/toaster.stories.tsx +80 -0
- package/src/components/toaster.tsx +36 -0
- package/src/components/toggle.stories.tsx +21 -0
- package/src/components/toggle.tsx +103 -0
- package/src/components/tool-call-card.stories.tsx +97 -0
- package/src/components/tool-call-card.test.tsx +88 -0
- package/src/components/tool-call-card.tsx +231 -0
- package/src/components/tooltip.stories.tsx +25 -0
- package/src/components/tooltip.tsx +43 -0
- package/src/components/top-nav.stories.tsx +20 -0
- package/src/components/top-nav.tsx +76 -0
- package/src/docs/Colors.stories.tsx +192 -0
- package/src/docs/GettingStarted.mdx +72 -0
- package/src/docs/Mcp.mdx +52 -0
- package/src/docs/Typography.stories.tsx +114 -0
- package/src/hooks/use-in-view.ts +32 -0
- package/src/hooks/use-reduced-motion.ts +26 -0
- package/src/index.ts +343 -0
- package/src/lib/cn.test.ts +67 -0
- package/src/lib/cn.ts +81 -0
- package/src/lib/motion.ts +109 -0
- package/src/lib/prefix-utilities.test.ts +56 -0
- package/src/lib/prefix-utilities.ts +50 -0
- package/src/styles/base.css +489 -0
- package/src/styles/motion.css +69 -0
- package/src/styles/overrides.css +44 -0
- package/src/styles/typography.css +173 -0
- package/src/theme/theme.css +391 -0
- package/src/theme/tokens.ts +29 -0
- package/src/theme/types.ts +26 -0
- package/README.md +0 -3
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { AlertCircle, Bot } from "lucide-react"
|
|
5
|
+
import { cn } from "../lib/cn"
|
|
6
|
+
import { Avatar, AvatarFallback, type AvatarProps } from "./avatar"
|
|
7
|
+
import { Separator } from "./separator"
|
|
8
|
+
import { Text } from "./text"
|
|
9
|
+
import { TextLink } from "./text-link"
|
|
10
|
+
|
|
11
|
+
export interface ChatAvatarProps extends Omit<AvatarProps, "children"> {
|
|
12
|
+
/** Replaces the default bot icon, e.g. with an `AvatarImage`. */
|
|
13
|
+
children?: React.ReactNode
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The assistant's identity mark: a small tinted avatar with a bot icon. */
|
|
17
|
+
export const ChatAvatar = React.forwardRef<React.ElementRef<typeof Avatar>, ChatAvatarProps>(
|
|
18
|
+
({ className, size = "sm", children, ...props }, ref) => (
|
|
19
|
+
<Avatar ref={ref} size={size} className={cn("ax:shrink-0", className)} {...props}>
|
|
20
|
+
{children ?? (
|
|
21
|
+
<AvatarFallback className="ax:bg-primary-weaker ax:text-primary-foreground">
|
|
22
|
+
<Bot className="ax:size-4" aria-hidden />
|
|
23
|
+
</AvatarFallback>
|
|
24
|
+
)}
|
|
25
|
+
</Avatar>
|
|
26
|
+
),
|
|
27
|
+
)
|
|
28
|
+
ChatAvatar.displayName = "ChatAvatar"
|
|
29
|
+
|
|
30
|
+
export interface ChatDividerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
|
|
31
|
+
/** Short system note, e.g. "Today" or "Conversation restarted". */
|
|
32
|
+
label: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A labelled rule between turns for system notes and date breaks. */
|
|
36
|
+
export const ChatDivider = React.forwardRef<HTMLDivElement, ChatDividerProps>(
|
|
37
|
+
({ label, className, ...props }, ref) => (
|
|
38
|
+
<div
|
|
39
|
+
ref={ref}
|
|
40
|
+
data-slot="chat-divider"
|
|
41
|
+
role="separator"
|
|
42
|
+
aria-label={label}
|
|
43
|
+
className={cn("ax:flex ax:items-center ax:gap-sm", className)}
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
<Separator className="ax:flex-1" />
|
|
47
|
+
<Text size="1" color="muted">{label}</Text>
|
|
48
|
+
<Separator className="ax:flex-1" />
|
|
49
|
+
</div>
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
ChatDivider.displayName = "ChatDivider"
|
|
53
|
+
|
|
54
|
+
export type ChatMessageFrom = "assistant" | "user"
|
|
55
|
+
export type ChatMessageStatus = "sent" | "failed"
|
|
56
|
+
|
|
57
|
+
export interface ChatMessageProps extends React.HTMLAttributes<HTMLElement> {
|
|
58
|
+
/** Who wrote it. Named `from` so it never clashes with the ARIA `role` attribute. */
|
|
59
|
+
from: ChatMessageFrom
|
|
60
|
+
/** Assistant only. None by default; pass `<ChatAvatar />` to show one beside the turn. */
|
|
61
|
+
avatar?: React.ReactNode
|
|
62
|
+
/** Drops the bubble so `children` lay out freely, e.g. steps, tool runs or a cited answer. */
|
|
63
|
+
bare?: boolean
|
|
64
|
+
/** User only. `failed` tints the bubble and swaps `meta` for a retry line. */
|
|
65
|
+
status?: ChatMessageStatus
|
|
66
|
+
onRetry?: () => void
|
|
67
|
+
/** Content under the bubble, such as listing cards or a sources line. */
|
|
68
|
+
attachments?: React.ReactNode
|
|
69
|
+
/** Assistant only. Usually `MessageActions`; sits after `meta` in the footer row. */
|
|
70
|
+
actions?: React.ReactNode
|
|
71
|
+
/** Small muted footer text, e.g. the time or "Delivered · 10:42". */
|
|
72
|
+
meta?: React.ReactNode
|
|
73
|
+
/** Extra classes for the column that holds the bubble and footer. */
|
|
74
|
+
bodyClassName?: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function Body({ children }: { children: React.ReactNode }) {
|
|
78
|
+
return typeof children === "string" ? <Text as="p" size="2" wrap="pretty">{children}</Text> : <>{children}</>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function FailedLine({ onRetry }: { onRetry?: () => void }) {
|
|
82
|
+
return (
|
|
83
|
+
<Text as="p" size="1" color="destructive" className="ax:flex ax:items-center ax:gap-1">
|
|
84
|
+
<AlertCircle className="ax:size-3.5" aria-hidden />
|
|
85
|
+
Not delivered.
|
|
86
|
+
{onRetry && (
|
|
87
|
+
<TextLink asChild size="1" underline="always" className="ax:text-destructive-foreground">
|
|
88
|
+
<button type="button" onClick={onRetry}>Retry</button>
|
|
89
|
+
</TextLink>
|
|
90
|
+
)}
|
|
91
|
+
</Text>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* ChatMessage - one turn in a conversation. Assistant turns sit in a
|
|
97
|
+
* primary-tinted bubble at the start with an actions and meta footer; user
|
|
98
|
+
* turns align to the end in a muted rounded bubble with a delivery line that
|
|
99
|
+
* offers retry when sending failed.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* <ChatMessage from="assistant" actions={<MessageActions copyText={text} />} meta="10:42">{text}</ChatMessage>
|
|
103
|
+
* <ChatMessage from="user" status="failed" onRetry={retry}>{text}</ChatMessage>
|
|
104
|
+
*/
|
|
105
|
+
export const ChatMessage = React.forwardRef<HTMLElement, ChatMessageProps>(
|
|
106
|
+
({ from, avatar, bare = false, status = "sent", onRetry, attachments, actions, meta, bodyClassName, className, children, ...props }, ref) => {
|
|
107
|
+
if (from === "user") {
|
|
108
|
+
const failed = status === "failed"
|
|
109
|
+
return (
|
|
110
|
+
<article
|
|
111
|
+
ref={ref}
|
|
112
|
+
data-slot="chat-message"
|
|
113
|
+
data-from="user"
|
|
114
|
+
aria-label="Your message"
|
|
115
|
+
className={cn("ax:group/message ax:flex ax:flex-col ax:items-end ax:gap-1", className)}
|
|
116
|
+
{...props}
|
|
117
|
+
>
|
|
118
|
+
{bare ? children : (
|
|
119
|
+
<div
|
|
120
|
+
className={cn(
|
|
121
|
+
"ax:max-w-[90%] ax:rounded-xl ax:px-md ax:py-xs",
|
|
122
|
+
failed ? "ax:bg-destructive-weakest" : "ax:bg-muted",
|
|
123
|
+
)}
|
|
124
|
+
>
|
|
125
|
+
<Body>{children}</Body>
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
{attachments}
|
|
129
|
+
{failed ? <FailedLine onRetry={onRetry} /> : meta && <Text size="1" color="muted">{meta}</Text>}
|
|
130
|
+
</article>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<article
|
|
136
|
+
ref={ref}
|
|
137
|
+
data-slot="chat-message"
|
|
138
|
+
data-from="assistant"
|
|
139
|
+
aria-label="Assistant message"
|
|
140
|
+
className={cn("ax:group/message ax:flex ax:gap-sm", className)}
|
|
141
|
+
{...props}
|
|
142
|
+
>
|
|
143
|
+
{avatar}
|
|
144
|
+
<div className={cn("ax:flex ax:min-w-0 ax:flex-1 ax:flex-col ax:gap-xs", bodyClassName)}>
|
|
145
|
+
{bare ? children : (
|
|
146
|
+
<div className="ax:self-start ax:rounded-xl ax:bg-primary-weakest ax:px-md ax:py-xs ax:max-w-[90%]">
|
|
147
|
+
<Body>{children}</Body>
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
{attachments}
|
|
151
|
+
{(actions || meta) && (
|
|
152
|
+
// With the time first, it marks the bubble's edge; MessageActions drops the inset that otherwise aligns its first icon there.
|
|
153
|
+
<div className={cn("ax:flex ax:items-center ax:gap-sm", meta && "ax:*:data-[slot=message-actions]:ms-0")}>
|
|
154
|
+
{meta && <Text size="1" color="muted">{meta}</Text>}
|
|
155
|
+
{actions}
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
</article>
|
|
160
|
+
)
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
ChatMessage.displayName = "ChatMessage"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { CheckboxCard, CheckboxCardDescription, CheckboxCardGroup, CheckboxCardTitle } from "./checkbox-card"
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof CheckboxCardGroup> = {
|
|
6
|
+
title: "Components/CheckboxCard",
|
|
7
|
+
component: CheckboxCardGroup,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof CheckboxCardGroup>
|
|
13
|
+
|
|
14
|
+
export const Default: Story = {
|
|
15
|
+
render: () => (
|
|
16
|
+
<CheckboxCardGroup className="ax:max-w-md">
|
|
17
|
+
<CheckboxCard defaultChecked>
|
|
18
|
+
<CheckboxCardTitle>Email alerts</CheckboxCardTitle>
|
|
19
|
+
<CheckboxCardDescription>Receive updates about your account.</CheckboxCardDescription>
|
|
20
|
+
</CheckboxCard>
|
|
21
|
+
<CheckboxCard>
|
|
22
|
+
<CheckboxCardTitle>SMS alerts</CheckboxCardTitle>
|
|
23
|
+
<CheckboxCardDescription>Get text messages for urgent events.</CheckboxCardDescription>
|
|
24
|
+
</CheckboxCard>
|
|
25
|
+
</CheckboxCardGroup>
|
|
26
|
+
),
|
|
27
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
|
5
|
+
import { Check } from "lucide-react"
|
|
6
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
7
|
+
import { cn } from "../lib/cn"
|
|
8
|
+
|
|
9
|
+
export type CheckboxCardSize = "sm" | "base" | "lg"
|
|
10
|
+
export type CheckboxCardOrientation = "horizontal" | "vertical"
|
|
11
|
+
|
|
12
|
+
// ─── Context ──────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
interface CheckboxCardGroupContextValue {
|
|
15
|
+
size: CheckboxCardSize
|
|
16
|
+
compact: boolean
|
|
17
|
+
orientation: CheckboxCardOrientation
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const CheckboxCardGroupContext = React.createContext<CheckboxCardGroupContextValue>({
|
|
21
|
+
size: "base",
|
|
22
|
+
compact: false,
|
|
23
|
+
orientation: "vertical",
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// ─── Group ────────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
// Multi-select layout wrapper. Each contained CheckboxCard manages its own
|
|
29
|
+
// checked state: the group only provides shared size + layout context.
|
|
30
|
+
export interface CheckboxCardGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
31
|
+
size?: CheckboxCardSize
|
|
32
|
+
compact?: boolean
|
|
33
|
+
orientation?: CheckboxCardOrientation
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const CheckboxCardGroup = React.forwardRef<HTMLDivElement, CheckboxCardGroupProps>(
|
|
37
|
+
({ className, size = "base", compact = false, orientation = "vertical", ...props }, ref) => (
|
|
38
|
+
<CheckboxCardGroupContext.Provider value={{ size, compact, orientation }}>
|
|
39
|
+
<div
|
|
40
|
+
ref={ref}
|
|
41
|
+
role="group"
|
|
42
|
+
className={cn(
|
|
43
|
+
compact
|
|
44
|
+
? orientation === "horizontal" ? "ax:flex ax:flex-row" : "ax:flex ax:flex-col"
|
|
45
|
+
: "ax:grid ax:gap-3",
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
</CheckboxCardGroupContext.Provider>
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
CheckboxCardGroup.displayName = "CheckboxCardGroup"
|
|
54
|
+
|
|
55
|
+
// ─── Card ─────────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
const checkboxCardVariants = cva(
|
|
58
|
+
[
|
|
59
|
+
"ax:relative ax:flex ax:items-start ax:cursor-pointer ax:border ax:border-border ax:bg-card",
|
|
60
|
+
"ax:transition-colors ax:transition-snappy",
|
|
61
|
+
// Selected border matches the selected title/icon colour (primary-strong /
|
|
62
|
+
// dark:primary) rather than the base --primary.
|
|
63
|
+
"ax:has-[[data-state=checked]]:border-primary",
|
|
64
|
+
"ax:dark:has-[[data-state=checked]]:border-primary",
|
|
65
|
+
// Selected background - a weak brand tint (mode-aware, auto-inverts in dark).
|
|
66
|
+
"ax:has-[[data-state=checked]]:bg-primary-muted",
|
|
67
|
+
// Hover only on unchecked cards - darker brand-aware border (≈30% foreground)
|
|
68
|
+
// for a clearly visible interactivity cue. `hover:z-10` lifts the hovered
|
|
69
|
+
// card above its compact-mode neighbours so the new border isn't clipped
|
|
70
|
+
// by the adjacent card's idle border.
|
|
71
|
+
"ax:[&:not(:has([data-state=checked]))]:hover:border-neutral-weak",
|
|
72
|
+
"ax:[&:not(:has([data-state=checked]))]:hover:z-10",
|
|
73
|
+
"ax:has-[[data-disabled]]:cursor-not-allowed ax:has-[[data-disabled]]:opacity-50",
|
|
74
|
+
// Checked-state title tint - mirrors RadioCard so both cards read the same way.
|
|
75
|
+
"ax:[&:has([data-state=checked])_[data-slot=checkbox-card-title]]:text-primary-foreground",
|
|
76
|
+
// Dark mode: primary-strong reads muddy on a dark card, so brighten the selected
|
|
77
|
+
// title (matches RadioCard and the bright check indicator).
|
|
78
|
+
"ax:dark:[&:has([data-state=checked])_[data-slot=checkbox-card-title]]:text-primary-foreground",
|
|
79
|
+
// Trailing check icon - three states:
|
|
80
|
+
// idle (unchecked, no hover) → transparent (slot still reserves space, no layout shift)
|
|
81
|
+
// hover on unchecked → --input (tinted to match the border at rest)
|
|
82
|
+
// checked → primary-strong / dark:primary, mirrors the selected title colour
|
|
83
|
+
"ax:[&_[data-slot=checkbox-card-check]]:text-transparent",
|
|
84
|
+
"ax:[&:not(:has([data-state=checked])):hover_[data-slot=checkbox-card-check]]:text-input",
|
|
85
|
+
"ax:[&:has([data-state=checked])_[data-slot=checkbox-card-check]]:text-primary",
|
|
86
|
+
"ax:dark:[&:has([data-state=checked])_[data-slot=checkbox-card-check]]:text-primary",
|
|
87
|
+
],
|
|
88
|
+
{
|
|
89
|
+
variants: {
|
|
90
|
+
size: {
|
|
91
|
+
sm: "ax:p-3 ax:gap-3 ax:text-xs",
|
|
92
|
+
base: "ax:p-4 ax:gap-3 ax:text-sm",
|
|
93
|
+
lg: "ax:p-5 ax:gap-4 ax:text-base",
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
defaultVariants: { size: "base" },
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
const COMPACT_LAYOUT: Record<CheckboxCardOrientation, string> = {
|
|
101
|
+
horizontal: [
|
|
102
|
+
"ax:rounded-none",
|
|
103
|
+
"ax:first:rounded-l-(--input-radius) ax:last:rounded-r-(--input-radius)",
|
|
104
|
+
"ax:[&:not(:first-child)]:-ml-px",
|
|
105
|
+
"ax:has-[[data-state=checked]]:z-10",
|
|
106
|
+
].join(" "),
|
|
107
|
+
vertical: [
|
|
108
|
+
"ax:rounded-none",
|
|
109
|
+
"ax:first:rounded-t-(--input-radius) ax:last:rounded-b-(--input-radius)",
|
|
110
|
+
"ax:[&:not(:first-child)]:-mt-px",
|
|
111
|
+
"ax:has-[[data-state=checked]]:z-10",
|
|
112
|
+
].join(" "),
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const CHECK_SIZE: Record<CheckboxCardSize, string> = {
|
|
116
|
+
sm: "ax:size-3.5",
|
|
117
|
+
base: "ax:size-4",
|
|
118
|
+
lg: "ax:size-4",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface CheckboxCardProps
|
|
122
|
+
extends VariantProps<typeof checkboxCardVariants>,
|
|
123
|
+
Pick<
|
|
124
|
+
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>,
|
|
125
|
+
"checked" | "defaultChecked" | "onCheckedChange" | "disabled" | "name" | "value" | "required"
|
|
126
|
+
> {
|
|
127
|
+
className?: string
|
|
128
|
+
children?: React.ReactNode
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const CheckboxCard = React.forwardRef<
|
|
132
|
+
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
|
133
|
+
CheckboxCardProps
|
|
134
|
+
>(
|
|
135
|
+
(
|
|
136
|
+
{ className, size: sizeProp, children, checked, defaultChecked, onCheckedChange, disabled, name, value, required },
|
|
137
|
+
ref
|
|
138
|
+
) => {
|
|
139
|
+
const { size: ctxSize, compact, orientation } = React.useContext(CheckboxCardGroupContext)
|
|
140
|
+
const size = sizeProp ?? ctxSize
|
|
141
|
+
const id = React.useId()
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<label
|
|
145
|
+
htmlFor={id}
|
|
146
|
+
className={cn(
|
|
147
|
+
checkboxCardVariants({ size }),
|
|
148
|
+
compact ? COMPACT_LAYOUT[orientation] : "ax:rounded-(--input-radius)",
|
|
149
|
+
className
|
|
150
|
+
)}
|
|
151
|
+
>
|
|
152
|
+
<div className="ax:flex-1 ax:min-w-0">{children}</div>
|
|
153
|
+
<Check
|
|
154
|
+
data-slot="checkbox-card-check"
|
|
155
|
+
aria-hidden="true"
|
|
156
|
+
className={cn(
|
|
157
|
+
"ax:shrink-0 ax:mt-0.5 ax:transition-colors ax:transition-snappy",
|
|
158
|
+
CHECK_SIZE[size]
|
|
159
|
+
)}
|
|
160
|
+
/>
|
|
161
|
+
<CheckboxPrimitive.Root
|
|
162
|
+
ref={ref}
|
|
163
|
+
id={id}
|
|
164
|
+
checked={checked}
|
|
165
|
+
defaultChecked={defaultChecked}
|
|
166
|
+
onCheckedChange={onCheckedChange}
|
|
167
|
+
disabled={disabled}
|
|
168
|
+
name={name}
|
|
169
|
+
value={value}
|
|
170
|
+
required={required}
|
|
171
|
+
className="ax:sr-only"
|
|
172
|
+
/>
|
|
173
|
+
</label>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
CheckboxCard.displayName = "CheckboxCard"
|
|
178
|
+
|
|
179
|
+
// ─── Sub-components ───────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
export const CheckboxCardTitle = React.forwardRef<
|
|
182
|
+
HTMLParagraphElement,
|
|
183
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
184
|
+
>(({ className, ...props }, ref) => (
|
|
185
|
+
<p
|
|
186
|
+
ref={ref}
|
|
187
|
+
data-slot="checkbox-card-title"
|
|
188
|
+
className={cn("ax:t-label-sm ax:text-foreground", className)}
|
|
189
|
+
{...props}
|
|
190
|
+
/>
|
|
191
|
+
))
|
|
192
|
+
CheckboxCardTitle.displayName = "CheckboxCardTitle"
|
|
193
|
+
|
|
194
|
+
export const CheckboxCardDescription = React.forwardRef<
|
|
195
|
+
HTMLParagraphElement,
|
|
196
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
197
|
+
>(({ className, ...props }, ref) => (
|
|
198
|
+
<p ref={ref} className={cn("ax:text-[0.875em] ax:text-muted-foreground ax:mt-1", className)} {...props} />
|
|
199
|
+
))
|
|
200
|
+
CheckboxCardDescription.displayName = "CheckboxCardDescription"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { Checkbox } from "./checkbox"
|
|
4
|
+
import { Label } from "./input"
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Checkbox> = {
|
|
7
|
+
title: "Components/Checkbox",
|
|
8
|
+
component: Checkbox,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default meta
|
|
13
|
+
type Story = StoryObj<typeof Checkbox>
|
|
14
|
+
|
|
15
|
+
export const Default: Story = {
|
|
16
|
+
render: () => (
|
|
17
|
+
<div className="ax:flex ax:items-center ax:gap-sm">
|
|
18
|
+
<Checkbox id="terms" />
|
|
19
|
+
<Label htmlFor="terms">Accept terms and conditions</Label>
|
|
20
|
+
</div>
|
|
21
|
+
),
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const Checked: Story = {
|
|
25
|
+
render: () => <Checkbox defaultChecked />,
|
|
26
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
|
5
|
+
import { Check, Minus } from "lucide-react"
|
|
6
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
7
|
+
import { cn } from "../lib/cn"
|
|
8
|
+
|
|
9
|
+
const checkboxVariants = cva(
|
|
10
|
+
[
|
|
11
|
+
"ax:peer ax:shrink-0 ax:rounded-xs ax:border ax:border-input",
|
|
12
|
+
"ax:transition-colors ax:transition-snappy",
|
|
13
|
+
"ax:focus-visible:outline-none ax:focus-visible:ring-2 ax:focus-visible:ring-ring ax:focus-visible:ring-offset-2 ax:focus-visible:ring-offset-background",
|
|
14
|
+
"ax:cursor-pointer ax:disabled:cursor-not-allowed ax:disabled:opacity-50",
|
|
15
|
+
// Selected: primary-weak fill + primary-strong glyph, matches RadioGroup. The
|
|
16
|
+
// fill auto-inverts to a light-ish tint in dark, so the glyph flips to a dark
|
|
17
|
+
// `primary-muted` there (dark: override) to keep contrast on that lighter fill.
|
|
18
|
+
"ax:data-[state=checked]:bg-primary-weak ax:data-[state=checked]:border-primary-weak ax:data-[state=checked]:text-primary-strong ax:dark:data-[state=checked]:text-primary-muted",
|
|
19
|
+
"ax:data-[state=indeterminate]:bg-primary-weak ax:data-[state=indeterminate]:border-primary-weak ax:data-[state=indeterminate]:text-primary-strong ax:dark:data-[state=indeterminate]:text-primary-muted",
|
|
20
|
+
],
|
|
21
|
+
{
|
|
22
|
+
variants: {
|
|
23
|
+
size: {
|
|
24
|
+
sm: "ax:size-3",
|
|
25
|
+
base: "ax:size-4",
|
|
26
|
+
lg: "ax:size-5",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: { size: "base" },
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const ICON_SIZE: Record<NonNullable<VariantProps<typeof checkboxVariants>["size"]>, string> = {
|
|
34
|
+
sm: "ax:size-2",
|
|
35
|
+
base: "ax:size-3",
|
|
36
|
+
lg: "ax:size-4",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CheckboxProps
|
|
40
|
+
extends React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>,
|
|
41
|
+
VariantProps<typeof checkboxVariants> {}
|
|
42
|
+
|
|
43
|
+
export const Checkbox = React.forwardRef<
|
|
44
|
+
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
|
45
|
+
CheckboxProps
|
|
46
|
+
>(({ className, size, ...props }, ref) => (
|
|
47
|
+
<CheckboxPrimitive.Root
|
|
48
|
+
ref={ref}
|
|
49
|
+
className={cn(checkboxVariants({ size }), className)}
|
|
50
|
+
{...props}
|
|
51
|
+
>
|
|
52
|
+
<CheckboxPrimitive.Indicator className="ax:flex ax:items-center ax:justify-center ax:text-current">
|
|
53
|
+
{props.checked === "indeterminate" ? (
|
|
54
|
+
<Minus className={ICON_SIZE[size ?? "base"]} strokeWidth={2} />
|
|
55
|
+
) : (
|
|
56
|
+
<Check className={ICON_SIZE[size ?? "base"]} strokeWidth={4} />
|
|
57
|
+
)}
|
|
58
|
+
</CheckboxPrimitive.Indicator>
|
|
59
|
+
</CheckboxPrimitive.Root>
|
|
60
|
+
))
|
|
61
|
+
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { Home, LineChart, Newspaper } from "lucide-react"
|
|
4
|
+
import { CitationMarker, SourceList, UnsupportedClaim, type CitationSource } from "./citations"
|
|
5
|
+
import { Text } from "./text"
|
|
6
|
+
import { TooltipProvider } from "./tooltip"
|
|
7
|
+
|
|
8
|
+
const SOURCES: CitationSource[] = [
|
|
9
|
+
{
|
|
10
|
+
id: 1,
|
|
11
|
+
title: "Saadiyat Lagoons, Apt 1316",
|
|
12
|
+
publisher: "Adres listings",
|
|
13
|
+
date: "2026-09-28",
|
|
14
|
+
url: "#",
|
|
15
|
+
excerpt: "Asking AED 4,162,000 for 1,734 sqft, three bedrooms with a lagoon view.",
|
|
16
|
+
kind: { label: "Listing", icon: Home },
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 2,
|
|
20
|
+
title: "Saadiyat Lagoons registered sales, Q3 2026",
|
|
21
|
+
publisher: "DLD",
|
|
22
|
+
date: "2026-09-30",
|
|
23
|
+
url: "#",
|
|
24
|
+
excerpt: "Median AED 2,550 per sqft across 42 registered sales.",
|
|
25
|
+
kind: { label: "Registered sales", icon: LineChart },
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 3,
|
|
29
|
+
title: "Saadiyat schools guide",
|
|
30
|
+
publisher: "The National",
|
|
31
|
+
date: "2025-11-02",
|
|
32
|
+
url: "#",
|
|
33
|
+
excerpt: "Two international schools sit within a ten-minute drive.",
|
|
34
|
+
kind: { label: "Article", icon: Newspaper },
|
|
35
|
+
},
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
const meta: Meta<typeof SourceList> = {
|
|
39
|
+
title: "Components/Citations",
|
|
40
|
+
component: SourceList,
|
|
41
|
+
tags: ["autodocs"],
|
|
42
|
+
decorators: [(Story) => <TooltipProvider><div style={{ maxWidth: 640 }}><Story /></div></TooltipProvider>],
|
|
43
|
+
args: { sources: SOURCES, asOf: "2026-10-06" },
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default meta
|
|
47
|
+
type Story = StoryObj<typeof SourceList>
|
|
48
|
+
|
|
49
|
+
export const Sources: Story = {}
|
|
50
|
+
|
|
51
|
+
export const Collapsed: Story = { args: { initial: 2, onExpand: () => undefined } }
|
|
52
|
+
|
|
53
|
+
export const Highlighted: Story = { args: { highlighted: 2 } }
|
|
54
|
+
|
|
55
|
+
export const CitedAnswer: Story = {
|
|
56
|
+
render: () => (
|
|
57
|
+
<Text as="p" size="2" wrap="pretty">
|
|
58
|
+
Apt 1316 is listed at about AED 2,400 per sqft
|
|
59
|
+
<CitationMarker source={SOURCES[0]!} />, below the area median of AED 2,550
|
|
60
|
+
<CitationMarker source={SOURCES[1]!} />.{" "}
|
|
61
|
+
<UnsupportedClaim>Prices here are likely to rise next year.</UnsupportedClaim>
|
|
62
|
+
</Text>
|
|
63
|
+
),
|
|
64
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
import { renderToStaticMarkup } from "react-dom/server"
|
|
4
|
+
import { CitationMarker, SourceCard, SourceList, UnsupportedClaim, formatSourceDate, isStale, type CitationSource } from "./citations"
|
|
5
|
+
import { TooltipProvider } from "./tooltip"
|
|
6
|
+
|
|
7
|
+
const source = (id: number, date = "2026-09-28"): CitationSource => ({
|
|
8
|
+
id,
|
|
9
|
+
title: `Source ${id} title`,
|
|
10
|
+
publisher: "Adres listings",
|
|
11
|
+
date,
|
|
12
|
+
url: "https://example.com",
|
|
13
|
+
excerpt: "The passage used.",
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe("isStale", () => {
|
|
17
|
+
it("is false up to the limit and true after it", () => {
|
|
18
|
+
expect(isStale("2026-04-06", "2026-10-06")).toBe(false)
|
|
19
|
+
expect(isStale("2026-03-01", "2026-10-06")).toBe(true)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it("takes a custom limit in days", () => {
|
|
23
|
+
expect(isStale("2026-09-01", "2026-10-06", 30)).toBe(true)
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe("formatSourceDate", () => {
|
|
28
|
+
it("formats in UTC as day, short month, year", () => {
|
|
29
|
+
expect(formatSourceDate("2026-10-06")).toBe("6 Oct 2026")
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe("SourceCard", () => {
|
|
34
|
+
it("marks old sources only when asOf is given", () => {
|
|
35
|
+
expect(renderToStaticMarkup(<SourceCard source={source(1, "2025-01-01")} asOf="2026-10-06" />)).toContain("Older than 6 months")
|
|
36
|
+
expect(renderToStaticMarkup(<SourceCard source={source(1, "2025-01-01")} />)).not.toContain("Older than 6 months")
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe("SourceList", () => {
|
|
41
|
+
it("shows the first sources and offers the rest", () => {
|
|
42
|
+
const html = renderToStaticMarkup(
|
|
43
|
+
<SourceList sources={[source(1), source(2), source(3)]} initial={2} onExpand={() => undefined} getSourceId={(id) => `s-${id}`} />,
|
|
44
|
+
)
|
|
45
|
+
expect(html).toContain('id="s-1"')
|
|
46
|
+
expect(html).not.toContain('id="s-3"')
|
|
47
|
+
expect(html).toContain("Show 1 more source")
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe("CitationMarker and UnsupportedClaim", () => {
|
|
52
|
+
it("names the marker by number and title", () => {
|
|
53
|
+
expect(renderToStaticMarkup(<CitationMarker source={source(2)} />)).toContain('aria-label="Source 2: Source 2 title"')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("gives the unsupported mark a reachable button", () => {
|
|
57
|
+
const html = renderToStaticMarkup(<TooltipProvider><UnsupportedClaim>Prices will rise.</UnsupportedClaim></TooltipProvider>)
|
|
58
|
+
expect(html).toContain('aria-label="Not backed by a source"')
|
|
59
|
+
expect(html).toContain("Prices will rise.")
|
|
60
|
+
})
|
|
61
|
+
})
|