@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,97 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { CalendarCheck, LineChart, Search } from "lucide-react"
|
|
4
|
+
import { Button } from "./button"
|
|
5
|
+
import { Text } from "./text"
|
|
6
|
+
import { ToolCallCard, toolPartToCallCard } from "./tool-call-card"
|
|
7
|
+
|
|
8
|
+
const INPUT = [
|
|
9
|
+
{ label: "Community", value: "Yas Island" },
|
|
10
|
+
{ label: "Type", value: "Townhouse" },
|
|
11
|
+
{ label: "Max price", value: "AED 5,000,000" },
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
const OUTPUT = [
|
|
15
|
+
{ label: "Matches", value: "12" },
|
|
16
|
+
{ label: "Lowest price", value: "AED 4,200,000" },
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
const meta: Meta<typeof ToolCallCard> = {
|
|
20
|
+
title: "Components/ToolCallCard",
|
|
21
|
+
component: ToolCallCard,
|
|
22
|
+
tags: ["autodocs"],
|
|
23
|
+
decorators: [(Story) => <div style={{ maxWidth: 640 }}><Story /></div>],
|
|
24
|
+
argTypes: {
|
|
25
|
+
status: { control: "select", options: ["awaiting-approval", "running", "success", "error", "declined", "cancelled"] },
|
|
26
|
+
icon: { control: false },
|
|
27
|
+
},
|
|
28
|
+
args: {
|
|
29
|
+
icon: Search,
|
|
30
|
+
title: "Searched listings",
|
|
31
|
+
toolName: "search_listings",
|
|
32
|
+
subtitle: "12 townhouses under AED 5M",
|
|
33
|
+
status: "success",
|
|
34
|
+
durationMs: 1400,
|
|
35
|
+
input: INPUT,
|
|
36
|
+
output: OUTPUT,
|
|
37
|
+
defaultOpen: true,
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default meta
|
|
42
|
+
type Story = StoryObj<typeof ToolCallCard>
|
|
43
|
+
|
|
44
|
+
export const Success: Story = {}
|
|
45
|
+
|
|
46
|
+
export const Running: Story = {
|
|
47
|
+
args: { title: "Searching listings", subtitle: undefined, status: "running", onCancel: () => undefined, defaultOpen: false },
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const Failed: Story = {
|
|
51
|
+
args: {
|
|
52
|
+
icon: LineChart,
|
|
53
|
+
title: "Check registered sales",
|
|
54
|
+
toolName: "get_dld_transactions",
|
|
55
|
+
subtitle: "Couldn't finish",
|
|
56
|
+
status: "error",
|
|
57
|
+
error: "The sales registry didn't respond. Nothing was changed.",
|
|
58
|
+
onRetry: () => undefined,
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const AwaitingApproval: Story = {
|
|
63
|
+
args: {
|
|
64
|
+
icon: CalendarCheck,
|
|
65
|
+
title: "Book a viewing",
|
|
66
|
+
toolName: "book_viewing",
|
|
67
|
+
subtitle: "Waiting for your approval",
|
|
68
|
+
status: "awaiting-approval",
|
|
69
|
+
children: (
|
|
70
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
|
|
71
|
+
<Text as="p" size="2">The assistant wants to book a viewing on Saturday at 11:00. Nothing happens until you approve.</Text>
|
|
72
|
+
<div style={{ display: "flex", gap: "0.5rem" }}>
|
|
73
|
+
<Button size="sm">Approve</Button>
|
|
74
|
+
<Button variant="ghost" size="sm">Decline</Button>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
),
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const Compact: Story = { args: { compact: true, defaultOpen: false } }
|
|
82
|
+
|
|
83
|
+
export const FromAiSdkToolPart: Story = {
|
|
84
|
+
render: () => (
|
|
85
|
+
<ToolCallCard
|
|
86
|
+
icon={Search}
|
|
87
|
+
defaultOpen
|
|
88
|
+
{...toolPartToCallCard({
|
|
89
|
+
type: "tool-search_listings",
|
|
90
|
+
state: "output-available",
|
|
91
|
+
input: { community: "Yas Island", maxPrice: 5000000 },
|
|
92
|
+
output: { count: 12, cheapest: { price: 4200000, ref: "YA-22B" } },
|
|
93
|
+
toolCallId: "call_8f2a91",
|
|
94
|
+
})}
|
|
95
|
+
/>
|
|
96
|
+
),
|
|
97
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
import { renderToStaticMarkup } from "react-dom/server"
|
|
4
|
+
import {
|
|
5
|
+
TOOL_CALL_STATUS, ToolCallCard, toolCallNeedsAttention, toolPartToCallCard, type ToolCallStatus, type ToolPart,
|
|
6
|
+
} from "./tool-call-card"
|
|
7
|
+
|
|
8
|
+
const Icon = () => <svg />
|
|
9
|
+
const INPUT = [{ label: "Community", value: "Yas Island" }]
|
|
10
|
+
const OUTPUT = [{ label: "Matches", value: "12" }]
|
|
11
|
+
|
|
12
|
+
const render = (status: ToolCallStatus, extra: Partial<React.ComponentProps<typeof ToolCallCard>> = {}) =>
|
|
13
|
+
renderToStaticMarkup(<ToolCallCard icon={Icon} title="Search listings" status={status} input={INPUT} output={OUTPUT} {...extra} />)
|
|
14
|
+
|
|
15
|
+
describe("ToolCallCard", () => {
|
|
16
|
+
it.each(Object.entries(TOOL_CALL_STATUS))("labels %s as %o", (status, { label }) => {
|
|
17
|
+
expect(render(status as ToolCallStatus)).toContain(`>${label}</`)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it("opens for approval and errors, stays closed otherwise", () => {
|
|
21
|
+
expect(toolCallNeedsAttention("awaiting-approval")).toBe(true)
|
|
22
|
+
expect(toolCallNeedsAttention("error")).toBe(true)
|
|
23
|
+
expect(toolCallNeedsAttention("success")).toBe(false)
|
|
24
|
+
expect(render("error")).toContain('aria-expanded="true"')
|
|
25
|
+
expect(render("success")).toContain('aria-expanded="false"')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it("shows the approval body in place of the fields while awaiting approval", () => {
|
|
29
|
+
const html = render("awaiting-approval", { children: <p>Approve this booking?</p> })
|
|
30
|
+
expect(html).toContain("Approve this booking?")
|
|
31
|
+
expect(html).not.toContain("Yas Island")
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("shows results only once the call succeeded", () => {
|
|
35
|
+
expect(render("success", { defaultOpen: true })).toContain(">Result<")
|
|
36
|
+
expect(render("running", { defaultOpen: true })).not.toContain(">Result<")
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("marks the card busy while running", () => {
|
|
40
|
+
expect(render("running")).toContain('aria-busy="true"')
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe("toolPartToCallCard", () => {
|
|
45
|
+
const part: ToolPart = {
|
|
46
|
+
type: "tool-search_listings",
|
|
47
|
+
state: "output-available",
|
|
48
|
+
input: { community: "Yas Island", maxPrice: 5_000_000 },
|
|
49
|
+
output: { count: 12, cheapest: { price: 4_200_000, ref: "YA-22B" } },
|
|
50
|
+
toolCallId: "call_8f2",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
it("names the tool and turns its id into a plain title", () => {
|
|
54
|
+
const props = toolPartToCallCard(part)
|
|
55
|
+
expect(props.toolName).toBe("search_listings")
|
|
56
|
+
expect(props.title).toBe("Search listings")
|
|
57
|
+
expect(props.callId).toBe("call_8f2")
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it.each([
|
|
61
|
+
["input-streaming", "running"],
|
|
62
|
+
["input-available", "running"],
|
|
63
|
+
["approval-requested", "awaiting-approval"],
|
|
64
|
+
["output-available", "success"],
|
|
65
|
+
["output-error", "error"],
|
|
66
|
+
["output-denied", "declined"],
|
|
67
|
+
] as const)("maps %s to %s", (state, status) => {
|
|
68
|
+
expect(toolPartToCallCard({ ...part, state }).status).toBe(status)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it("lists record entries as fields and pretty-prints nested values", () => {
|
|
72
|
+
const props = toolPartToCallCard(part)
|
|
73
|
+
expect(props.input?.map((field) => field.label)).toEqual(["community", "maxPrice"])
|
|
74
|
+
const html = renderToStaticMarkup(<>{props.output?.find((field) => field.label === "cheapest")?.value}</>)
|
|
75
|
+
expect(html).toContain("<pre")
|
|
76
|
+
expect(html).toContain(""price": 4200000")
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("wraps a non-record output in a single result field and passes errors through", () => {
|
|
80
|
+
expect(toolPartToCallCard({ ...part, output: [1, 2] }).output?.map((field) => field.label)).toEqual(["result"])
|
|
81
|
+
expect(toolPartToCallCard({ ...part, state: "output-error", errorText: "Timed out" }).error).toBe("Timed out")
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it("shows the call id at the foot of open details", () => {
|
|
85
|
+
const html = renderToStaticMarkup(<ToolCallCard icon={Icon} defaultOpen {...toolPartToCallCard(part)} />)
|
|
86
|
+
expect(html).toContain("Call ID: call_8f2")
|
|
87
|
+
})
|
|
88
|
+
})
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { ChevronDown } from "lucide-react"
|
|
5
|
+
import { cn } from "../lib/cn"
|
|
6
|
+
import { Alert, AlertDescription } from "./alert"
|
|
7
|
+
import { Badge, type BadgeProps } from "./badge"
|
|
8
|
+
import { Button } from "./button"
|
|
9
|
+
import { Spinner } from "./spinner"
|
|
10
|
+
import { Text } from "./text"
|
|
11
|
+
|
|
12
|
+
export type ToolCallStatus = "awaiting-approval" | "running" | "success" | "error" | "declined" | "cancelled"
|
|
13
|
+
|
|
14
|
+
export interface ToolCallField {
|
|
15
|
+
label: string
|
|
16
|
+
value: React.ReactNode
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const TOOL_CALL_STATUS: Record<ToolCallStatus, { label: string; variant: NonNullable<BadgeProps["variant"]> }> = {
|
|
20
|
+
"awaiting-approval": { label: "Needs approval", variant: "warning" },
|
|
21
|
+
running: { label: "Running", variant: "muted" },
|
|
22
|
+
success: { label: "Done", variant: "success" },
|
|
23
|
+
error: { label: "Failed", variant: "destructive" },
|
|
24
|
+
declined: { label: "Declined", variant: "muted" },
|
|
25
|
+
cancelled: { label: "Cancelled", variant: "muted" },
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Approval and errors need the person, so the card opens for them. */
|
|
29
|
+
export const toolCallNeedsAttention = (status: ToolCallStatus) => status === "awaiting-approval" || status === "error"
|
|
30
|
+
|
|
31
|
+
const seconds = (ms: number) => `${(ms / 1000).toFixed(1)}s`
|
|
32
|
+
|
|
33
|
+
function FieldGroup({ label, fields }: { label: string; fields: ToolCallField[] }) {
|
|
34
|
+
return (
|
|
35
|
+
<div className="ax:flex ax:flex-col ax:gap-xs">
|
|
36
|
+
<Text as="p" size="2" weight="medium" color="muted">{label}</Text>
|
|
37
|
+
<dl className="ax:flex ax:flex-col ax:gap-1 ax:rounded ax:border ax:border-border ax:bg-background ax:p-xs ax:font-mono ax:text-sm">
|
|
38
|
+
{fields.map((field) => (
|
|
39
|
+
<div key={field.label} className="ax:flex ax:min-w-0 ax:flex-wrap ax:gap-x-xs">
|
|
40
|
+
<dt className="ax:text-muted-foreground">{field.label}:</dt>
|
|
41
|
+
<dd className="ax:min-w-0 ax:wrap-break-word ax:text-foreground">{field.value}</dd>
|
|
42
|
+
</div>
|
|
43
|
+
))}
|
|
44
|
+
</dl>
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ToolCallCardProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
|
|
50
|
+
/** The tool's icon; replaced by a spinner while running. */
|
|
51
|
+
icon: React.ElementType
|
|
52
|
+
/** What the call does in plain words, in the tense of its status ("Searching listings", "Searched listings"). */
|
|
53
|
+
title: string
|
|
54
|
+
/** The technical tool name, shown in mono from `sm` up. */
|
|
55
|
+
toolName?: string
|
|
56
|
+
/** One line under the title: the result summary, or why it stopped. */
|
|
57
|
+
subtitle?: string
|
|
58
|
+
status: ToolCallStatus
|
|
59
|
+
/** Shown once the call succeeds. */
|
|
60
|
+
durationMs?: number
|
|
61
|
+
input?: ToolCallField[]
|
|
62
|
+
/** Shown once the call succeeds. */
|
|
63
|
+
output?: ToolCallField[]
|
|
64
|
+
/** Shown when the call fails, with Retry when `onRetry` is set. */
|
|
65
|
+
error?: string
|
|
66
|
+
onRetry?: () => void
|
|
67
|
+
/** Adds Cancel while running. */
|
|
68
|
+
onCancel?: () => void
|
|
69
|
+
/** Tighter header for a run of several calls. */
|
|
70
|
+
compact?: boolean
|
|
71
|
+
/** Whether the body starts open. Defaults to open while awaiting approval or after an error. */
|
|
72
|
+
defaultOpen?: boolean
|
|
73
|
+
/** The approval body shown while `awaiting-approval`, in place of the fields. */
|
|
74
|
+
children?: React.ReactNode
|
|
75
|
+
/** The call's id, shown small at the foot of the details for support and debugging. */
|
|
76
|
+
callId?: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* ToolCallCard - one tool call by an assistant: what it called in plain
|
|
81
|
+
* words, live status, and the inputs and results as labelled fields on
|
|
82
|
+
* demand. Calls with side effects stop at an approval step (`children`).
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* <ToolCallCard icon={Search} title="Searched listings" toolName="search_listings" status="success"
|
|
86
|
+
* subtitle="12 matches" durationMs={1400} input={inputFields} output={resultFields} />
|
|
87
|
+
*/
|
|
88
|
+
export const ToolCallCard = React.forwardRef<HTMLDivElement, ToolCallCardProps>(
|
|
89
|
+
(
|
|
90
|
+
{
|
|
91
|
+
icon: Icon, title, toolName, subtitle, status, durationMs, input = [], output = [], error, onRetry, onCancel,
|
|
92
|
+
compact = false, defaultOpen, children, callId, className, ...props
|
|
93
|
+
},
|
|
94
|
+
ref,
|
|
95
|
+
) => {
|
|
96
|
+
const id = React.useId()
|
|
97
|
+
const needsAttention = toolCallNeedsAttention(status)
|
|
98
|
+
const [open, setOpen] = React.useState(defaultOpen ?? needsAttention)
|
|
99
|
+
React.useEffect(() => {
|
|
100
|
+
if (needsAttention) setOpen(true)
|
|
101
|
+
}, [needsAttention])
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div
|
|
105
|
+
ref={ref}
|
|
106
|
+
data-slot="tool-call-card"
|
|
107
|
+
className={cn(
|
|
108
|
+
"ax:overflow-hidden ax:rounded ax:border ax:border-border ax:bg-card",
|
|
109
|
+
status === "awaiting-approval" && "ax:border-warning-weaker",
|
|
110
|
+
className,
|
|
111
|
+
)}
|
|
112
|
+
aria-busy={status === "running"}
|
|
113
|
+
{...props}
|
|
114
|
+
>
|
|
115
|
+
<div className={cn("ax:flex ax:items-center ax:gap-sm ax:px-sm", compact ? "ax:py-1.5" : "ax:py-xs")}>
|
|
116
|
+
<span className="ax:flex ax:size-4 ax:shrink-0 ax:items-center ax:justify-center ax:text-muted-foreground">
|
|
117
|
+
{status === "running" ? <Spinner size="sm" decorative /> : <Icon className="ax:size-4" aria-hidden />}
|
|
118
|
+
</span>
|
|
119
|
+
<div className="ax:flex ax:min-w-0 ax:flex-1 ax:flex-col">
|
|
120
|
+
<span className="ax:flex ax:min-w-0 ax:items-baseline ax:gap-xs">
|
|
121
|
+
<Text size="2" weight="medium" truncate>{title}</Text>
|
|
122
|
+
{toolName && <Text size="1" color="muted" className="ax:hidden ax:font-mono ax:sm:inline">{toolName}</Text>}
|
|
123
|
+
</span>
|
|
124
|
+
{subtitle && <Text size="1" color={status === "error" ? "destructive" : "muted"} truncate>{subtitle}</Text>}
|
|
125
|
+
</div>
|
|
126
|
+
{status === "success" && durationMs !== undefined && (
|
|
127
|
+
<Text size="1" color="muted" className="ax:hidden ax:tabular-nums ax:sm:inline">{seconds(durationMs)}</Text>
|
|
128
|
+
)}
|
|
129
|
+
<Badge variant={TOOL_CALL_STATUS[status].variant} size="sm" className="ax:rounded-full" aria-live="polite">
|
|
130
|
+
{TOOL_CALL_STATUS[status].label}
|
|
131
|
+
</Badge>
|
|
132
|
+
{status === "running" && onCancel && <Button variant="ghost" size="sm" className="ax:rounded-full" onClick={onCancel}>Cancel</Button>}
|
|
133
|
+
<Button
|
|
134
|
+
variant="ghost"
|
|
135
|
+
size="icon-sm"
|
|
136
|
+
className="ax:rounded-full ax:text-muted-foreground ax:hover:text-foreground"
|
|
137
|
+
aria-expanded={open}
|
|
138
|
+
aria-controls={`${id}-body`}
|
|
139
|
+
aria-label={open ? `Hide details for ${title}` : `Show details for ${title}`}
|
|
140
|
+
onClick={() => setOpen((value) => !value)}
|
|
141
|
+
>
|
|
142
|
+
<ChevronDown className={cn("ax:transition-transform ax:motion-reduce:transition-none", open && "ax:rotate-180")} />
|
|
143
|
+
</Button>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{open && (
|
|
147
|
+
<div id={`${id}-body`} className="ax:flex ax:flex-col ax:gap-sm ax:border-t ax:border-border ax:p-sm">
|
|
148
|
+
{status === "awaiting-approval" && children ? children : (
|
|
149
|
+
<>
|
|
150
|
+
{status === "error" && error && (
|
|
151
|
+
<Alert variant="destructive" size="md">
|
|
152
|
+
<AlertDescription>{error}</AlertDescription>
|
|
153
|
+
{onRetry && (
|
|
154
|
+
<Button variant="ghost" size="sm" className="ax:rounded-full ax:self-start" onClick={onRetry}>Retry</Button>
|
|
155
|
+
)}
|
|
156
|
+
</Alert>
|
|
157
|
+
)}
|
|
158
|
+
{input.length > 0 && <FieldGroup label="Input" fields={input} />}
|
|
159
|
+
{status === "success" && output.length > 0 && <FieldGroup label="Result" fields={output} />}
|
|
160
|
+
</>
|
|
161
|
+
)}
|
|
162
|
+
{callId && <Text as="p" size="1" color="muted" className="ax:font-mono ax:break-all">Call ID: {callId}</Text>}
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
)
|
|
167
|
+
},
|
|
168
|
+
)
|
|
169
|
+
ToolCallCard.displayName = "ToolCallCard"
|
|
170
|
+
|
|
171
|
+
/** A tool call part as the Vercel AI SDK streams it: `type` is "tool-<name>". */
|
|
172
|
+
export interface ToolPart {
|
|
173
|
+
type: string
|
|
174
|
+
state: "input-streaming" | "input-available" | "approval-requested" | "output-available" | "output-error" | "output-denied"
|
|
175
|
+
input?: unknown
|
|
176
|
+
output?: unknown
|
|
177
|
+
errorText?: string
|
|
178
|
+
toolCallId?: string
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const TOOL_PART_STATUS: Record<ToolPart["state"], ToolCallStatus> = {
|
|
182
|
+
"input-streaming": "running",
|
|
183
|
+
"input-available": "running",
|
|
184
|
+
"approval-requested": "awaiting-approval",
|
|
185
|
+
"output-available": "success",
|
|
186
|
+
"output-error": "error",
|
|
187
|
+
"output-denied": "declined",
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Plain values as text; objects and arrays as scrollable pretty-printed JSON. */
|
|
191
|
+
export function formatToolValue(value: unknown): React.ReactNode {
|
|
192
|
+
if (value === null || value === undefined) return String(value)
|
|
193
|
+
if (typeof value !== "object") return String(value)
|
|
194
|
+
return <pre className="ax:max-h-60 ax:overflow-auto ax:whitespace-pre-wrap">{JSON.stringify(value, null, 2)}</pre>
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function toFields(value: unknown, fallbackLabel: string): ToolCallField[] {
|
|
198
|
+
if (value === undefined) return []
|
|
199
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
200
|
+
return Object.entries(value).map(([label, entry]) => ({ label, value: formatToolValue(entry) }))
|
|
201
|
+
}
|
|
202
|
+
return [{ label: fallbackLabel, value: formatToolValue(value) }]
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** "tool-search_listings" to "Search listings". */
|
|
206
|
+
const humanize = (name: string) => {
|
|
207
|
+
const words = name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim().toLowerCase()
|
|
208
|
+
return words.charAt(0).toUpperCase() + words.slice(1)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export type ToolPartCardProps = Pick<ToolCallCardProps, "title" | "toolName" | "status" | "input" | "output" | "error" | "callId">
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Maps an AI SDK tool part to ToolCallCard props. Pass the icon yourself, and
|
|
215
|
+
* override `title` with plain words in the right tense when you have them.
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* <ToolCallCard icon={Search} {...toolPartToCallCard(part)} />
|
|
219
|
+
*/
|
|
220
|
+
export function toolPartToCallCard(part: ToolPart): ToolPartCardProps {
|
|
221
|
+
const toolName = part.type.replace(/^tool-/, "")
|
|
222
|
+
return {
|
|
223
|
+
title: humanize(toolName),
|
|
224
|
+
toolName,
|
|
225
|
+
status: TOOL_PART_STATUS[part.state],
|
|
226
|
+
input: toFields(part.input, "input"),
|
|
227
|
+
output: toFields(part.output, "result"),
|
|
228
|
+
error: part.errorText,
|
|
229
|
+
callId: part.toolCallId,
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { Button } from "./button"
|
|
4
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip"
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Tooltip> = {
|
|
7
|
+
title: "Components/Tooltip",
|
|
8
|
+
component: Tooltip,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
decorators: [(Story) => <TooltipProvider><Story /></TooltipProvider>],
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default meta
|
|
14
|
+
type Story = StoryObj<typeof Tooltip>
|
|
15
|
+
|
|
16
|
+
export const Default: Story = {
|
|
17
|
+
render: () => (
|
|
18
|
+
<Tooltip>
|
|
19
|
+
<TooltipTrigger asChild>
|
|
20
|
+
<Button variant="outline">Hover me</Button>
|
|
21
|
+
</TooltipTrigger>
|
|
22
|
+
<TooltipContent>Helpful tooltip text</TooltipContent>
|
|
23
|
+
</Tooltip>
|
|
24
|
+
),
|
|
25
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
|
5
|
+
import { cn } from "../lib/cn"
|
|
6
|
+
|
|
7
|
+
export const TooltipProvider = TooltipPrimitive.Provider
|
|
8
|
+
export const Tooltip = TooltipPrimitive.Root
|
|
9
|
+
export const TooltipTrigger = TooltipPrimitive.Trigger
|
|
10
|
+
|
|
11
|
+
export interface TooltipContentProps
|
|
12
|
+
extends React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> {}
|
|
13
|
+
|
|
14
|
+
export const TooltipContent = React.forwardRef<
|
|
15
|
+
React.ElementRef<typeof TooltipPrimitive.Content>,
|
|
16
|
+
TooltipContentProps
|
|
17
|
+
>(({ className, sideOffset = 6, ...props }, ref) => (
|
|
18
|
+
<TooltipPrimitive.Portal>
|
|
19
|
+
<TooltipPrimitive.Content
|
|
20
|
+
ref={ref}
|
|
21
|
+
sideOffset={sideOffset}
|
|
22
|
+
className={cn(
|
|
23
|
+
// Layered above Dialog/Drawer (z-[1100]) so tooltips on overlay UI stay visible.
|
|
24
|
+
"ax:z-1200 ax:overflow-hidden ax:rounded-none",
|
|
25
|
+
"ax:bg-foreground ax:px-sm ax:py-xs",
|
|
26
|
+
"ax:t-body-sm ax:text-background",
|
|
27
|
+
"ax:shadow-md",
|
|
28
|
+
"ax:animate-in ax:fade-in-0 ax:zoom-in-[0.96]",
|
|
29
|
+
"ax:data-[state=closed]:animate-out ax:data-[state=closed]:fade-out-0 ax:data-[state=closed]:zoom-out-[0.96]",
|
|
30
|
+
"ax:data-[side=bottom]:slide-in-from-top-1",
|
|
31
|
+
"ax:data-[side=left]:slide-in-from-right-1",
|
|
32
|
+
"ax:data-[side=right]:slide-in-from-left-1",
|
|
33
|
+
"ax:data-[side=top]:slide-in-from-bottom-1",
|
|
34
|
+
// Tooltips need to feel instant, intentionally faster than --motion-fast (160ms).
|
|
35
|
+
"ax:[animation-duration:100ms] ax:[animation-timing-function:var(--motion-ease-out)]",
|
|
36
|
+
"ax:motion-reduce:animate-none",
|
|
37
|
+
className
|
|
38
|
+
)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
</TooltipPrimitive.Portal>
|
|
42
|
+
))
|
|
43
|
+
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { Breadcrumbs, TopNav } from "./top-nav"
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof TopNav> = {
|
|
6
|
+
title: "Components/TopNav",
|
|
7
|
+
component: TopNav,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof TopNav>
|
|
13
|
+
|
|
14
|
+
export const Default: Story = {
|
|
15
|
+
render: () => (
|
|
16
|
+
<TopNav>
|
|
17
|
+
<Breadcrumbs items={[{ label: "Home", href: "#" }, { label: "Projects", href: "#" }, { label: "Overview" }]} />
|
|
18
|
+
</TopNav>
|
|
19
|
+
),
|
|
20
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../lib/cn"
|
|
5
|
+
|
|
6
|
+
export interface TopNavProps extends React.HTMLAttributes<HTMLElement> {
|
|
7
|
+
/** Left-aligned slot (typically breadcrumbs or a page title). */
|
|
8
|
+
start?: React.ReactNode
|
|
9
|
+
/** Right-aligned slot (search, notifications, user menu). */
|
|
10
|
+
end?: React.ReactNode
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* TopNav - sticky top bar for the web app shell.
|
|
15
|
+
* Supports both webapp (with sidebar context) and standalone usage.
|
|
16
|
+
*/
|
|
17
|
+
export const TopNav = React.forwardRef<HTMLElement, TopNavProps>(
|
|
18
|
+
({ className, start, end, children, ...props }, ref) => (
|
|
19
|
+
<header
|
|
20
|
+
ref={ref}
|
|
21
|
+
className={cn(
|
|
22
|
+
"ax:sticky ax:top-0 ax:z-100 ax:flex ax:h-5xl ax:w-full ax:shrink-0 ax:items-center ax:gap-md",
|
|
23
|
+
"ax:border-b ax:border-border ax:bg-background/80 ax:backdrop-blur-md",
|
|
24
|
+
"ax:px-md ax:md:px-xl",
|
|
25
|
+
className
|
|
26
|
+
)}
|
|
27
|
+
{...props}
|
|
28
|
+
>
|
|
29
|
+
<div className="ax:flex ax:items-center ax:gap-sm ax:flex-1 ax:min-w-0">{start}</div>
|
|
30
|
+
<div className="ax:flex-1 ax:flex ax:items-center ax:justify-center ax:min-w-0">{children}</div>
|
|
31
|
+
<div className="ax:flex ax:items-center ax:gap-xs ax:flex-1 ax:min-w-0 ax:justify-end">{end}</div>
|
|
32
|
+
</header>
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
TopNav.displayName = "TopNav"
|
|
36
|
+
|
|
37
|
+
export interface BreadcrumbsProps extends React.HTMLAttributes<HTMLElement> {
|
|
38
|
+
items: { label: string; href?: string }[]
|
|
39
|
+
/** Custom separator. Default: '/'. */
|
|
40
|
+
separator?: React.ReactNode
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const Breadcrumbs = React.forwardRef<HTMLElement, BreadcrumbsProps>(
|
|
44
|
+
({ className, items, separator = "/", ...props }, ref) => (
|
|
45
|
+
<nav ref={ref} aria-label="Breadcrumb" className={cn("ax:flex ax:items-center", className)} {...props}>
|
|
46
|
+
<ol className="ax:flex ax:items-center ax:gap-xs">
|
|
47
|
+
{items.map((item, i) => {
|
|
48
|
+
const isLast = i === items.length - 1
|
|
49
|
+
return (
|
|
50
|
+
<li key={`${item.label}-${i}`} className="ax:flex ax:items-center ax:gap-xs">
|
|
51
|
+
{i > 0 && (
|
|
52
|
+
<span aria-hidden="true" className="ax:t-caption ax:text-muted-foreground/60">
|
|
53
|
+
{separator}
|
|
54
|
+
</span>
|
|
55
|
+
)}
|
|
56
|
+
{item.href ? (
|
|
57
|
+
<a
|
|
58
|
+
href={item.href}
|
|
59
|
+
aria-current={isLast ? "page" : undefined}
|
|
60
|
+
className="ax:t-body-sm ax:text-muted-foreground ax:hover:text-foreground ax:transition-colors ax:focus-visible:outline-none ax:focus-visible:underline"
|
|
61
|
+
>
|
|
62
|
+
{item.label}
|
|
63
|
+
</a>
|
|
64
|
+
) : (
|
|
65
|
+
<span aria-current={isLast ? "page" : undefined} className="ax:t-label-sm ax:text-foreground">
|
|
66
|
+
{item.label}
|
|
67
|
+
</span>
|
|
68
|
+
)}
|
|
69
|
+
</li>
|
|
70
|
+
)
|
|
71
|
+
})}
|
|
72
|
+
</ol>
|
|
73
|
+
</nav>
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
Breadcrumbs.displayName = "Breadcrumbs"
|