@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,284 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { TrendingDown, TrendingUp } from "lucide-react"
|
|
5
|
+
import { Card, CardContent } from "./card"
|
|
6
|
+
import { Skeleton } from "./skeleton"
|
|
7
|
+
import { cn } from "../lib/cn"
|
|
8
|
+
|
|
9
|
+
export type KpiCardSize = "mini" | "sm" | "md" | "lg"
|
|
10
|
+
|
|
11
|
+
// ─── Group context ────────────────────────────────────────────────────────────
|
|
12
|
+
//
|
|
13
|
+
// `KpiCardGroup compact` merges its children into a single visual block:
|
|
14
|
+
// the cards lose their individual rounding/shadow and overlap their borders
|
|
15
|
+
// (`-mt-px` on mobile, `-ms-px` on desktop). Children read this via context
|
|
16
|
+
// so the parent grouping is fully declarative; consumers don't need to thread
|
|
17
|
+
// per-card props.
|
|
18
|
+
|
|
19
|
+
interface KpiCardGroupContextValue {
|
|
20
|
+
compact: boolean
|
|
21
|
+
columns?: 2 | 3 | 4
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const KpiCardGroupContext = React.createContext<KpiCardGroupContextValue>({ compact: false })
|
|
25
|
+
|
|
26
|
+
export interface KpiCardGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
27
|
+
/** Merge children into a borderless single-block layout. */
|
|
28
|
+
compact?: boolean
|
|
29
|
+
/** Lay the cards out in a fixed N-column grid instead of a single flex row.
|
|
30
|
+
* With `compact`, the grid renders as a seamless bordered "table" - 1px
|
|
31
|
+
* hairline dividers between cells inside a rounded outer frame. Give a card
|
|
32
|
+
* `sm:col-span-*` to make a row span fewer, wider columns (e.g. a 4-column
|
|
33
|
+
* grid whose second row is two `col-span-2` cells). */
|
|
34
|
+
columns?: 2 | 3 | 4
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const GRID_COLS: Record<2 | 3 | 4, string> = {
|
|
38
|
+
2: "ax:sm:grid-cols-2",
|
|
39
|
+
3: "ax:sm:grid-cols-3",
|
|
40
|
+
4: "ax:sm:grid-cols-4",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const KpiCardGroup = React.forwardRef<HTMLDivElement, KpiCardGroupProps>(
|
|
44
|
+
({ compact = false, columns, className, children, ...props }, ref) => (
|
|
45
|
+
<KpiCardGroupContext.Provider value={{ compact, columns }}>
|
|
46
|
+
<div
|
|
47
|
+
ref={ref}
|
|
48
|
+
className={cn(
|
|
49
|
+
columns
|
|
50
|
+
? compact
|
|
51
|
+
// Seamless bordered "table": the container paints the seams:
|
|
52
|
+
// `bg-border` shows through the `gap-px` as 1px dividers, clipped
|
|
53
|
+
// to a rounded outer frame. Cards inside drop their own chrome.
|
|
54
|
+
? cn("ax:grid ax:grid-cols-1 ax:gap-px ax:overflow-hidden ax:rounded-md ax:border ax:border-border ax:bg-border", GRID_COLS[columns])
|
|
55
|
+
// Non-compact: a plain N-column grid of separate cards.
|
|
56
|
+
: cn("ax:grid ax:gap-md ax:grid-cols-1", GRID_COLS[columns])
|
|
57
|
+
: compact
|
|
58
|
+
// Stacks vertically on mobile, becomes a horizontal row at `sm` and up.
|
|
59
|
+
? "ax:flex ax:flex-col ax:sm:flex-row"
|
|
60
|
+
: "ax:grid ax:gap-md ax:sm:grid-cols-2 ax:lg:grid-cols-4",
|
|
61
|
+
className,
|
|
62
|
+
)}
|
|
63
|
+
{...props}
|
|
64
|
+
>
|
|
65
|
+
{children}
|
|
66
|
+
</div>
|
|
67
|
+
</KpiCardGroupContext.Provider>
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
KpiCardGroup.displayName = "KpiCardGroup"
|
|
71
|
+
|
|
72
|
+
// ─── KpiCard ──────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
export interface KpiCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
75
|
+
/** Overline label sitting above the value. */
|
|
76
|
+
title: string
|
|
77
|
+
/** The metric itself. Strings are useful for pre-formatted values
|
|
78
|
+
* ("76 %", "1.2k"). Numbers get a `0` fallback for the empty state. */
|
|
79
|
+
value: string | number
|
|
80
|
+
/** Optional period-over-period delta as a percentage. Positive → success
|
|
81
|
+
* pill with an up-arrow; negative → destructive pill with a down-arrow.
|
|
82
|
+
* `0` and `undefined` both render no delta - zero isn't a trend. */
|
|
83
|
+
change?: number
|
|
84
|
+
/** Optional suffix appended to the value (e.g. "%", "h"). Rendered in a
|
|
85
|
+
* smaller muted style so it doesn't compete with the number. */
|
|
86
|
+
suffix?: string
|
|
87
|
+
/** Optional topic icon - rendered muted, top-end of the card. */
|
|
88
|
+
icon?: React.ReactNode
|
|
89
|
+
/** Optional context line under the value (e.g. "vs last 30 days"). */
|
|
90
|
+
description?: React.ReactNode
|
|
91
|
+
/** Visual density. `md` is the default. */
|
|
92
|
+
size?: KpiCardSize
|
|
93
|
+
/** While true, swaps the title/value/delta for skeleton placeholders. */
|
|
94
|
+
loading?: boolean
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Size tokens ──────────────────────────────────────────────────────────────
|
|
98
|
+
//
|
|
99
|
+
// Trend value uses raw `text-* font-bold` (not `t-h*`) on purpose: the
|
|
100
|
+
// typography tokens hard-code `text-foreground-strong`, which would override
|
|
101
|
+
// the semantic `text-success-strong` / `text-destructive-strong` color on the trend pill.
|
|
102
|
+
|
|
103
|
+
const CARD_PADDING: Record<KpiCardSize, string> = {
|
|
104
|
+
mini: "ax:p-md", // 16px (same footprint as sm; mini shrinks the value, not the card)
|
|
105
|
+
sm: "ax:p-md", // 16px
|
|
106
|
+
md: "ax:p-lg", // 20px
|
|
107
|
+
lg: "ax:p-xl", // 24px
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const VALUE_CLASS: Record<KpiCardSize, string> = {
|
|
111
|
+
mini: "ax:t-h2", // text-lg (18px) - a compact metric
|
|
112
|
+
sm: "ax:t-h1", // text-2xl (24px)
|
|
113
|
+
md: "ax:t-display-md", // text-3xl (30px)
|
|
114
|
+
lg: "ax:t-display-lg", // text-4xl (36px)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Vertical gap between the label, value, and description. `mini` halves the
|
|
118
|
+
// default (gap-sm = 12px → 6px) to sit its smaller value tight to the label.
|
|
119
|
+
const CONTENT_GAP: Record<KpiCardSize, string> = {
|
|
120
|
+
mini: "ax:gap-1.5", // 6px
|
|
121
|
+
sm: "ax:gap-0", // 12px
|
|
122
|
+
md: "ax:gap-0",
|
|
123
|
+
lg: "ax:gap-0",
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const SUFFIX_CLASS: Record<KpiCardSize, string> = {
|
|
127
|
+
mini: "ax:t-caption",
|
|
128
|
+
sm: "ax:t-body-sm",
|
|
129
|
+
md: "ax:t-h5",
|
|
130
|
+
lg: "ax:t-h4",
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const TREND_CLASS: Record<KpiCardSize, string> = {
|
|
134
|
+
mini: "ax:text-xs ax:font-semibold",
|
|
135
|
+
sm: "ax:text-sm ax:font-semibold",
|
|
136
|
+
md: "ax:text-base ax:font-semibold",
|
|
137
|
+
lg: "ax:text-lg ax:font-semibold",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const TREND_ICON_CLASS: Record<KpiCardSize, string> = {
|
|
141
|
+
mini: "ax:size-3",
|
|
142
|
+
sm: "ax:size-3.5",
|
|
143
|
+
md: "ax:size-4",
|
|
144
|
+
lg: "ax:size-5",
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const ICON_CLASS: Record<KpiCardSize, string> = {
|
|
148
|
+
mini: "ax:size-3 ax:[&>svg]:size-3",
|
|
149
|
+
sm: "ax:size-3.5 ax:[&>svg]:size-3.5",
|
|
150
|
+
md: "ax:size-4 ax:[&>svg]:size-4",
|
|
151
|
+
lg: "ax:size-5 ax:[&>svg]:size-5",
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const SKELETON_VALUE_HEIGHT: Record<KpiCardSize, string> = {
|
|
155
|
+
mini: "ax:h-5",
|
|
156
|
+
sm: "ax:h-6",
|
|
157
|
+
md: "ax:h-7",
|
|
158
|
+
lg: "ax:h-9",
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Compact-group layout: cards round only their outer edges and overlap
|
|
162
|
+
// borders by 1px so the seam between two cards renders as a single line
|
|
163
|
+
// rather than a doubled-up border.
|
|
164
|
+
const COMPACT_CARD_LAYOUT = [
|
|
165
|
+
"ax:flex-1 ax:min-w-0",
|
|
166
|
+
// Drop shadow + per-card rounding when inside a compact group. `bg-card`
|
|
167
|
+
// restores the solid fill that the flat `level0` elevation clears, so the
|
|
168
|
+
// merged block keeps its surface while staying shadowless.
|
|
169
|
+
"ax:rounded-none ax:shadow-none ax:bg-card",
|
|
170
|
+
// Mobile (stacked): round top of first card, bottom of last; overlap rows.
|
|
171
|
+
"ax:first:rounded-t-md ax:last:rounded-b-md",
|
|
172
|
+
"ax:[&:not(:first-child)]:-mt-px",
|
|
173
|
+
// Desktop (row): round inline-start of first, inline-end of last;
|
|
174
|
+
// overlap columns instead of rows.
|
|
175
|
+
"ax:sm:first:rounded-t-none ax:sm:last:rounded-b-none",
|
|
176
|
+
"ax:sm:first:rounded-s-md ax:sm:last:rounded-e-md",
|
|
177
|
+
"ax:sm:[&:not(:first-child)]:mt-0 ax:sm:[&:not(:first-child)]:-ms-px",
|
|
178
|
+
].join(" ")
|
|
179
|
+
|
|
180
|
+
// Grid-table layout (compact + columns): cells carry no chrome of their own:
|
|
181
|
+
// the group's rounded frame and gap-px hairlines draw every edge, so a card is
|
|
182
|
+
// just a filled rectangle.
|
|
183
|
+
const GRID_TABLE_CARD_LAYOUT = "ax:min-w-0 ax:border-0 ax:rounded-none ax:shadow-none ax:bg-card"
|
|
184
|
+
|
|
185
|
+
/** Single-metric card with optional trend delta. A common dashboard
|
|
186
|
+
* primitive: pairs a big number with a label and (optionally) a
|
|
187
|
+
* period-over-period change. The empty state ("No data yet") activates
|
|
188
|
+
* only when the numeric value is `0` and no `change` is provided. */
|
|
189
|
+
export const KpiCard = React.forwardRef<HTMLDivElement, KpiCardProps>(
|
|
190
|
+
(
|
|
191
|
+
{
|
|
192
|
+
title,
|
|
193
|
+
value,
|
|
194
|
+
change,
|
|
195
|
+
suffix,
|
|
196
|
+
icon,
|
|
197
|
+
description,
|
|
198
|
+
size = "md",
|
|
199
|
+
loading,
|
|
200
|
+
className,
|
|
201
|
+
...props
|
|
202
|
+
},
|
|
203
|
+
ref,
|
|
204
|
+
) => {
|
|
205
|
+
const { compact, columns } = React.useContext(KpiCardGroupContext)
|
|
206
|
+
const gridTable = compact && columns != null
|
|
207
|
+
|
|
208
|
+
const numericValue = typeof value === "number" ? value : Number(value)
|
|
209
|
+
const isEmpty = numericValue === 0
|
|
210
|
+
const hasChange = change !== undefined && change !== 0 && !isEmpty
|
|
211
|
+
const isPositive = (change ?? 0) > 0
|
|
212
|
+
|
|
213
|
+
// When an onClick is provided the card becomes interactive: resting
|
|
214
|
+
// shadow-xs lifts to shadow-md on hover, cursor flips to pointer.
|
|
215
|
+
// Compact cards are excluded: they have no individual shadow.
|
|
216
|
+
const isClickable = !compact && typeof props.onClick === "function"
|
|
217
|
+
|
|
218
|
+
const cardClasses = cn(
|
|
219
|
+
CARD_PADDING[size],
|
|
220
|
+
gridTable ? GRID_TABLE_CARD_LAYOUT : compact && COMPACT_CARD_LAYOUT,
|
|
221
|
+
className,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
if (loading) {
|
|
225
|
+
return (
|
|
226
|
+
<Card ref={ref} elevation={compact ? "level0" : "level2"} interactive={isClickable} className={cardClasses} {...props}>
|
|
227
|
+
<CardContent className={cn("ax:p-0 ax:flex ax:flex-col", CONTENT_GAP[size])}>
|
|
228
|
+
<Skeleton className="ax:h-3 ax:w-24" />
|
|
229
|
+
<div className="ax:flex ax:items-end ax:justify-between ax:gap-sm">
|
|
230
|
+
<Skeleton className={cn(SKELETON_VALUE_HEIGHT[size], "ax:w-32")} />
|
|
231
|
+
<Skeleton className="ax:h-4 ax:w-12" />
|
|
232
|
+
</div>
|
|
233
|
+
</CardContent>
|
|
234
|
+
</Card>
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<Card ref={ref} elevation={compact ? "level0" : "level2"} interactive={isClickable} className={cardClasses} {...props}>
|
|
240
|
+
<CardContent className={cn("ax:p-0 ax:flex ax:flex-col", CONTENT_GAP[size])}>
|
|
241
|
+
<div className="ax:flex ax:items-start ax:justify-between ax:gap-sm">
|
|
242
|
+
<p className="ax:t-overline ax:text-muted-foreground">{title}</p>
|
|
243
|
+
{icon && (
|
|
244
|
+
<span className={cn("ax:shrink-0 ax:text-muted-foreground ax:[&>svg]:shrink-0", ICON_CLASS[size])}>
|
|
245
|
+
{icon}
|
|
246
|
+
</span>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<div className={cn("ax:flex ax:items-end ax:gap-sm", compact ? "ax:justify-start" : "ax:justify-between")}>
|
|
251
|
+
<p className={cn(VALUE_CLASS[size], "ax:font-semibold ax:text-foreground")}>
|
|
252
|
+
{isEmpty ? "0" : value}
|
|
253
|
+
{suffix && <span className={cn(SUFFIX_CLASS[size], "ax:text-muted-foreground ax:ms-1")}>{suffix}</span>}
|
|
254
|
+
</p>
|
|
255
|
+
|
|
256
|
+
{hasChange ? (
|
|
257
|
+
<div
|
|
258
|
+
className={cn(
|
|
259
|
+
"ax:flex ax:items-center ax:gap-1",
|
|
260
|
+
TREND_CLASS[size],
|
|
261
|
+
isPositive ? "ax:text-success-foreground" : "ax:text-destructive-foreground",
|
|
262
|
+
)}
|
|
263
|
+
>
|
|
264
|
+
{isPositive ? (
|
|
265
|
+
<TrendingUp className={cn(TREND_ICON_CLASS[size], "ax:shrink-0")} />
|
|
266
|
+
) : (
|
|
267
|
+
<TrendingDown className={cn(TREND_ICON_CLASS[size], "ax:shrink-0")} />
|
|
268
|
+
)}
|
|
269
|
+
<span>{Math.abs(change ?? 0)}%</span>
|
|
270
|
+
</div>
|
|
271
|
+
) : isEmpty ? (
|
|
272
|
+
<span className="ax:t-caption ax:text-muted-foreground">No data yet</span>
|
|
273
|
+
) : null}
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
{description && !isEmpty && (
|
|
277
|
+
<p className="ax:t-caption ax:text-muted-foreground">{description}</p>
|
|
278
|
+
)}
|
|
279
|
+
</CardContent>
|
|
280
|
+
</Card>
|
|
281
|
+
)
|
|
282
|
+
},
|
|
283
|
+
)
|
|
284
|
+
KpiCard.displayName = "KpiCard"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { ListNav, ListNavItem } from "./list-nav"
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof ListNav> = {
|
|
6
|
+
title: "Components/ListNav",
|
|
7
|
+
component: ListNav,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof ListNav>
|
|
13
|
+
|
|
14
|
+
export const Default: Story = {
|
|
15
|
+
render: () => (
|
|
16
|
+
<ListNav title="Settings">
|
|
17
|
+
<ListNavItem active>General</ListNavItem>
|
|
18
|
+
<ListNavItem>Notifications</ListNavItem>
|
|
19
|
+
<ListNavItem>Billing</ListNavItem>
|
|
20
|
+
</ListNav>
|
|
21
|
+
),
|
|
22
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Slot, Slottable } from "@radix-ui/react-slot"
|
|
5
|
+
import { cn } from "../lib/cn"
|
|
6
|
+
|
|
7
|
+
export type ListNavSize = "base" | "lg"
|
|
8
|
+
|
|
9
|
+
// Size is shared from ListNav to its items via context, so `<ListNav size="lg">`
|
|
10
|
+
// sets the density for the whole list; a single item can still override it.
|
|
11
|
+
interface ListNavContextValue {
|
|
12
|
+
size: ListNavSize
|
|
13
|
+
}
|
|
14
|
+
const ListNavContext = React.createContext<ListNavContextValue>({ size: "base" })
|
|
15
|
+
|
|
16
|
+
// Per-size item padding + type. `lg` widens the row into a comfortable ~44px
|
|
17
|
+
// tap target for touchscreens; `base` stays compact for dense desktop lists.
|
|
18
|
+
const ITEM_SIZE: Record<ListNavSize, string> = {
|
|
19
|
+
base: "ax:px-xs ax:py-2xs ax:t-label-sm",
|
|
20
|
+
lg: "ax:px-sm ax:py-sm ax:t-label-md",
|
|
21
|
+
}
|
|
22
|
+
// Leading icon (and the inset placeholder) column width per size.
|
|
23
|
+
const ICON_BOX: Record<ListNavSize, string> = {
|
|
24
|
+
base: "ax:size-4",
|
|
25
|
+
lg: "ax:size-5",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── ListNav ──────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
export interface ListNavProps extends Omit<React.HTMLAttributes<HTMLElement>, "title"> {
|
|
31
|
+
/** Optional eyebrow-style title rendered above the list (uses `t-overline`). */
|
|
32
|
+
title?: React.ReactNode
|
|
33
|
+
/** Row density. `base` (default) is compact; `lg` adds padding for a larger,
|
|
34
|
+
* touch-friendly tap target. Cascades to all items via context. */
|
|
35
|
+
size?: ListNavSize
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* ListNav - vertical list of selectable items with an optional eyebrow title.
|
|
40
|
+
*
|
|
41
|
+
* Use for in-page section jumpers, dense filter lists, or any sidebar-adjacent
|
|
42
|
+
* nav where items don't need an icon column. For app-shell navigation with
|
|
43
|
+
* brand, collapse, and tooltips, reach for `Sidebar` instead.
|
|
44
|
+
*
|
|
45
|
+
* Compose multiple `ListNav`s vertically when grouping is needed: each owns
|
|
46
|
+
* its own title.
|
|
47
|
+
*/
|
|
48
|
+
export const ListNav = React.forwardRef<HTMLElement, ListNavProps>(
|
|
49
|
+
({ className, title, size = "base", children, ...props }, ref) => (
|
|
50
|
+
<ListNavContext.Provider value={{ size }}>
|
|
51
|
+
<nav
|
|
52
|
+
ref={ref}
|
|
53
|
+
className={cn("ax:flex ax:flex-col", className)}
|
|
54
|
+
{...props}
|
|
55
|
+
>
|
|
56
|
+
{title && (
|
|
57
|
+
// Title x-padding tracks the item padding so it stays flush with rows.
|
|
58
|
+
<p className={cn("ax:t-overline ax:text-foreground-strong ax:pt-sm ax:pb-sm", size === "lg" ? "ax:px-sm" : "ax:px-xs")}>
|
|
59
|
+
{title}
|
|
60
|
+
</p>
|
|
61
|
+
)}
|
|
62
|
+
<div className="ax:flex ax:flex-col">{children}</div>
|
|
63
|
+
</nav>
|
|
64
|
+
</ListNavContext.Provider>
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
ListNav.displayName = "ListNav"
|
|
68
|
+
|
|
69
|
+
// ─── ListNavItem ──────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
export interface ListNavItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
72
|
+
active?: boolean
|
|
73
|
+
/** Optional leading icon. */
|
|
74
|
+
icon?: React.ReactNode
|
|
75
|
+
/**
|
|
76
|
+
* Reserves the leading icon column even when no icon is provided. Use when
|
|
77
|
+
* mixing items with and without icons in the same list, so labels stay
|
|
78
|
+
* aligned. Has no effect if `icon` is set.
|
|
79
|
+
*/
|
|
80
|
+
inset?: boolean
|
|
81
|
+
/** Trailing slot - typically a Badge, count, or shortcut hint. */
|
|
82
|
+
trailing?: React.ReactNode
|
|
83
|
+
/**
|
|
84
|
+
* Render as the child element (e.g. a Next.js `<Link>`). The child receives
|
|
85
|
+
* the className, ref, and event props so it behaves as a list-nav row while
|
|
86
|
+
* keeping its own navigation behaviour.
|
|
87
|
+
*/
|
|
88
|
+
asChild?: boolean
|
|
89
|
+
/** Overrides the row density inherited from `ListNav` for this item. */
|
|
90
|
+
size?: ListNavSize
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const ListNavItem = React.forwardRef<HTMLButtonElement, ListNavItemProps>(
|
|
94
|
+
({ className, active, icon, inset, trailing, asChild, type, size: sizeProp, children, ...props }, ref) => {
|
|
95
|
+
const { size: ctxSize } = React.useContext(ListNavContext)
|
|
96
|
+
const size = sizeProp ?? ctxSize
|
|
97
|
+
const Comp = asChild ? Slot : "button"
|
|
98
|
+
// `type="button"` only makes sense on a real <button>; if `asChild` wraps
|
|
99
|
+
// an <a> or <Link>, applying it would emit invalid HTML.
|
|
100
|
+
const buttonOnly = asChild ? {} : { type: type ?? "button" }
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<Comp
|
|
104
|
+
ref={ref as React.Ref<HTMLButtonElement>}
|
|
105
|
+
aria-current={active ? "page" : undefined}
|
|
106
|
+
className={cn(
|
|
107
|
+
"ax:w-full ax:text-start ax:flex ax:items-center ax:gap-sm ax:rounded-(--input-radius) ax:cursor-pointer",
|
|
108
|
+
ITEM_SIZE[size],
|
|
109
|
+
"ax:transition-colors",
|
|
110
|
+
"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",
|
|
111
|
+
active
|
|
112
|
+
// `bg-primary-weakest` auto-inverts to a deep tint in dark; keep it
|
|
113
|
+
// translucent there (/25). `text-primary-foreground` flips near-white in
|
|
114
|
+
// dark (washed-out), so use a bright branded sage (`-strong`) instead.
|
|
115
|
+
? "ax:bg-primary-muted ax:text-primary-foreground ax:dark:text-primary-strong ax:font-medium"
|
|
116
|
+
: "ax:text-muted-foreground ax:hover:text-foreground ax:hover:bg-muted",
|
|
117
|
+
className,
|
|
118
|
+
)}
|
|
119
|
+
{...buttonOnly}
|
|
120
|
+
{...props}
|
|
121
|
+
>
|
|
122
|
+
{icon ? (
|
|
123
|
+
<span className={cn("ax:shrink-0 ax:flex ax:items-center ax:justify-center ax:[&>svg]:size-full ax:[&>svg]:shrink-0", ICON_BOX[size])}>
|
|
124
|
+
{icon}
|
|
125
|
+
</span>
|
|
126
|
+
) : inset ? (
|
|
127
|
+
// Empty placeholder occupying the icon column so labels stay aligned
|
|
128
|
+
// with sibling rows that DO have an icon.
|
|
129
|
+
<span aria-hidden="true" className={cn("ax:shrink-0", ICON_BOX[size])} />
|
|
130
|
+
) : null}
|
|
131
|
+
{asChild ? (
|
|
132
|
+
// Slot needs exactly one slottable child: the consumer's element (e.g. an <a>)
|
|
133
|
+
// receives the icon and trailing siblings around its own content.
|
|
134
|
+
<Slottable>{children}</Slottable>
|
|
135
|
+
) : (
|
|
136
|
+
// leading-snug (not the row's t-label-sm leading-none): `truncate` clips
|
|
137
|
+
// with overflow-hidden, and a 1.0 line-height would shave descenders.
|
|
138
|
+
<span className="ax:flex-1 ax:truncate ax:leading-snug">{children}</span>
|
|
139
|
+
)}
|
|
140
|
+
{trailing && <span className="ax:shrink-0 ax:flex ax:items-center">{trailing}</span>}
|
|
141
|
+
</Comp>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
ListNavItem.displayName = "ListNavItem"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { LogoCloud } from "./logo-cloud"
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof LogoCloud> = {
|
|
6
|
+
title: "Components/LogoCloud",
|
|
7
|
+
component: LogoCloud,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
args: {
|
|
10
|
+
eyebrow: "Trusted by teams at",
|
|
11
|
+
items: [
|
|
12
|
+
{ name: "Acme", logo: <span className="ax:t-h5">Acme</span> },
|
|
13
|
+
{ name: "Globex", logo: <span className="ax:t-h5">Globex</span> },
|
|
14
|
+
{ name: "Initech", logo: <span className="ax:t-h5">Initech</span> },
|
|
15
|
+
{ name: "Umbrella", logo: <span className="ax:t-h5">Umbrella</span> },
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default meta
|
|
21
|
+
type Story = StoryObj<typeof LogoCloud>
|
|
22
|
+
|
|
23
|
+
export const Default: Story = {}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "../lib/cn"
|
|
3
|
+
|
|
4
|
+
export interface LogoCloudItem {
|
|
5
|
+
name: string
|
|
6
|
+
logo: React.ReactNode
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface LogoCloudProps extends React.HTMLAttributes<HTMLElement> {
|
|
10
|
+
eyebrow?: React.ReactNode
|
|
11
|
+
items: LogoCloudItem[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function LogoCloud({ className, eyebrow, items, ...props }: LogoCloudProps) {
|
|
15
|
+
return (
|
|
16
|
+
<section className={cn("ax:w-full ax:px-xl ax:md:px-3xl ax:py-4xl ax:md:py-5xl", className)} {...props}>
|
|
17
|
+
<div className="ax:mx-auto ax:max-w-420">
|
|
18
|
+
{eyebrow && (
|
|
19
|
+
<p className="ax:text-center ax:t-overline ax:text-muted-foreground ax:mb-2xl">
|
|
20
|
+
{eyebrow}
|
|
21
|
+
</p>
|
|
22
|
+
)}
|
|
23
|
+
<div
|
|
24
|
+
className={cn(
|
|
25
|
+
"ax:grid ax:grid-cols-2 ax:gap-x-2xl ax:gap-y-xl ax:items-center",
|
|
26
|
+
"ax:sm:grid-cols-3 ax:md:grid-cols-4 ax:lg:grid-cols-6"
|
|
27
|
+
)}
|
|
28
|
+
>
|
|
29
|
+
{items.map((item) => (
|
|
30
|
+
<div
|
|
31
|
+
key={item.name}
|
|
32
|
+
title={item.name}
|
|
33
|
+
className="ax:flex ax:items-center ax:justify-center ax:opacity-50 ax:grayscale ax:hover:opacity-80 ax:hover:grayscale-0 ax:transition-[opacity,filter] ax:transition-smooth"
|
|
34
|
+
>
|
|
35
|
+
{item.logo}
|
|
36
|
+
</div>
|
|
37
|
+
))}
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</section>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { MediaGallery, type MediaItem } from "./media-gallery"
|
|
4
|
+
|
|
5
|
+
const media: MediaItem[] = [
|
|
6
|
+
{ type: "image", src: "https://picsum.photos/800/600?random=1", label: "Property exterior" },
|
|
7
|
+
{ type: "image", src: "https://picsum.photos/800/600?random=2", label: "Living room" },
|
|
8
|
+
{ type: "image", src: "https://picsum.photos/800/600?random=3", label: "Kitchen" },
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
const meta: Meta<typeof MediaGallery> = {
|
|
12
|
+
title: "Components/MediaGallery",
|
|
13
|
+
component: MediaGallery,
|
|
14
|
+
tags: ["autodocs"],
|
|
15
|
+
args: {
|
|
16
|
+
title: "Gallery",
|
|
17
|
+
media,
|
|
18
|
+
thumbnailCount: 3,
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default meta
|
|
23
|
+
type Story = StoryObj<typeof MediaGallery>
|
|
24
|
+
|
|
25
|
+
export const Default: Story = {}
|