@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,204 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../lib/cn"
|
|
5
|
+
|
|
6
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export type BarGraphSize = "sm" | "md" | "lg"
|
|
9
|
+
export type BarGraphDirection = "horizontal" | "vertical"
|
|
10
|
+
|
|
11
|
+
export interface BarGraphItem {
|
|
12
|
+
/** Display label, e.g. `"Compliant"`, `"Q3"`. */
|
|
13
|
+
label: string
|
|
14
|
+
/** Absolute count shown beside the bar. The bar fill is `value / max`. */
|
|
15
|
+
value: number
|
|
16
|
+
/** Tailwind background utility, e.g. `"ax:bg-chart-1"`, `"ax:bg-success"`.
|
|
17
|
+
* The track renders the same colour at 15 % opacity, mirroring the
|
|
18
|
+
* Activity Rings idiom so bar charts and rings read as one system. */
|
|
19
|
+
color: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface BarGraphProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
23
|
+
/** One item per bar, rendered in order. */
|
|
24
|
+
bars: BarGraphItem[]
|
|
25
|
+
/** Ceiling value - each bar fills to `value / max`.
|
|
26
|
+
* Omit to let the tallest bar reach 100 % (relative comparison).
|
|
27
|
+
* Pass explicitly when bars are fractions of a known whole. */
|
|
28
|
+
max?: number
|
|
29
|
+
/** Growth direction. `horizontal` grows left → right (default);
|
|
30
|
+
* `vertical` grows bottom → top. */
|
|
31
|
+
direction?: BarGraphDirection
|
|
32
|
+
/** Visual density: drives bar thickness, chart height, and type scale. */
|
|
33
|
+
size?: BarGraphSize
|
|
34
|
+
/** Render the computed percentage beside / above each bar. Defaults to `true`. */
|
|
35
|
+
showPct?: boolean
|
|
36
|
+
/** Render the raw value count beside / above each bar. Defaults to `true`. */
|
|
37
|
+
showCount?: boolean
|
|
38
|
+
/** Horizontal-mode label alignment within its fixed-width column. `right`
|
|
39
|
+
* (default) sets a shared axis flush against the bars; `left` aligns with
|
|
40
|
+
* a left-aligned heading above the chart instead. No effect in `vertical`
|
|
41
|
+
* mode (labels there are centred under each bar). */
|
|
42
|
+
labelAlign?: "left" | "right"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Size tokens ─────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
// Horizontal bar track height - mirrors the Progress bar height scale
|
|
48
|
+
const H_TRACK_H: Record<BarGraphSize, string> = {
|
|
49
|
+
sm: "ax:h-1.5",
|
|
50
|
+
md: "ax:h-2.5",
|
|
51
|
+
lg: "ax:h-4",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Vertical chart area height in px (bar drawing area, excluding label/stat space)
|
|
55
|
+
const V_CHART_H: Record<BarGraphSize, number> = {
|
|
56
|
+
sm: 80,
|
|
57
|
+
md: 120,
|
|
58
|
+
lg: 160,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Label column width (horizontal) - right-aligned so all bars share one axis
|
|
62
|
+
const LABEL_W: Record<BarGraphSize, string> = {
|
|
63
|
+
sm: "ax:w-20",
|
|
64
|
+
md: "ax:w-28",
|
|
65
|
+
lg: "ax:w-32",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const LABEL_CLS: Record<BarGraphSize, string> = {
|
|
69
|
+
sm: "ax:t-label-2xs",
|
|
70
|
+
md: "ax:t-label-xs",
|
|
71
|
+
lg: "ax:t-label-xs",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const STAT_CLS: Record<BarGraphSize, string> = {
|
|
75
|
+
sm: "ax:t-micro",
|
|
76
|
+
md: "ax:t-caption",
|
|
77
|
+
lg: "ax:t-label-sm",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
export const BarGraph = React.forwardRef<HTMLDivElement, BarGraphProps>(
|
|
83
|
+
(
|
|
84
|
+
{
|
|
85
|
+
bars,
|
|
86
|
+
max,
|
|
87
|
+
direction = "horizontal",
|
|
88
|
+
size = "md",
|
|
89
|
+
showPct = true,
|
|
90
|
+
showCount = true,
|
|
91
|
+
labelAlign = "right",
|
|
92
|
+
className,
|
|
93
|
+
...props
|
|
94
|
+
},
|
|
95
|
+
ref,
|
|
96
|
+
) => {
|
|
97
|
+
const resolvedMax = max ?? Math.max(...bars.map((b) => b.value), 1)
|
|
98
|
+
|
|
99
|
+
// ── Vertical ─────────────────────────────────────────────────────────────
|
|
100
|
+
if (direction === "vertical") {
|
|
101
|
+
return (
|
|
102
|
+
<div
|
|
103
|
+
ref={ref}
|
|
104
|
+
className={cn("ax:flex ax:items-end ax:gap-sm", className)}
|
|
105
|
+
{...props}
|
|
106
|
+
>
|
|
107
|
+
{bars.map((bar, i) => {
|
|
108
|
+
const pct = Math.min(100, Math.max(0, (bar.value / resolvedMax) * 100))
|
|
109
|
+
return (
|
|
110
|
+
<div key={i} className="ax:flex ax:flex-col ax:items-center ax:gap-xs ax:flex-1 ax:min-w-0">
|
|
111
|
+
{/* Stats above bar - percentage + count */}
|
|
112
|
+
{(showPct || showCount) && (
|
|
113
|
+
<div className={cn(STAT_CLS[size], "ax:flex ax:flex-col ax:items-center ax:gap-2xs ax:tabular-nums")}>
|
|
114
|
+
{showPct && <span className="ax:text-foreground ax:font-medium">{Math.round(pct)}%</span>}
|
|
115
|
+
{showCount && <span className="ax:text-muted-foreground">{bar.value}</span>}
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
{/* Bar column: track behind fill, fill grows from bottom */}
|
|
120
|
+
<div
|
|
121
|
+
className="ax:relative ax:w-full ax:overflow-hidden ax:rounded-md"
|
|
122
|
+
style={{ height: V_CHART_H[size] }}
|
|
123
|
+
>
|
|
124
|
+
{/* Track - same hue at 15 % opacity, matches the ring idiom */}
|
|
125
|
+
<div className={cn("ax:absolute ax:inset-0", bar.color, "ax:opacity-[0.15]")} />
|
|
126
|
+
{/* Fill - animated height from bottom */}
|
|
127
|
+
<div
|
|
128
|
+
className={cn(
|
|
129
|
+
"ax:absolute ax:bottom-0 ax:left-0 ax:right-0 ax:rounded-md",
|
|
130
|
+
bar.color,
|
|
131
|
+
"ax:[transition:height_var(--motion-slow)_var(--motion-ease-out)]",
|
|
132
|
+
)}
|
|
133
|
+
style={{ height: `${pct}%` }}
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{/* Label below bar */}
|
|
138
|
+
<span
|
|
139
|
+
className={cn(LABEL_CLS[size], "ax:text-muted-foreground ax:text-center ax:w-full ax:truncate")}
|
|
140
|
+
>
|
|
141
|
+
{bar.label}
|
|
142
|
+
</span>
|
|
143
|
+
</div>
|
|
144
|
+
)
|
|
145
|
+
})}
|
|
146
|
+
</div>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Horizontal (default) ──────────────────────────────────────────────────
|
|
151
|
+
return (
|
|
152
|
+
<div
|
|
153
|
+
ref={ref}
|
|
154
|
+
className={cn("ax:flex ax:flex-col ax:gap-sm", className)}
|
|
155
|
+
{...props}
|
|
156
|
+
>
|
|
157
|
+
{bars.map((bar, i) => {
|
|
158
|
+
const pct = Math.min(100, Math.max(0, (bar.value / resolvedMax) * 100))
|
|
159
|
+
return (
|
|
160
|
+
<div key={i} className="ax:flex ax:items-center ax:gap-sm">
|
|
161
|
+
{/* Row label - fixed-width column, right-aligned for clean axis */}
|
|
162
|
+
<span
|
|
163
|
+
className={cn(
|
|
164
|
+
LABEL_CLS[size],
|
|
165
|
+
LABEL_W[size],
|
|
166
|
+
"ax:shrink-0 ax:text-muted-foreground",
|
|
167
|
+
labelAlign === "left" ? "ax:text-left" : "ax:text-right",
|
|
168
|
+
)}
|
|
169
|
+
>
|
|
170
|
+
{bar.label}
|
|
171
|
+
</span>
|
|
172
|
+
|
|
173
|
+
{/* Track + fill */}
|
|
174
|
+
<div className={cn("ax:relative ax:flex-1 ax:overflow-hidden ax:rounded-full", H_TRACK_H[size])}>
|
|
175
|
+
{/* Track - same hue at 15 % opacity */}
|
|
176
|
+
<div className={cn("ax:absolute ax:inset-0", bar.color, "ax:opacity-[0.15]")} />
|
|
177
|
+
{/* Fill - animated width from left */}
|
|
178
|
+
<div
|
|
179
|
+
className={cn(
|
|
180
|
+
"ax:absolute ax:inset-y-0 ax:left-0 ax:rounded-full",
|
|
181
|
+
bar.color,
|
|
182
|
+
"ax:[transition:width_var(--motion-slow)_var(--motion-ease-out)]",
|
|
183
|
+
)}
|
|
184
|
+
style={{ width: `${pct}%` }}
|
|
185
|
+
/>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
{/* Stats - percentage + count, tabular so digits don't dance */}
|
|
189
|
+
<div className={cn(STAT_CLS[size], "ax:flex ax:items-baseline ax:gap-xs ax:shrink-0 ax:tabular-nums")}>
|
|
190
|
+
{showPct && (
|
|
191
|
+
<span className="ax:text-muted-foreground ax:w-9 ax:text-right">{Math.round(pct)}%</span>
|
|
192
|
+
)}
|
|
193
|
+
{showCount && (
|
|
194
|
+
<span className="ax:text-foreground ax:font-medium">{bar.value}</span>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
)
|
|
199
|
+
})}
|
|
200
|
+
</div>
|
|
201
|
+
)
|
|
202
|
+
},
|
|
203
|
+
)
|
|
204
|
+
BarGraph.displayName = "BarGraph"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { BentoGrid, BentoItem } from "./bento-grid"
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof BentoGrid> = {
|
|
6
|
+
title: "Components/BentoGrid",
|
|
7
|
+
component: BentoGrid,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof BentoGrid>
|
|
13
|
+
|
|
14
|
+
export const Default: Story = {
|
|
15
|
+
render: () => (
|
|
16
|
+
<BentoGrid cols={3} className="ax:max-w-3xl">
|
|
17
|
+
<BentoItem colSpan={2} className="ax:p-lg">Wide feature</BentoItem>
|
|
18
|
+
<BentoItem className="ax:p-lg">Side card</BentoItem>
|
|
19
|
+
<BentoItem className="ax:p-lg">Analytics</BentoItem>
|
|
20
|
+
<BentoItem colSpan={2} className="ax:p-lg">Reports</BentoItem>
|
|
21
|
+
</BentoGrid>
|
|
22
|
+
),
|
|
23
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "../lib/cn"
|
|
3
|
+
|
|
4
|
+
// ─── Column / row span lookup tables ─────────────────────────────────────────
|
|
5
|
+
// Full class strings required so Tailwind's JIT includes them in the bundle.
|
|
6
|
+
|
|
7
|
+
const GRID_COLS = {
|
|
8
|
+
2: "ax:grid-cols-2",
|
|
9
|
+
3: "ax:grid-cols-3",
|
|
10
|
+
4: "ax:grid-cols-4",
|
|
11
|
+
} as const
|
|
12
|
+
|
|
13
|
+
const COL_SPAN = {
|
|
14
|
+
1: "ax:col-span-1",
|
|
15
|
+
2: "ax:col-span-2",
|
|
16
|
+
3: "ax:col-span-3",
|
|
17
|
+
4: "ax:col-span-4",
|
|
18
|
+
} as const
|
|
19
|
+
|
|
20
|
+
const ROW_SPAN = {
|
|
21
|
+
1: "ax:row-span-1",
|
|
22
|
+
2: "ax:row-span-2",
|
|
23
|
+
3: "ax:row-span-3",
|
|
24
|
+
} as const
|
|
25
|
+
|
|
26
|
+
// ─── BentoGrid ────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export interface BentoGridProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
29
|
+
/** Number of columns at all breakpoints. Default: 3. */
|
|
30
|
+
cols?: 2 | 3 | 4
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const BentoGrid = React.forwardRef<HTMLDivElement, BentoGridProps>(
|
|
34
|
+
({ className, cols = 3, ...props }, ref) => (
|
|
35
|
+
<div
|
|
36
|
+
ref={ref}
|
|
37
|
+
className={cn("ax:grid ax:gap-3", GRID_COLS[cols], className)}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
BentoGrid.displayName = "BentoGrid"
|
|
43
|
+
|
|
44
|
+
// ─── BentoItem ────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
export interface BentoItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
47
|
+
/** How many grid columns this cell spans. Default: 1. */
|
|
48
|
+
colSpan?: 1 | 2 | 3 | 4
|
|
49
|
+
/** How many grid rows this cell spans. Default: 1. */
|
|
50
|
+
rowSpan?: 1 | 2 | 3
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const BentoItem = React.forwardRef<HTMLDivElement, BentoItemProps>(
|
|
54
|
+
({ className, colSpan = 1, rowSpan = 1, ...props }, ref) => (
|
|
55
|
+
<div
|
|
56
|
+
ref={ref}
|
|
57
|
+
className={cn(
|
|
58
|
+
"ax:rounded-2xl ax:bg-card ax:border ax:border-border ax:overflow-hidden",
|
|
59
|
+
COL_SPAN[colSpan],
|
|
60
|
+
ROW_SPAN[rowSpan],
|
|
61
|
+
className
|
|
62
|
+
)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
BentoItem.displayName = "BentoItem"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { Blockquote, BlockquoteCaption, BlockquoteFigure } from "./blockquote"
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Blockquote> = {
|
|
6
|
+
title: "Components/Blockquote",
|
|
7
|
+
component: Blockquote,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
args: {
|
|
10
|
+
children: "Design is not just what it looks like. Design is how it works.",
|
|
11
|
+
size: "md",
|
|
12
|
+
color: "primary",
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default meta
|
|
17
|
+
type Story = StoryObj<typeof Blockquote>
|
|
18
|
+
|
|
19
|
+
export const Default: Story = {}
|
|
20
|
+
|
|
21
|
+
export const WithCaption: Story = {
|
|
22
|
+
render: (args) => (
|
|
23
|
+
<BlockquoteFigure>
|
|
24
|
+
<Blockquote {...args} />
|
|
25
|
+
<BlockquoteCaption>Steve Jobs</BlockquoteCaption>
|
|
26
|
+
</BlockquoteFigure>
|
|
27
|
+
),
|
|
28
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
5
|
+
import { cn } from "../lib/cn"
|
|
6
|
+
|
|
7
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export type BlockquoteSize = "sm" | "md" | "lg" | "xl"
|
|
10
|
+
export type BlockquoteColor = "primary" | "neutral"
|
|
11
|
+
|
|
12
|
+
// ─── Variants ─────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export const blockquoteVariants = cva(
|
|
15
|
+
"ax:border-l-2 ax:pl-4 ax:text-foreground",
|
|
16
|
+
{
|
|
17
|
+
variants: {
|
|
18
|
+
size: {
|
|
19
|
+
sm: "ax:t-body-sm",
|
|
20
|
+
md: "ax:t-body-md",
|
|
21
|
+
lg: "ax:t-body-lg",
|
|
22
|
+
// xl is one step bigger than t-body-lg for editorial / hero quotes.
|
|
23
|
+
// No t-body-xl exists, but font-normal makes the body intent explicit.
|
|
24
|
+
xl: "ax:text-xl ax:font-normal ax:leading-normal",
|
|
25
|
+
},
|
|
26
|
+
color: {
|
|
27
|
+
primary: "ax:border-primary",
|
|
28
|
+
neutral: "ax:border-border-strong",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
defaultVariants: {
|
|
32
|
+
size: "md",
|
|
33
|
+
color: "primary",
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
// ─── Figure wrapper ───────────────────────────────────────────────────────────
|
|
39
|
+
//
|
|
40
|
+
// Renders `<figure>` so a contained <figcaption> (via BlockquoteCaption) is
|
|
41
|
+
// valid HTML. Use whenever Blockquote is paired with a caption.
|
|
42
|
+
|
|
43
|
+
export interface BlockquoteFigureProps extends React.HTMLAttributes<HTMLElement> {}
|
|
44
|
+
|
|
45
|
+
export const BlockquoteFigure = React.forwardRef<HTMLElement, BlockquoteFigureProps>(
|
|
46
|
+
({ className, ...props }, ref) => (
|
|
47
|
+
// Pure structural wrapper: the caption manages its own top margin so the
|
|
48
|
+
// figure has no styling opinions of its own.
|
|
49
|
+
<figure ref={ref} className={className} {...props} />
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
BlockquoteFigure.displayName = "BlockquoteFigure"
|
|
53
|
+
|
|
54
|
+
// ─── Blockquote ───────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
export interface BlockquoteProps
|
|
57
|
+
extends Omit<React.BlockquoteHTMLAttributes<HTMLQuoteElement>, "color">,
|
|
58
|
+
VariantProps<typeof blockquoteVariants> {}
|
|
59
|
+
|
|
60
|
+
export const Blockquote = React.forwardRef<HTMLQuoteElement, BlockquoteProps>(
|
|
61
|
+
({ className, size, color, ...props }, ref) => (
|
|
62
|
+
<blockquote
|
|
63
|
+
ref={ref}
|
|
64
|
+
className={cn(blockquoteVariants({ size, color }), className)}
|
|
65
|
+
{...props}
|
|
66
|
+
/>
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
Blockquote.displayName = "Blockquote"
|
|
70
|
+
|
|
71
|
+
// ─── Caption ──────────────────────────────────────────────────────────────────
|
|
72
|
+
//
|
|
73
|
+
// Renders `<figcaption>` for attribution. Must be a child of `<BlockquoteFigure>`
|
|
74
|
+
// (or any `<figure>`) to be valid HTML: `<figcaption>` outside `<figure>` is
|
|
75
|
+
// invalid markup that screen readers handle inconsistently.
|
|
76
|
+
|
|
77
|
+
export interface BlockquoteCaptionProps extends React.HTMLAttributes<HTMLElement> {}
|
|
78
|
+
|
|
79
|
+
export const BlockquoteCaption = React.forwardRef<HTMLElement, BlockquoteCaptionProps>(
|
|
80
|
+
({ className, ...props }, ref) => (
|
|
81
|
+
<figcaption
|
|
82
|
+
ref={ref}
|
|
83
|
+
className={cn("ax:mt-2 ax:t-body-sm ax:text-muted-foreground", className)}
|
|
84
|
+
{...props}
|
|
85
|
+
/>
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
BlockquoteCaption.displayName = "BlockquoteCaption"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { BoxList } from "./box-list"
|
|
4
|
+
|
|
5
|
+
const items = [
|
|
6
|
+
{ title: "Unlimited projects", description: "Create as many workspaces as you need." },
|
|
7
|
+
{ title: "Team collaboration", description: "Invite teammates and assign roles." },
|
|
8
|
+
{ title: "Priority support", description: "Get help within one business day." },
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
const meta: Meta<typeof BoxList> = {
|
|
12
|
+
title: "Components/BoxList",
|
|
13
|
+
component: BoxList,
|
|
14
|
+
tags: ["autodocs"],
|
|
15
|
+
args: { items, columns: 3 },
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default meta
|
|
19
|
+
type Story = StoryObj<typeof BoxList>
|
|
20
|
+
|
|
21
|
+
export const Default: Story = {}
|
|
22
|
+
|
|
23
|
+
export const WithChecks: Story = {
|
|
24
|
+
args: { check: true },
|
|
25
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Check } from "lucide-react"
|
|
3
|
+
import { cn } from "../lib/cn"
|
|
4
|
+
|
|
5
|
+
export interface BoxListItem {
|
|
6
|
+
/** Heading line, rendered with the title type token. Omit for a plain
|
|
7
|
+
* body-text item (the `description` reads as the box's only line). */
|
|
8
|
+
title?: React.ReactNode
|
|
9
|
+
/** Supporting line. Sits under a `title`, or stands alone as body text. */
|
|
10
|
+
description?: React.ReactNode
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Responsive column count at desktop. */
|
|
14
|
+
const COLUMNS: Record<1 | 2 | 3 | 4, string> = {
|
|
15
|
+
1: "",
|
|
16
|
+
2: "ax:sm:grid-cols-2",
|
|
17
|
+
3: "ax:sm:grid-cols-2 ax:lg:grid-cols-3",
|
|
18
|
+
4: "ax:sm:grid-cols-2 ax:lg:grid-cols-4",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type BoxListSize = "sm" | "md" | "lg"
|
|
22
|
+
|
|
23
|
+
/** Box padding per size. */
|
|
24
|
+
const SIZE: Record<BoxListSize, string> = {
|
|
25
|
+
sm: "ax:p-md",
|
|
26
|
+
md: "ax:p-lg",
|
|
27
|
+
lg: "ax:p-xl",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Type scale per text size - steps the title and body line up together. */
|
|
31
|
+
const TEXT: Record<BoxListSize, { title: string; desc: string }> = {
|
|
32
|
+
sm: { title: "ax:t-h6", desc: "ax:t-caption" },
|
|
33
|
+
md: { title: "ax:t-h5", desc: "ax:t-body-sm" },
|
|
34
|
+
lg: { title: "ax:t-h4", desc: "ax:t-body-md" },
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface BoxListProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
38
|
+
items: BoxListItem[]
|
|
39
|
+
/** Columns at desktop (1–4). Default 3. Ignored when `vertical`. */
|
|
40
|
+
columns?: 1 | 2 | 3 | 4
|
|
41
|
+
/** Box padding. Default "lg". */
|
|
42
|
+
size?: BoxListSize
|
|
43
|
+
/** Type scale for the title + description. Default "md". */
|
|
44
|
+
textSize?: BoxListSize
|
|
45
|
+
/** Stack items in a single vertical column instead of a grid. Overrides
|
|
46
|
+
* `columns`. */
|
|
47
|
+
vertical?: boolean
|
|
48
|
+
/** Show a leading bullet dot before each item's title. */
|
|
49
|
+
bullet?: boolean
|
|
50
|
+
/** Show a leading Check icon before each item's title - for "included" /
|
|
51
|
+
* affirmative sets. Takes precedence over `bullet` when both are set. */
|
|
52
|
+
check?: boolean
|
|
53
|
+
/** Show a two-digit ordinal (01, 02, …) as a t-overline eyebrow above each
|
|
54
|
+
* item. Takes precedence over `check` and `bullet` when several are set. */
|
|
55
|
+
numerical?: boolean
|
|
56
|
+
/** Sit each item on the muted surface (`bg-muted`) instead of the default
|
|
57
|
+
* card surface - a softer, lower-contrast fill for secondary lists. */
|
|
58
|
+
muted?: boolean
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* BoxList - a responsive grid of bordered "box" cards, each a title with an
|
|
63
|
+
* optional supporting line. The tidy way to present a set of features,
|
|
64
|
+
* services, terms or options without hand-rolling the card + grid each time.
|
|
65
|
+
*
|
|
66
|
+
* Optionally mark each item (precedence: numerical → check → bullet):
|
|
67
|
+
* `numerical` prefixes a 01, 02, … eyebrow (ordered sequences), `check` a Check
|
|
68
|
+
* icon (affirmative sets), or `bullet` a leading dot (an unordered set).
|
|
69
|
+
*/
|
|
70
|
+
export function BoxList({ items, columns = 3, size = "lg", textSize = "md", vertical = false, bullet = false, check = false, numerical = false, muted = false, className, ...props }: BoxListProps) {
|
|
71
|
+
return (
|
|
72
|
+
<div
|
|
73
|
+
className={cn(
|
|
74
|
+
"ax:gap-2xs",
|
|
75
|
+
vertical ? "ax:flex ax:flex-col" : cn("ax:grid", COLUMNS[columns]),
|
|
76
|
+
className,
|
|
77
|
+
)}
|
|
78
|
+
{...props}
|
|
79
|
+
>
|
|
80
|
+
{items.map((item, i) => (
|
|
81
|
+
<div key={i} className={cn("ax:rounded-xs", muted ? "ax:bg-muted" : "ax:bg-card", SIZE[size])}>
|
|
82
|
+
{numerical && (
|
|
83
|
+
<div className="ax:mb-sm ax:t-overline ax:text-primary">{String(i + 1).padStart(2, "0")}</div>
|
|
84
|
+
)}
|
|
85
|
+
<div className="ax:flex ax:items-center ax:gap-sm">
|
|
86
|
+
{!numerical && check && (
|
|
87
|
+
<Check aria-hidden="true" className="ax:size-4 ax:shrink-0 ax:text-primary" />
|
|
88
|
+
)}
|
|
89
|
+
{!numerical && !check && bullet && (
|
|
90
|
+
<span aria-hidden="true" className="ax:size-1.5 ax:shrink-0 ax:rounded-full ax:bg-foreground-strong" />
|
|
91
|
+
)}
|
|
92
|
+
<div
|
|
93
|
+
className={cn(
|
|
94
|
+
item.title != null ? TEXT[textSize].title : TEXT[textSize].desc,
|
|
95
|
+
"ax:text-foreground",
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
{item.title ?? item.description}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
{item.title != null && item.description != null && (
|
|
102
|
+
<div className={cn("ax:mt-2xs ax:text-muted-foreground", TEXT[textSize].desc)}>{item.description}</div>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
))}
|
|
106
|
+
</div>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { BulletList, BulletListItem } from "./bullet-list"
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof BulletList> = {
|
|
6
|
+
title: "Components/BulletList",
|
|
7
|
+
component: BulletList,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj<typeof BulletList>
|
|
13
|
+
|
|
14
|
+
export const Default: Story = {
|
|
15
|
+
render: () => (
|
|
16
|
+
<BulletList>
|
|
17
|
+
<BulletListItem>Responsive by default</BulletListItem>
|
|
18
|
+
<BulletListItem>Accessible components</BulletListItem>
|
|
19
|
+
<BulletListItem>Brand-aware tokens</BulletListItem>
|
|
20
|
+
</BulletList>
|
|
21
|
+
),
|
|
22
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../lib/cn"
|
|
5
|
+
|
|
6
|
+
export type BulletListSize = "sm" | "md" | "lg"
|
|
7
|
+
|
|
8
|
+
// ─── Size tokens ──────────────────────────────────────────────────────────────
|
|
9
|
+
//
|
|
10
|
+
// Text uses the matching `t-body-*` token so the bullet rhythm tracks the
|
|
11
|
+
// surrounding prose. Bullet diameter scales with text size; row gap stays at
|
|
12
|
+
// `gap-sm` (12px), comfortable across all three sizes without feeling loose.
|
|
13
|
+
|
|
14
|
+
const TEXT_CLASS: Record<BulletListSize, string> = {
|
|
15
|
+
sm: "ax:t-body-sm",
|
|
16
|
+
md: "ax:t-body-md",
|
|
17
|
+
lg: "ax:t-body-lg",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const BULLET_CLASS: Record<BulletListSize, string> = {
|
|
21
|
+
sm: "ax:size-1", // 4px
|
|
22
|
+
md: "ax:size-1.5", // 6px
|
|
23
|
+
lg: "ax:size-2", // 8px
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ROW_GAP_CLASS: Record<BulletListSize, string> = {
|
|
27
|
+
sm: "ax:gap-xs",
|
|
28
|
+
md: "ax:gap-sm",
|
|
29
|
+
lg: "ax:gap-sm",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const LIST_GAP_CLASS: Record<BulletListSize, string> = {
|
|
33
|
+
sm: "ax:space-y-2xs",
|
|
34
|
+
md: "ax:space-y-xs",
|
|
35
|
+
lg: "ax:space-y-sm",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Context ──────────────────────────────────────────────────────────────────
|
|
39
|
+
//
|
|
40
|
+
// Size is propagated via context so an item can pick up the right text/bullet
|
|
41
|
+
// classes without the consumer threading it through every <BulletListItem>.
|
|
42
|
+
|
|
43
|
+
interface BulletListContextValue {
|
|
44
|
+
size: BulletListSize
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const BulletListContext = React.createContext<BulletListContextValue>({ size: "md" })
|
|
48
|
+
|
|
49
|
+
// ─── BulletList ───────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
export interface BulletListProps extends React.HTMLAttributes<HTMLUListElement> {
|
|
52
|
+
size?: BulletListSize
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Bulleted list with a small foreground-strong dot, body-text rhythm, and
|
|
56
|
+
* consistent spacing. Compose with `BulletListItem`. Use for short lists
|
|
57
|
+
* of related items (goals, features, qualities) where the surrounding
|
|
58
|
+
* context already provides the heading. */
|
|
59
|
+
export const BulletList = React.forwardRef<HTMLUListElement, BulletListProps>(
|
|
60
|
+
({ size = "md", className, children, ...props }, ref) => (
|
|
61
|
+
<BulletListContext.Provider value={{ size }}>
|
|
62
|
+
<ul
|
|
63
|
+
ref={ref}
|
|
64
|
+
className={cn("ax:list-none", LIST_GAP_CLASS[size], className)}
|
|
65
|
+
{...props}
|
|
66
|
+
>
|
|
67
|
+
{children}
|
|
68
|
+
</ul>
|
|
69
|
+
</BulletListContext.Provider>
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
BulletList.displayName = "BulletList"
|
|
73
|
+
|
|
74
|
+
// ─── BulletListItem ───────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
export interface BulletListItemProps extends React.LiHTMLAttributes<HTMLLIElement> {}
|
|
77
|
+
|
|
78
|
+
export const BulletListItem = React.forwardRef<HTMLLIElement, BulletListItemProps>(
|
|
79
|
+
({ className, children, ...props }, ref) => {
|
|
80
|
+
const { size } = React.useContext(BulletListContext)
|
|
81
|
+
return (
|
|
82
|
+
<li
|
|
83
|
+
ref={ref}
|
|
84
|
+
className={cn(
|
|
85
|
+
"ax:flex ax:items-center ax:text-foreground",
|
|
86
|
+
ROW_GAP_CLASS[size],
|
|
87
|
+
TEXT_CLASS[size],
|
|
88
|
+
className,
|
|
89
|
+
)}
|
|
90
|
+
{...props}
|
|
91
|
+
>
|
|
92
|
+
<span
|
|
93
|
+
aria-hidden="true"
|
|
94
|
+
className={cn("ax:rounded-full ax:shrink-0 ax:bg-foreground-strong", BULLET_CLASS[size])}
|
|
95
|
+
/>
|
|
96
|
+
<span>{children}</span>
|
|
97
|
+
</li>
|
|
98
|
+
)
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
BulletListItem.displayName = "BulletListItem"
|