@adres/mgda-ui 0.0.0-stage → 0.0.1
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,392 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { motion, type HTMLMotionProps } from "motion/react"
|
|
5
|
+
import { cn } from "../lib/cn"
|
|
6
|
+
import { useReducedMotion } from "../hooks/use-reduced-motion"
|
|
7
|
+
import { hover, transitions } from "../lib/motion"
|
|
8
|
+
|
|
9
|
+
export type CardVariant = "default" | "surface" | "ghost"
|
|
10
|
+
export type CardPadding = "none" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl"
|
|
11
|
+
export type CardGap = "sm" | "md" | "lg" | "xl"
|
|
12
|
+
export type CardElevation = "none" | "xs" | "sm" | "level0" | "level1" | "level2" | "level3"
|
|
13
|
+
|
|
14
|
+
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
15
|
+
/** Visual treatment of the card surface. */
|
|
16
|
+
variant?: CardVariant
|
|
17
|
+
/** Padding inside the card. All responsive variants scale from their mobile floor up to
|
|
18
|
+
* the named size at XL (1280px+), one step per breakpoint.
|
|
19
|
+
* Fixed: none · sm(12px) · md(16px)
|
|
20
|
+
* Responsive: lg(→20px) · xl(→24px) · 2xl(→32px) · 3xl(→40px) · 4xl(→48px) */
|
|
21
|
+
padding?: CardPadding
|
|
22
|
+
/** When set, makes the card a vertical flex column with the given gap between children.
|
|
23
|
+
* sm=12px md=16px lg=20px xl=24px */
|
|
24
|
+
gap?: CardGap
|
|
25
|
+
/** Surface elevation - bundles a background fill with a shadow into one level,
|
|
26
|
+
* overriding the variant's own background and shadow (the variant's border, if
|
|
27
|
+
* any, is kept). Reads the brand-themeable --elevation-* tokens.
|
|
28
|
+
* level0 - transparent, flat
|
|
29
|
+
* level1 - translucent surface (50%), flat
|
|
30
|
+
* level2 - solid surface + subtle shadow
|
|
31
|
+
* level3 - solid surface + stronger shadow
|
|
32
|
+
* Left unset, the card keeps its variant's default (brand-themeable) chrome. */
|
|
33
|
+
elevation?: CardElevation
|
|
34
|
+
/** When true, the card zooms subtly on hover. Disabled by reduced-motion preference. */
|
|
35
|
+
interactive?: boolean
|
|
36
|
+
/** Hover animation style when `interactive` is true.
|
|
37
|
+
* "scale" (default) - a light zoom (1.03×) that raises the shadow one level ·
|
|
38
|
+
* "lift" - translates up 4px */
|
|
39
|
+
hoverEffect?: "lift" | "scale"
|
|
40
|
+
/** When true, removes the border and shadow from the card. */
|
|
41
|
+
borderless?: boolean
|
|
42
|
+
/** Split the card into a muted header band with a content section beneath it.
|
|
43
|
+
* The card's own padding is dropped and each sub-component (CardHeader /
|
|
44
|
+
* CardContent / CardFooter) supplies its own padding instead; the header runs
|
|
45
|
+
* full-bleed with `bg-muted` + a bottom divider, its top corners rounded by
|
|
46
|
+
* the card's own radius. */
|
|
47
|
+
sectioned?: boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// When `sectioned`, the card goes padding-less and hands the padding scale to its
|
|
51
|
+
// sub-components via context so the muted header band can run full-bleed to the
|
|
52
|
+
// rounded edges while each section keeps consistent interior padding.
|
|
53
|
+
// `headerSize` mirrors the sectioned header's `size` (when set) so the content
|
|
54
|
+
// and footer can match the header's padding scale.
|
|
55
|
+
interface CardSectionContextValue { padding: CardPadding; headerSize?: CardHeaderSize }
|
|
56
|
+
const CardSectionContext = React.createContext<CardSectionContextValue | null>(null)
|
|
57
|
+
|
|
58
|
+
const CARD_BASE = "ax:rounded-md ax:text-card-foreground ax:overflow-hidden"
|
|
59
|
+
|
|
60
|
+
const CARD_VARIANT: Record<CardVariant, string> = {
|
|
61
|
+
default: "ax:border ax:bg-card ax:shadow-sm",
|
|
62
|
+
surface: "ax:border ax:border-border ax:bg-background",
|
|
63
|
+
ghost: "ax:bg-transparent",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Just the border treatment of each variant, with no background or shadow. Used
|
|
67
|
+
// when `elevation` is set: the level supplies the surface (bg + shadow), so the
|
|
68
|
+
// variant's own bg/shadow are dropped rather than fighting the level's utilities.
|
|
69
|
+
const CARD_VARIANT_BORDER: Record<CardVariant, string> = {
|
|
70
|
+
default: "ax:border",
|
|
71
|
+
surface: "ax:border ax:border-border",
|
|
72
|
+
ghost: "",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const CARD_PADDING: Record<CardPadding, string> = {
|
|
76
|
+
none: "",
|
|
77
|
+
sm: "ax:p-sm", // 12px - fixed
|
|
78
|
+
md: "ax:p-md", // 16px - fixed
|
|
79
|
+
lg: "ax:p-md ax:md:p-lg", // 16 → 20px
|
|
80
|
+
xl: "ax:p-md ax:sm:p-lg ax:xl:p-xl", // 16 → 20 → 24px
|
|
81
|
+
"2xl": "ax:p-sm ax:sm:p-md ax:md:p-lg ax:lg:p-xl ax:xl:p-2xl", // 12 → 16 → 20 → 24 → 32px
|
|
82
|
+
"3xl": "ax:p-md ax:sm:p-lg ax:md:p-xl ax:lg:p-2xl ax:xl:p-3xl", // 16 → 20 → 24 → 32 → 40px
|
|
83
|
+
"4xl": "ax:p-lg ax:sm:p-xl ax:md:p-2xl ax:lg:p-3xl ax:xl:p-4xl", // 20 → 24 → 32 → 40 → 48px
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const CARD_GAP: Record<CardGap, string> = {
|
|
87
|
+
sm: "ax:flex ax:flex-col ax:gap-sm",
|
|
88
|
+
md: "ax:flex ax:flex-col ax:gap-md",
|
|
89
|
+
lg: "ax:flex ax:flex-col ax:gap-lg",
|
|
90
|
+
xl: "ax:flex ax:flex-col ax:gap-xl",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Surface elevation - fill + shadow per level, read from the brand-themeable
|
|
94
|
+
// --elevation-* tokens. Applied AFTER the variant classes in cn() so tailwind-merge
|
|
95
|
+
// lets the level win on background-color and box-shadow while the variant's border
|
|
96
|
+
// (if any) survives.
|
|
97
|
+
const CARD_ELEVATION: Record<CardElevation, string> = {
|
|
98
|
+
none: "ax:bg-card ax:shadow-none",
|
|
99
|
+
xs: "ax:bg-card ax:shadow-xs",
|
|
100
|
+
sm: "ax:bg-card ax:shadow-sm",
|
|
101
|
+
level0: "ax:bg-[var(--elevation-0-surface)] ax:shadow-[var(--elevation-0-shadow)]",
|
|
102
|
+
level1: "ax:bg-[var(--elevation-1-surface)] ax:shadow-[var(--elevation-1-shadow)]",
|
|
103
|
+
level2: "ax:bg-[var(--elevation-2-surface)] ax:shadow-[var(--elevation-2-shadow)]",
|
|
104
|
+
level3: "ax:bg-[var(--elevation-3-surface)] ax:shadow-[var(--elevation-3-shadow)]",
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Exposes the active padding value to Inset children so they can break out via negative margin.
|
|
108
|
+
const INSET_STYLE: Record<CardPadding, React.CSSProperties> = {
|
|
109
|
+
none: {},
|
|
110
|
+
sm: { "--inset-padding": "var(--space-sm)" } as React.CSSProperties,
|
|
111
|
+
md: { "--inset-padding": "var(--space-md)" } as React.CSSProperties,
|
|
112
|
+
lg: { "--inset-padding": "var(--space-lg)" } as React.CSSProperties,
|
|
113
|
+
xl: { "--inset-padding": "var(--space-xl)" } as React.CSSProperties,
|
|
114
|
+
"2xl": { "--inset-padding": "var(--space-2xl)" } as React.CSSProperties,
|
|
115
|
+
"3xl": { "--inset-padding": "var(--space-3xl)" } as React.CSSProperties,
|
|
116
|
+
"4xl": { "--inset-padding": "var(--space-4xl)" } as React.CSSProperties,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Utility for applying Card surface classes to non-div elements (e.g. motion.figure, a). */
|
|
120
|
+
export function cardCn(variant: CardVariant = "default", padding: CardPadding = "xl", extra?: string) {
|
|
121
|
+
return cn(CARD_BASE, CARD_VARIANT[variant], CARD_PADDING[padding], extra)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
|
125
|
+
({ className, variant = "default", padding = "xl", gap, elevation, interactive, hoverEffect = "scale", borderless, sectioned, style, children, ...props }, ref) => {
|
|
126
|
+
const reduce = useReducedMotion()
|
|
127
|
+
|
|
128
|
+
// For non-interactive default cards, defer border-color and shadow to brand CSS variables
|
|
129
|
+
// so themes can control card chrome without touching Tailwind utilities.
|
|
130
|
+
// Interactive cards are excluded because hover:shadow-md (class) can't beat inline styles.
|
|
131
|
+
const cardBrandStyle: React.CSSProperties =
|
|
132
|
+
variant === "default" && !borderless && !elevation && !interactive
|
|
133
|
+
? { borderColor: "var(--card-border-color)", boxShadow: "var(--card-shadow)" }
|
|
134
|
+
: {}
|
|
135
|
+
|
|
136
|
+
const mergedStyle = { ...INSET_STYLE[padding], ...cardBrandStyle, ...style }
|
|
137
|
+
const borderlessClass = borderless
|
|
138
|
+
? elevation ? "ax:border-0" : "ax:border-0 ax:shadow-none"
|
|
139
|
+
: undefined
|
|
140
|
+
// Sectioned cards go padding-less: the header/content sections own their padding.
|
|
141
|
+
const paddingClass = sectioned ? "" : CARD_PADDING[padding]
|
|
142
|
+
const gapClass = sectioned ? undefined : (gap ? CARD_GAP[gap] : undefined)
|
|
143
|
+
// Mirror the sectioned CardBanner's `size` so content/footer match its padding.
|
|
144
|
+
let headerSize: CardHeaderSize | undefined
|
|
145
|
+
if (sectioned) {
|
|
146
|
+
React.Children.forEach(children, (child) => {
|
|
147
|
+
if (React.isValidElement(child) && (child.type as { displayName?: string })?.displayName === "CardBanner") {
|
|
148
|
+
headerSize = (child.props as CardBannerProps).size ?? "lg"
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
const renderChildren = sectioned
|
|
153
|
+
? <CardSectionContext.Provider value={{ padding, headerSize }}>{children}</CardSectionContext.Provider>
|
|
154
|
+
: children
|
|
155
|
+
// When elevation is set it owns the surface (bg + shadow); keep only the
|
|
156
|
+
// variant's border so the level's utilities aren't fighting the variant's own
|
|
157
|
+
// background/shadow. Unset → the variant's full default chrome.
|
|
158
|
+
const surfaceClass = cn(
|
|
159
|
+
elevation
|
|
160
|
+
? cn(CARD_VARIANT_BORDER[variant], CARD_ELEVATION[elevation])
|
|
161
|
+
: CARD_VARIANT[variant],
|
|
162
|
+
// Sectioned: paint the header surface on the card root so the card's
|
|
163
|
+
// rounded top corners read as the header, AND so the content container's
|
|
164
|
+
// rounded top corners reveal the header colour behind them.
|
|
165
|
+
sectioned && "ax:bg-neutral-weakest",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if (interactive && !reduce) {
|
|
169
|
+
// "scale" - card chrome grows, content stays at natural size via counter-scale.
|
|
170
|
+
// The outer div scales up (overlapping siblings); the inner div scales back down
|
|
171
|
+
// so text and icons remain visually unchanged.
|
|
172
|
+
if (hoverEffect === "scale") {
|
|
173
|
+
const SCALE = 1.03
|
|
174
|
+
return (
|
|
175
|
+
<motion.div
|
|
176
|
+
ref={ref as React.Ref<HTMLDivElement>}
|
|
177
|
+
style={mergedStyle}
|
|
178
|
+
className={cn(
|
|
179
|
+
CARD_BASE, surfaceClass, borderlessClass,
|
|
180
|
+
"ax:hover:shadow-md ax:transition-shadow ax:transition-snappy ax:cursor-pointer ax:relative ax:hover:z-10",
|
|
181
|
+
className
|
|
182
|
+
)}
|
|
183
|
+
whileHover={{ scale: SCALE }}
|
|
184
|
+
transition={transitions.spring}
|
|
185
|
+
{...(props as HTMLMotionProps<"div">)}
|
|
186
|
+
>
|
|
187
|
+
<motion.div
|
|
188
|
+
className={cn(paddingClass, gapClass, "ax:h-full ax:flex ax:flex-col")}
|
|
189
|
+
whileHover={{ scale: 1 / SCALE }}
|
|
190
|
+
transition={transitions.spring}
|
|
191
|
+
>
|
|
192
|
+
{renderChildren}
|
|
193
|
+
</motion.div>
|
|
194
|
+
</motion.div>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<motion.div
|
|
200
|
+
ref={ref as React.Ref<HTMLDivElement>}
|
|
201
|
+
style={mergedStyle}
|
|
202
|
+
className={cn(
|
|
203
|
+
CARD_BASE, surfaceClass, paddingClass, gapClass, borderlessClass,
|
|
204
|
+
sectioned && "ax:flex ax:flex-col",
|
|
205
|
+
"ax:hover:shadow-md ax:transition-shadow ax:transition-snappy ax:cursor-pointer",
|
|
206
|
+
className
|
|
207
|
+
)}
|
|
208
|
+
whileHover={hover.lift}
|
|
209
|
+
transition={transitions.spring}
|
|
210
|
+
{...(props as HTMLMotionProps<"div">)}
|
|
211
|
+
>
|
|
212
|
+
{renderChildren}
|
|
213
|
+
</motion.div>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<div
|
|
219
|
+
ref={ref}
|
|
220
|
+
style={mergedStyle}
|
|
221
|
+
className={cn(
|
|
222
|
+
CARD_BASE, surfaceClass, paddingClass, gapClass, borderlessClass,
|
|
223
|
+
sectioned && "ax:flex ax:flex-col",
|
|
224
|
+
interactive && "ax:cursor-pointer",
|
|
225
|
+
className
|
|
226
|
+
)}
|
|
227
|
+
{...props}
|
|
228
|
+
>
|
|
229
|
+
{renderChildren}
|
|
230
|
+
</div>
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
)
|
|
234
|
+
Card.displayName = "Card"
|
|
235
|
+
|
|
236
|
+
// ── CardHeader sizing ──────────────────────────────────────────────────────
|
|
237
|
+
export type CardHeaderSize = "xs" | "sm" | "md" | "lg" | "xl"
|
|
238
|
+
|
|
239
|
+
// Title type token per size - the header's `size` drives its CardTitle.
|
|
240
|
+
const HEADER_TITLE: Record<CardHeaderSize, string> = {
|
|
241
|
+
xs: "ax:t-h6", sm: "ax:t-h5", md: "ax:t-h4", lg: "ax:t-h3", xl: "ax:t-h2",
|
|
242
|
+
}
|
|
243
|
+
// Description type token per size - drives a nested CardDescription.
|
|
244
|
+
const HEADER_DESC: Record<CardHeaderSize, string> = {
|
|
245
|
+
xs: "ax:t-body-xs", sm: "ax:t-body-sm", md: "ax:t-body-sm", lg: "ax:t-body-md", xl: "ax:t-body-md",
|
|
246
|
+
}
|
|
247
|
+
// Padding when the header owns its box (a `sectioned` band): horizontal on the
|
|
248
|
+
// p-* scale, vertical on the tighter py-* scale.
|
|
249
|
+
const HEADER_PADDING: Record<CardHeaderSize, string> = {
|
|
250
|
+
xs: "ax:px-sm ax:py-xs", sm: "ax:px-md ax:py-sm", md: "ax:px-lg ax:py-md", lg: "ax:px-xl ax:py-lg", xl: "ax:px-2xl ax:py-xl",
|
|
251
|
+
}
|
|
252
|
+
// Bottom-only padding (non-sectioned: root handles top/sides, header adds the
|
|
253
|
+
// gap before the content).
|
|
254
|
+
const HEADER_PADDING_BOTTOM: Record<CardHeaderSize, string> = {
|
|
255
|
+
xs: "ax:pb-sm", sm: "ax:pb-md", md: "ax:pb-lg", lg: "ax:pb-xl", xl: "ax:pb-2xl",
|
|
256
|
+
}
|
|
257
|
+
// Content/footer padding when the sectioned header carries a `size`: all-sides
|
|
258
|
+
// on the header's horizontal scale so the sections line up under the header.
|
|
259
|
+
const SECTION_PADDING: Record<CardHeaderSize, string> = {
|
|
260
|
+
xs: "ax:p-sm", sm: "ax:p-md", md: "ax:p-lg", lg: "ax:p-xl", xl: "ax:p-2xl",
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Lets CardHeader's `size` drive the type tokens of a nested CardTitle /
|
|
264
|
+
// CardDescription. Defaults match the legacy (unsized) header.
|
|
265
|
+
interface CardHeaderType { title: string; description: string }
|
|
266
|
+
const CardHeaderTypeContext = React.createContext<CardHeaderType>({ title: "ax:t-h3", description: "ax:t-body-sm" })
|
|
267
|
+
|
|
268
|
+
export interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
269
|
+
/** Sizes the header - its bottom padding and the nested CardTitle /
|
|
270
|
+
* CardDescription type tokens. xs=t-h6 · sm=t-h5 · md=t-h4 · lg=t-h3 (default)
|
|
271
|
+
* · xl=t-h2. Leave unset to keep the legacy defaults (t-h3 / t-body-sm). */
|
|
272
|
+
size?: CardHeaderSize
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// CardHeader - the default header: title + description stacked with a small gap,
|
|
276
|
+
// bottom padding before the content (root padding handles top/sides). For the
|
|
277
|
+
// darker full-bleed band of a `sectioned` card, use CardBanner instead.
|
|
278
|
+
export const CardHeader = React.forwardRef<HTMLDivElement, CardHeaderProps>(
|
|
279
|
+
({ className, size, ...props }, ref) => {
|
|
280
|
+
// Explicit `size` drives the title/description type tokens; unset keeps the
|
|
281
|
+
// legacy defaults (t-h3 / t-body-sm).
|
|
282
|
+
const type = React.useMemo<CardHeaderType>(
|
|
283
|
+
() => size
|
|
284
|
+
? { title: HEADER_TITLE[size], description: HEADER_DESC[size] }
|
|
285
|
+
: { title: "ax:t-h3", description: "ax:t-body-sm" },
|
|
286
|
+
[size],
|
|
287
|
+
)
|
|
288
|
+
return (
|
|
289
|
+
<CardHeaderTypeContext.Provider value={type}>
|
|
290
|
+
<div
|
|
291
|
+
ref={ref}
|
|
292
|
+
className={cn("ax:flex ax:flex-col ax:gap-sm", size ? HEADER_PADDING_BOTTOM[size] : "ax:pb-xl", className)}
|
|
293
|
+
{...props}
|
|
294
|
+
/>
|
|
295
|
+
</CardHeaderTypeContext.Provider>
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
)
|
|
299
|
+
CardHeader.displayName = "CardHeader"
|
|
300
|
+
|
|
301
|
+
export interface CardBannerProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
302
|
+
/** Sizes the band - its padding and the nested CardTitle / CardDescription
|
|
303
|
+
* type tokens (and the matching CardContent/CardFooter padding).
|
|
304
|
+
* xs=t-h6 · sm=t-h5 · md=t-h4 · lg=t-h3 (default) · xl=t-h2. */
|
|
305
|
+
size?: CardHeaderSize
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// CardBanner - the darker, full-bleed header band for a `sectioned` card. Its top
|
|
309
|
+
// corners are rounded by the card's radius; the CardContent below tucks under it
|
|
310
|
+
// with rounded top corners that reveal this band's colour.
|
|
311
|
+
export const CardBanner = React.forwardRef<HTMLDivElement, CardBannerProps>(
|
|
312
|
+
({ className, size = "lg", ...props }, ref) => {
|
|
313
|
+
const type = React.useMemo<CardHeaderType>(
|
|
314
|
+
() => ({ title: HEADER_TITLE[size], description: HEADER_DESC[size] }),
|
|
315
|
+
[size],
|
|
316
|
+
)
|
|
317
|
+
return (
|
|
318
|
+
<CardHeaderTypeContext.Provider value={type}>
|
|
319
|
+
<div
|
|
320
|
+
ref={ref}
|
|
321
|
+
className={cn(
|
|
322
|
+
"ax:flex ax:flex-col ax:bg-neutral-weakest/50 ax:dark:bg-neutral-weaker/50",
|
|
323
|
+
HEADER_PADDING[size],
|
|
324
|
+
className,
|
|
325
|
+
)}
|
|
326
|
+
{...props}
|
|
327
|
+
/>
|
|
328
|
+
</CardHeaderTypeContext.Provider>
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
)
|
|
332
|
+
CardBanner.displayName = "CardBanner"
|
|
333
|
+
|
|
334
|
+
export const CardTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
|
335
|
+
({ className, ...props }, ref) => {
|
|
336
|
+
const { title } = React.useContext(CardHeaderTypeContext)
|
|
337
|
+
return (
|
|
338
|
+
<h3
|
|
339
|
+
ref={ref}
|
|
340
|
+
className={cn(title, "ax:text-foreground", className)}
|
|
341
|
+
{...props}
|
|
342
|
+
/>
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
)
|
|
346
|
+
CardTitle.displayName = "CardTitle"
|
|
347
|
+
|
|
348
|
+
export const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
|
349
|
+
({ className, ...props }, ref) => {
|
|
350
|
+
const { description } = React.useContext(CardHeaderTypeContext)
|
|
351
|
+
return (
|
|
352
|
+
<p ref={ref} className={cn(description, "ax:text-muted-foreground", className)} {...props} />
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
)
|
|
356
|
+
CardDescription.displayName = "CardDescription"
|
|
357
|
+
|
|
358
|
+
// CardContent - root padding handles all spacing; in a `sectioned` card it
|
|
359
|
+
// supplies its own padding instead.
|
|
360
|
+
export const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
361
|
+
({ className, ...props }, ref) => {
|
|
362
|
+
const section = React.useContext(CardSectionContext)
|
|
363
|
+
return (
|
|
364
|
+
<div
|
|
365
|
+
ref={ref}
|
|
366
|
+
className={cn("ax:t-body-sm ax:text-foreground", section && cn("ax:bg-card ax:rounded-t-lg ax:flex-1", section.headerSize ? SECTION_PADDING[section.headerSize] : CARD_PADDING[section.padding]), className)}
|
|
367
|
+
{...props}
|
|
368
|
+
/>
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
)
|
|
372
|
+
CardContent.displayName = "CardContent"
|
|
373
|
+
|
|
374
|
+
// CardFooter - top padding separates it from CardContent above. In a `sectioned`
|
|
375
|
+
// card it becomes a divided band with its own padding.
|
|
376
|
+
export const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
377
|
+
({ className, ...props }, ref) => {
|
|
378
|
+
const section = React.useContext(CardSectionContext)
|
|
379
|
+
return (
|
|
380
|
+
<div
|
|
381
|
+
ref={ref}
|
|
382
|
+
className={cn(
|
|
383
|
+
"ax:flex ax:items-center",
|
|
384
|
+
section ? cn("ax:bg-card ax:border-t ax:border-border", section.headerSize ? SECTION_PADDING[section.headerSize] : CARD_PADDING[section.padding]) : "ax:pt-xl",
|
|
385
|
+
className,
|
|
386
|
+
)}
|
|
387
|
+
{...props}
|
|
388
|
+
/>
|
|
389
|
+
)
|
|
390
|
+
}
|
|
391
|
+
)
|
|
392
|
+
CardFooter.displayName = "CardFooter"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { CardsNav } from "./cards-nav"
|
|
4
|
+
|
|
5
|
+
const items = [
|
|
6
|
+
{ title: "Documentation", description: "Guides and API references.", href: "#docs" },
|
|
7
|
+
{ title: "Playground", description: "Try components live.", href: "#playground" },
|
|
8
|
+
{ title: "Brands", description: "Explore brand themes.", href: "#brands" },
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
const meta: Meta<typeof CardsNav> = {
|
|
12
|
+
title: "Components/CardsNav",
|
|
13
|
+
component: CardsNav,
|
|
14
|
+
tags: ["autodocs"],
|
|
15
|
+
args: { items },
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default meta
|
|
19
|
+
type Story = StoryObj<typeof CardsNav>
|
|
20
|
+
|
|
21
|
+
export const Default: Story = {
|
|
22
|
+
render: (args) => (
|
|
23
|
+
<div className="ax:grid ax:gap-md ax:sm:grid-cols-3 ax:max-w-3xl">
|
|
24
|
+
<CardsNav {...args} />
|
|
25
|
+
</div>
|
|
26
|
+
),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const WithOverline: Story = {
|
|
30
|
+
render: (args) => (
|
|
31
|
+
<div className="ax:grid ax:gap-md ax:sm:grid-cols-3 ax:max-w-3xl">
|
|
32
|
+
<CardsNav {...args} overline />
|
|
33
|
+
</div>
|
|
34
|
+
),
|
|
35
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { ArrowUpRight } from "lucide-react"
|
|
3
|
+
import { cn } from "../lib/cn"
|
|
4
|
+
|
|
5
|
+
export interface CardsNavItem {
|
|
6
|
+
/** Card heading. */
|
|
7
|
+
title: React.ReactNode
|
|
8
|
+
/** Optional supporting line under the title. */
|
|
9
|
+
description?: React.ReactNode
|
|
10
|
+
/** Destination passed to the link element (`as`). */
|
|
11
|
+
href: string
|
|
12
|
+
/** Custom overline content; falls back to a two-digit ordinal (01, 02, …)
|
|
13
|
+
* when the `overline` prop is on and this is omitted. */
|
|
14
|
+
overline?: React.ReactNode
|
|
15
|
+
/** Optional accent colour for the overline, as any CSS colour string (e.g.
|
|
16
|
+
* `"hsl(198 100% 44%)"`). Tints just the overline; leaves the rest neutral. */
|
|
17
|
+
accent?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type CardsNavSize = "sm" | "md" | "lg"
|
|
21
|
+
|
|
22
|
+
/** Card padding per size. */
|
|
23
|
+
const SIZE: Record<CardsNavSize, string> = {
|
|
24
|
+
sm: "ax:p-lg",
|
|
25
|
+
md: "ax:p-xl",
|
|
26
|
+
lg: "ax:p-2xl",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Title + description type scale per text size. */
|
|
30
|
+
const TEXT: Record<CardsNavSize, { title: string; desc: string }> = {
|
|
31
|
+
sm: { title: "ax:t-h5", desc: "ax:t-body-sm" },
|
|
32
|
+
md: { title: "ax:t-h4", desc: "ax:t-body-sm" },
|
|
33
|
+
lg: { title: "ax:t-h3", desc: "ax:t-body-md" },
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CardsNavProps {
|
|
37
|
+
items: CardsNavItem[]
|
|
38
|
+
/** Element rendered for each card. Default `"a"`; pass a router `Link` (e.g.
|
|
39
|
+
* Next.js) for client-side navigation. */
|
|
40
|
+
as?: React.ElementType
|
|
41
|
+
/** Card padding. Default "md". */
|
|
42
|
+
size?: CardsNavSize
|
|
43
|
+
/** Title + description type scale. Default "md". */
|
|
44
|
+
textSize?: CardsNavSize
|
|
45
|
+
/** Show the top overline row - a per-item `overline` or, failing that, a
|
|
46
|
+
* two-digit ordinal. Default false. */
|
|
47
|
+
overline?: boolean
|
|
48
|
+
/** Open links in a new tab (adds `target="_blank"` + safe `rel`). Use for
|
|
49
|
+
* external destinations. Default false. */
|
|
50
|
+
newTab?: boolean
|
|
51
|
+
/** Extra classes applied to each card. */
|
|
52
|
+
className?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* CardsNav - a set of navigational cards, each a title, optional supporting
|
|
57
|
+
* line and a trailing up-right arrow that leans on hover. Renders the cards as
|
|
58
|
+
* a fragment (no wrapper): place them in your own grid or flex container so the
|
|
59
|
+
* column count and gap stay with the layout.
|
|
60
|
+
*
|
|
61
|
+
* Framework-agnostic: cards render as `<a>` by default; pass `as={Link}` to use
|
|
62
|
+
* a router link. Turn on `overline` for a numbered / eyebrow'd set.
|
|
63
|
+
*/
|
|
64
|
+
export function CardsNav({ items, as, size = "md", textSize = "md", overline = false, newTab = false, className }: CardsNavProps) {
|
|
65
|
+
const Comp = as ?? "a"
|
|
66
|
+
const text = TEXT[textSize]
|
|
67
|
+
const newTabProps = newTab ? { target: "_blank", rel: "noopener noreferrer" } : {}
|
|
68
|
+
return (
|
|
69
|
+
<>
|
|
70
|
+
{items.map((item, i) => {
|
|
71
|
+
// The arrow shares a row with whichever element leads the card (the
|
|
72
|
+
// overline when it's shown, otherwise the title) rather than floating
|
|
73
|
+
// alone in a top row.
|
|
74
|
+
const arrow = (
|
|
75
|
+
<ArrowUpRight className="ax:size-4 ax:shrink-0 ax:text-neutral-weaker ax:transition-[color,transform] ax:group-hover:-translate-y-0.5 ax:group-hover:translate-x-0.5 ax:group-hover:text-primary ax:rtl:-scale-x-100" />
|
|
76
|
+
)
|
|
77
|
+
return (
|
|
78
|
+
<Comp
|
|
79
|
+
key={i}
|
|
80
|
+
href={item.href}
|
|
81
|
+
{...newTabProps}
|
|
82
|
+
className={cn(
|
|
83
|
+
"ax:group ax:flex ax:flex-col ax:rounded-sm ax:border ax:border-border ax:bg-card ax:transition-colors ax:dark:bg-card/50 ax:hover:border-primary ax:hover:bg-card",
|
|
84
|
+
SIZE[size],
|
|
85
|
+
className,
|
|
86
|
+
)}
|
|
87
|
+
>
|
|
88
|
+
{overline ? (
|
|
89
|
+
<>
|
|
90
|
+
<div className="ax:flex ax:items-center ax:justify-between ax:gap-md">
|
|
91
|
+
<span
|
|
92
|
+
className="ax:t-eyebrow-small ax:tabular-nums ax:text-muted-foreground"
|
|
93
|
+
style={item.accent ? { color: item.accent } : undefined}
|
|
94
|
+
>
|
|
95
|
+
{item.overline ?? String(i + 1).padStart(2, "0")}
|
|
96
|
+
</span>
|
|
97
|
+
{arrow}
|
|
98
|
+
</div>
|
|
99
|
+
<span className={cn("ax:mt-xs ax:text-foreground ax:transition-colors ax:group-hover:text-primary-foreground", text.title)}>{item.title}</span>
|
|
100
|
+
</>
|
|
101
|
+
) : (
|
|
102
|
+
<div className="ax:flex ax:items-start ax:justify-between ax:gap-md">
|
|
103
|
+
<span className={cn("ax:text-foreground ax:transition-colors ax:group-hover:text-primary-foreground", text.title)}>{item.title}</span>
|
|
104
|
+
{arrow}
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
{item.description != null && (
|
|
108
|
+
<span className={cn("ax:mt-2xs ax:max-w-prose ax:text-muted-foreground", text.desc)}>{item.description}</span>
|
|
109
|
+
)}
|
|
110
|
+
</Comp>
|
|
111
|
+
)
|
|
112
|
+
})}
|
|
113
|
+
</>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { Calculator, Lightbulb, Search } from "lucide-react"
|
|
4
|
+
import {
|
|
5
|
+
ChainOfThought, ChainOfThoughtContent, ChainOfThoughtItem, ChainOfThoughtStep, ChainOfThoughtTrigger,
|
|
6
|
+
} from "./chain-of-thought"
|
|
7
|
+
|
|
8
|
+
const meta: Meta<typeof ChainOfThought> = {
|
|
9
|
+
title: "Components/ChainOfThought",
|
|
10
|
+
component: ChainOfThought,
|
|
11
|
+
tags: ["autodocs"],
|
|
12
|
+
decorators: [(Story) => <div style={{ maxWidth: 560 }}><Story /></div>],
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default meta
|
|
16
|
+
type Story = StoryObj<typeof ChainOfThought>
|
|
17
|
+
|
|
18
|
+
export const Basic: Story = {
|
|
19
|
+
render: () => (
|
|
20
|
+
<ChainOfThought>
|
|
21
|
+
<ChainOfThoughtStep defaultOpen>
|
|
22
|
+
<ChainOfThoughtTrigger>Understanding the request</ChainOfThoughtTrigger>
|
|
23
|
+
<ChainOfThoughtContent>
|
|
24
|
+
<ChainOfThoughtItem>Townhouse on Yas Island, under AED 5M, with a private garden.</ChainOfThoughtItem>
|
|
25
|
+
</ChainOfThoughtContent>
|
|
26
|
+
</ChainOfThoughtStep>
|
|
27
|
+
<ChainOfThoughtStep>
|
|
28
|
+
<ChainOfThoughtTrigger>Searching listings</ChainOfThoughtTrigger>
|
|
29
|
+
<ChainOfThoughtContent>
|
|
30
|
+
<ChainOfThoughtItem>Found 42 townhouses in Yas Acres and Noya.</ChainOfThoughtItem>
|
|
31
|
+
<ChainOfThoughtItem>11 have a private garden.</ChainOfThoughtItem>
|
|
32
|
+
</ChainOfThoughtContent>
|
|
33
|
+
</ChainOfThoughtStep>
|
|
34
|
+
<ChainOfThoughtStep>
|
|
35
|
+
<ChainOfThoughtTrigger>Comparing prices</ChainOfThoughtTrigger>
|
|
36
|
+
<ChainOfThoughtContent>
|
|
37
|
+
<ChainOfThoughtItem>Lowest asking price is AED 4.2M in Yas Acres.</ChainOfThoughtItem>
|
|
38
|
+
</ChainOfThoughtContent>
|
|
39
|
+
</ChainOfThoughtStep>
|
|
40
|
+
</ChainOfThought>
|
|
41
|
+
),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const WithIcons: Story = {
|
|
45
|
+
render: () => (
|
|
46
|
+
<ChainOfThought>
|
|
47
|
+
<ChainOfThoughtStep>
|
|
48
|
+
<ChainOfThoughtTrigger icon={<Search />}>Searched 128 listings on Yas Island</ChainOfThoughtTrigger>
|
|
49
|
+
<ChainOfThoughtContent>
|
|
50
|
+
<ChainOfThoughtItem>42 townhouses under AED 5M, 11 with a private garden.</ChainOfThoughtItem>
|
|
51
|
+
</ChainOfThoughtContent>
|
|
52
|
+
</ChainOfThoughtStep>
|
|
53
|
+
<ChainOfThoughtStep>
|
|
54
|
+
<ChainOfThoughtTrigger icon={<Calculator />}>Compared registered sales</ChainOfThoughtTrigger>
|
|
55
|
+
<ChainOfThoughtContent>
|
|
56
|
+
<ChainOfThoughtItem>Median sale price in Yas Acres is AED 4.6M over the last 12 months.</ChainOfThoughtItem>
|
|
57
|
+
</ChainOfThoughtContent>
|
|
58
|
+
</ChainOfThoughtStep>
|
|
59
|
+
<ChainOfThoughtStep>
|
|
60
|
+
<ChainOfThoughtTrigger icon={<Lightbulb />}>Drew a conclusion</ChainOfThoughtTrigger>
|
|
61
|
+
<ChainOfThoughtContent>
|
|
62
|
+
<ChainOfThoughtItem>The AED 4.2M listing is priced below recent sales.</ChainOfThoughtItem>
|
|
63
|
+
</ChainOfThoughtContent>
|
|
64
|
+
</ChainOfThoughtStep>
|
|
65
|
+
</ChainOfThought>
|
|
66
|
+
),
|
|
67
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
import { renderToStaticMarkup } from "react-dom/server"
|
|
4
|
+
import {
|
|
5
|
+
ChainOfThought, ChainOfThoughtContent, ChainOfThoughtItem, ChainOfThoughtStep, ChainOfThoughtTrigger,
|
|
6
|
+
} from "./chain-of-thought"
|
|
7
|
+
|
|
8
|
+
function step(props: React.ComponentProps<typeof ChainOfThoughtStep>, icon?: React.ReactNode) {
|
|
9
|
+
return (
|
|
10
|
+
<ChainOfThoughtStep {...props}>
|
|
11
|
+
<ChainOfThoughtTrigger icon={icon}>Searching listings</ChainOfThoughtTrigger>
|
|
12
|
+
<ChainOfThoughtContent>
|
|
13
|
+
<ChainOfThoughtItem>Found 42 townhouses</ChainOfThoughtItem>
|
|
14
|
+
</ChainOfThoughtContent>
|
|
15
|
+
</ChainOfThoughtStep>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("ChainOfThought", () => {
|
|
20
|
+
it("keeps a step closed by default", () => {
|
|
21
|
+
const html = renderToStaticMarkup(<ChainOfThought>{step({})}</ChainOfThought>)
|
|
22
|
+
expect(html).toContain('aria-expanded="false"')
|
|
23
|
+
expect(html).not.toContain("Found 42 townhouses")
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("opens with defaultOpen and points the trigger at its content", () => {
|
|
27
|
+
const html = renderToStaticMarkup(<ChainOfThought>{step({ defaultOpen: true })}</ChainOfThought>)
|
|
28
|
+
expect(html).toContain('aria-expanded="true"')
|
|
29
|
+
expect(html).toContain("Found 42 townhouses")
|
|
30
|
+
const controls = html.match(/aria-controls="([^"]+)"/)?.[1]
|
|
31
|
+
expect(controls).toBeTruthy()
|
|
32
|
+
expect(html).toContain(`id="${controls}"`)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("lets a controlled open win over defaultOpen", () => {
|
|
36
|
+
const html = renderToStaticMarkup(<ChainOfThought>{step({ open: false, defaultOpen: true })}</ChainOfThought>)
|
|
37
|
+
expect(html).toContain('aria-expanded="false"')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("shows a trailing chevron only when there is no icon to swap", () => {
|
|
41
|
+
const chevrons = (html: string) => html.match(/lucide-chevron-down/g)?.length ?? 0
|
|
42
|
+
expect(chevrons(renderToStaticMarkup(step({})))).toBe(1)
|
|
43
|
+
expect(chevrons(renderToStaticMarkup(step({}, <svg />)))).toBe(1)
|
|
44
|
+
expect(renderToStaticMarkup(step({}, <svg />))).toContain("ax:absolute")
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it("throws when a trigger is used outside a step", () => {
|
|
48
|
+
expect(() => renderToStaticMarkup(<ChainOfThoughtTrigger>Orphan</ChainOfThoughtTrigger>)).toThrow(/ChainOfThoughtStep/)
|
|
49
|
+
})
|
|
50
|
+
})
|