@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,252 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
|
5
|
+
import { Check, ChevronRight, Circle } from "lucide-react"
|
|
6
|
+
import { cn } from "../lib/cn"
|
|
7
|
+
|
|
8
|
+
// ─── Primitives (pass-through) ────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export const DropdownMenu = DropdownMenuPrimitive.Root
|
|
11
|
+
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
|
12
|
+
export const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
|
13
|
+
export const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
|
14
|
+
export const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
|
15
|
+
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
|
16
|
+
|
|
17
|
+
// ─── Shared row-state classes ─────────────────────────────────────────────────
|
|
18
|
+
//
|
|
19
|
+
// Radix exposes BOTH `:focus` (DOM focus) AND `data-highlighted` (keyboard nav
|
|
20
|
+
// highlight) for the active item. Listening for both ensures the highlight
|
|
21
|
+
// renders correctly regardless of whether the user is mousing or tabbing.
|
|
22
|
+
|
|
23
|
+
const ROW_BASE_DEFAULT =
|
|
24
|
+
"ax:text-foreground " +
|
|
25
|
+
"ax:focus:bg-muted ax:focus:text-foreground " +
|
|
26
|
+
"ax:data-[highlighted]:bg-muted ax:data-[highlighted]:text-foreground"
|
|
27
|
+
|
|
28
|
+
const ROW_BASE_DESTRUCTIVE =
|
|
29
|
+
"ax:text-destructive-strong " +
|
|
30
|
+
"ax:focus:bg-destructive/10 ax:focus:text-destructive-strong " +
|
|
31
|
+
"ax:data-[highlighted]:bg-destructive/10 ax:data-[highlighted]:text-destructive-strong " +
|
|
32
|
+
"ax:[&_svg]:text-destructive-strong"
|
|
33
|
+
|
|
34
|
+
// ─── Content ──────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
export interface DropdownMenuContentProps
|
|
37
|
+
extends React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> {}
|
|
38
|
+
|
|
39
|
+
export const DropdownMenuContent = React.forwardRef<
|
|
40
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
41
|
+
DropdownMenuContentProps
|
|
42
|
+
>(({ className, sideOffset = 6, ...props }, ref) => (
|
|
43
|
+
<DropdownMenuPrimitive.Portal>
|
|
44
|
+
<DropdownMenuPrimitive.Content
|
|
45
|
+
ref={ref}
|
|
46
|
+
sideOffset={sideOffset}
|
|
47
|
+
className={cn(
|
|
48
|
+
// Layered above Dialog/Drawer (z-[1100]/z-[1101]) so menus opened from
|
|
49
|
+
// within a drawer's footer (e.g. a mobile nav account card) stay visible.
|
|
50
|
+
"ax:z-1200 ax:min-w-[10rem] ax:overflow-hidden ax:rounded-(--input-radius) ax:border ax:border-border ax:bg-popover ax:p-2xs",
|
|
51
|
+
"ax:text-popover-foreground ax:shadow-md",
|
|
52
|
+
"ax:data-[state=open]:animate-in ax:data-[state=closed]:animate-out",
|
|
53
|
+
"ax:data-[state=closed]:fade-out-0 ax:data-[state=open]:fade-in-0",
|
|
54
|
+
"ax:data-[state=closed]:zoom-out-[0.96] ax:data-[state=open]:zoom-in-[0.96]",
|
|
55
|
+
"ax:data-[side=bottom]:slide-in-from-top-1",
|
|
56
|
+
"ax:data-[side=left]:slide-in-from-right-1",
|
|
57
|
+
"ax:data-[side=right]:slide-in-from-left-1",
|
|
58
|
+
"ax:data-[side=top]:slide-in-from-bottom-1",
|
|
59
|
+
"ax:[animation-duration:var(--motion-fast)] ax:[animation-timing-function:var(--motion-ease-out)]",
|
|
60
|
+
"ax:motion-reduce:animate-none",
|
|
61
|
+
className
|
|
62
|
+
)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
</DropdownMenuPrimitive.Portal>
|
|
66
|
+
))
|
|
67
|
+
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
|
68
|
+
|
|
69
|
+
// ─── Item ─────────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
export interface DropdownMenuItemProps
|
|
72
|
+
extends React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> {
|
|
73
|
+
/** Indents the item to align with siblings that have a checkbox/radio indicator column. */
|
|
74
|
+
inset?: boolean
|
|
75
|
+
/** Colour treatment. `destructive` tints text + icon + hover/highlight background. */
|
|
76
|
+
variant?: "default" | "destructive"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const DropdownMenuItem = React.forwardRef<
|
|
80
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
81
|
+
DropdownMenuItemProps
|
|
82
|
+
>(({ className, inset, variant = "default", ...props }, ref) => (
|
|
83
|
+
<DropdownMenuPrimitive.Item
|
|
84
|
+
ref={ref}
|
|
85
|
+
className={cn(
|
|
86
|
+
"ax:relative ax:flex ax:cursor-pointer ax:select-none ax:items-center ax:gap-xs ax:rounded-(--input-radius) ax:px-xs ax:py-1.5",
|
|
87
|
+
"ax:t-body-sm ax:outline-none ax:transition-colors",
|
|
88
|
+
"ax:data-[disabled]:pointer-events-none ax:data-[disabled]:opacity-50",
|
|
89
|
+
"ax:[&_svg]:size-4 ax:[&_svg]:shrink-0 ax:[&_svg]:text-muted-foreground",
|
|
90
|
+
variant === "destructive" ? ROW_BASE_DESTRUCTIVE : ROW_BASE_DEFAULT,
|
|
91
|
+
inset && "ax:pl-8",
|
|
92
|
+
className
|
|
93
|
+
)}
|
|
94
|
+
{...props}
|
|
95
|
+
/>
|
|
96
|
+
))
|
|
97
|
+
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
|
98
|
+
|
|
99
|
+
// ─── CheckboxItem ─────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
export const DropdownMenuCheckboxItem = React.forwardRef<
|
|
102
|
+
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
|
103
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
|
104
|
+
>(({ className, children, checked, ...props }, ref) => (
|
|
105
|
+
<DropdownMenuPrimitive.CheckboxItem
|
|
106
|
+
ref={ref}
|
|
107
|
+
className={cn(
|
|
108
|
+
"ax:relative ax:flex ax:cursor-pointer ax:select-none ax:items-center ax:rounded-(--input-radius) ax:py-1.5 ax:pl-8 ax:pr-xs",
|
|
109
|
+
"ax:t-body-sm ax:outline-none ax:transition-colors",
|
|
110
|
+
"ax:data-[disabled]:pointer-events-none ax:data-[disabled]:opacity-50",
|
|
111
|
+
ROW_BASE_DEFAULT,
|
|
112
|
+
className
|
|
113
|
+
)}
|
|
114
|
+
checked={checked}
|
|
115
|
+
{...props}
|
|
116
|
+
>
|
|
117
|
+
<span className="ax:absolute ax:left-2 ax:flex ax:size-3.5 ax:items-center ax:justify-center">
|
|
118
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
119
|
+
<Check className="ax:size-4" />
|
|
120
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
121
|
+
</span>
|
|
122
|
+
{children}
|
|
123
|
+
</DropdownMenuPrimitive.CheckboxItem>
|
|
124
|
+
))
|
|
125
|
+
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
|
|
126
|
+
|
|
127
|
+
// ─── RadioItem ────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
export const DropdownMenuRadioItem = React.forwardRef<
|
|
130
|
+
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
|
131
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
|
132
|
+
>(({ className, children, ...props }, ref) => (
|
|
133
|
+
<DropdownMenuPrimitive.RadioItem
|
|
134
|
+
ref={ref}
|
|
135
|
+
className={cn(
|
|
136
|
+
"ax:relative ax:flex ax:cursor-pointer ax:select-none ax:items-center ax:rounded-(--input-radius) ax:py-1.5 ax:pl-8 ax:pr-xs",
|
|
137
|
+
"ax:t-body-sm ax:outline-none ax:transition-colors",
|
|
138
|
+
"ax:data-[disabled]:pointer-events-none ax:data-[disabled]:opacity-50",
|
|
139
|
+
ROW_BASE_DEFAULT,
|
|
140
|
+
className
|
|
141
|
+
)}
|
|
142
|
+
{...props}
|
|
143
|
+
>
|
|
144
|
+
<span className="ax:absolute ax:left-2 ax:flex ax:size-3.5 ax:items-center ax:justify-center">
|
|
145
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
146
|
+
<Circle className="ax:size-2 ax:fill-current" />
|
|
147
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
148
|
+
</span>
|
|
149
|
+
{children}
|
|
150
|
+
</DropdownMenuPrimitive.RadioItem>
|
|
151
|
+
))
|
|
152
|
+
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
|
153
|
+
|
|
154
|
+
// ─── Label ────────────────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
export const DropdownMenuLabel = React.forwardRef<
|
|
157
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
158
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { inset?: boolean }
|
|
159
|
+
>(({ className, inset, ...props }, ref) => (
|
|
160
|
+
<DropdownMenuPrimitive.Label
|
|
161
|
+
ref={ref}
|
|
162
|
+
className={cn(
|
|
163
|
+
"ax:px-xs ax:py-1.5 ax:t-overline ax:text-muted-foreground",
|
|
164
|
+
inset && "ax:pl-8",
|
|
165
|
+
className
|
|
166
|
+
)}
|
|
167
|
+
{...props}
|
|
168
|
+
/>
|
|
169
|
+
))
|
|
170
|
+
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
|
171
|
+
|
|
172
|
+
// ─── Separator ────────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
export const DropdownMenuSeparator = React.forwardRef<
|
|
175
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
176
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
177
|
+
>(({ className, ...props }, ref) => (
|
|
178
|
+
<DropdownMenuPrimitive.Separator
|
|
179
|
+
ref={ref}
|
|
180
|
+
className={cn("ax:-mx-2xs ax:my-2xs ax:h-px ax:bg-border", className)}
|
|
181
|
+
{...props}
|
|
182
|
+
/>
|
|
183
|
+
))
|
|
184
|
+
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
|
185
|
+
|
|
186
|
+
// ─── SubTrigger / SubContent ─────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
export const DropdownMenuSubTrigger = React.forwardRef<
|
|
189
|
+
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
|
190
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { inset?: boolean }
|
|
191
|
+
>(({ className, inset, children, ...props }, ref) => (
|
|
192
|
+
<DropdownMenuPrimitive.SubTrigger
|
|
193
|
+
ref={ref}
|
|
194
|
+
className={cn(
|
|
195
|
+
"ax:flex ax:cursor-pointer ax:select-none ax:items-center ax:gap-xs ax:rounded-(--input-radius) ax:px-xs ax:py-1.5",
|
|
196
|
+
"ax:t-body-sm ax:text-foreground ax:outline-none ax:transition-colors",
|
|
197
|
+
"ax:focus:bg-muted ax:data-[highlighted]:bg-muted ax:data-[state=open]:bg-muted",
|
|
198
|
+
"ax:[&_svg]:size-4 ax:[&_svg]:shrink-0 ax:[&_svg]:text-muted-foreground",
|
|
199
|
+
inset && "ax:pl-8",
|
|
200
|
+
className
|
|
201
|
+
)}
|
|
202
|
+
{...props}
|
|
203
|
+
>
|
|
204
|
+
{children}
|
|
205
|
+
<ChevronRight className="ax:ml-auto ax:size-4" />
|
|
206
|
+
</DropdownMenuPrimitive.SubTrigger>
|
|
207
|
+
))
|
|
208
|
+
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
|
|
209
|
+
|
|
210
|
+
export const DropdownMenuSubContent = React.forwardRef<
|
|
211
|
+
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
|
212
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
|
213
|
+
>(({ className, ...props }, ref) => (
|
|
214
|
+
<DropdownMenuPrimitive.SubContent
|
|
215
|
+
ref={ref}
|
|
216
|
+
className={cn(
|
|
217
|
+
// See DropdownMenuContent, layered above Dialog/Drawer.
|
|
218
|
+
"ax:z-1200 ax:min-w-[10rem] ax:overflow-hidden ax:rounded-(--input-radius) ax:border ax:border-border ax:bg-popover ax:p-2xs",
|
|
219
|
+
"ax:text-popover-foreground ax:shadow-lg",
|
|
220
|
+
"ax:data-[state=open]:animate-in ax:data-[state=closed]:animate-out",
|
|
221
|
+
"ax:data-[state=closed]:fade-out-0 ax:data-[state=open]:fade-in-0",
|
|
222
|
+
"ax:data-[state=closed]:zoom-out-[0.96] ax:data-[state=open]:zoom-in-[0.96]",
|
|
223
|
+
// Side-aware slide-ins so sub-menus opening left vs right feel directional
|
|
224
|
+
// (mirrors the Content animation set above).
|
|
225
|
+
"ax:data-[side=bottom]:slide-in-from-top-1",
|
|
226
|
+
"ax:data-[side=left]:slide-in-from-right-1",
|
|
227
|
+
"ax:data-[side=right]:slide-in-from-left-1",
|
|
228
|
+
"ax:data-[side=top]:slide-in-from-bottom-1",
|
|
229
|
+
"ax:[animation-duration:var(--motion-fast)] ax:[animation-timing-function:var(--motion-ease-out)]",
|
|
230
|
+
"ax:motion-reduce:animate-none",
|
|
231
|
+
className
|
|
232
|
+
)}
|
|
233
|
+
{...props}
|
|
234
|
+
/>
|
|
235
|
+
))
|
|
236
|
+
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
|
|
237
|
+
|
|
238
|
+
// ─── Shortcut ─────────────────────────────────────────────────────────────────
|
|
239
|
+
//
|
|
240
|
+
// Right-aligned keyboard-shortcut hint for use inside DropdownMenuItem etc.
|
|
241
|
+
// Visually-only. Wire the actual shortcut keys via a global handler elsewhere.
|
|
242
|
+
|
|
243
|
+
export const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => (
|
|
244
|
+
<span
|
|
245
|
+
className={cn(
|
|
246
|
+
"ax:ml-auto ax:pl-md ax:t-caption ax:text-muted-foreground ax:tracking-wider",
|
|
247
|
+
className
|
|
248
|
+
)}
|
|
249
|
+
{...props}
|
|
250
|
+
/>
|
|
251
|
+
)
|
|
252
|
+
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { Dropzone } from "./dropzone"
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Dropzone> = {
|
|
6
|
+
title: "Components/Dropzone",
|
|
7
|
+
component: Dropzone,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
args: {
|
|
10
|
+
onFile: () => {},
|
|
11
|
+
accept: ".csv",
|
|
12
|
+
prompt: "Drop a CSV file here, or browse",
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default meta
|
|
17
|
+
type Story = StoryObj<typeof Dropzone>
|
|
18
|
+
|
|
19
|
+
export const Default: Story = {}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { FileText, UploadCloud } from "lucide-react"
|
|
5
|
+
import { Button } from "./button"
|
|
6
|
+
import { Spinner } from "./spinner"
|
|
7
|
+
import { cn } from "../lib/cn"
|
|
8
|
+
|
|
9
|
+
export type DropzoneErrorReason = "invalid-type" | "too-large"
|
|
10
|
+
|
|
11
|
+
export interface DropzoneError {
|
|
12
|
+
reason: DropzoneErrorReason
|
|
13
|
+
file: File
|
|
14
|
+
message: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// `onError` is our domain callback (file-rejection event); the HTMLAttributes
|
|
18
|
+
// `onError` (image-style ReactEventHandler) is omitted to keep the type clean.
|
|
19
|
+
export interface DropzoneProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onError"> {
|
|
20
|
+
/** Called when a file is dropped or selected via the file picker AND
|
|
21
|
+
* passes type / size validation. */
|
|
22
|
+
onFile: (file: File) => void
|
|
23
|
+
/** Called when a dropped or picked file fails validation (wrong type,
|
|
24
|
+
* exceeds `maxSize`). The file is NOT forwarded to `onFile`. */
|
|
25
|
+
onError?: (error: DropzoneError) => void
|
|
26
|
+
/** Currently-selected file. When set, the dropzone collapses to a
|
|
27
|
+
* selected-file row with a "Replace" button. */
|
|
28
|
+
file?: File | null
|
|
29
|
+
/** While true, the dropzone shows a spinner + "Processing…" instead of
|
|
30
|
+
* the upload affordances. Drive this from the parent's upload state. */
|
|
31
|
+
processing?: boolean
|
|
32
|
+
/** Disables the drop target and Browse button. */
|
|
33
|
+
disabled?: boolean
|
|
34
|
+
/** Accept attribute passed through to the underlying `<input>` (e.g.
|
|
35
|
+
* `".csv"`, `"image/*"`). Also enforced on drag-and-drop - browsers
|
|
36
|
+
* don't natively validate `accept` for drops. */
|
|
37
|
+
accept?: string
|
|
38
|
+
/** Maximum file size in bytes. Files exceeding this trigger `onError`
|
|
39
|
+
* rather than `onFile`. */
|
|
40
|
+
maxSize?: number
|
|
41
|
+
/** Localizable prompt text. Defaults to a CSV-flavored prompt. */
|
|
42
|
+
prompt?: string
|
|
43
|
+
/** Optional helper link rendered under the Browse button. */
|
|
44
|
+
templateHref?: string
|
|
45
|
+
templateLabel?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** File dropzone with three states: empty (drag-or-browse prompt), selected
|
|
49
|
+
* (file row + replace), and processing (spinner). The component is fully
|
|
50
|
+
* controlled: the parent owns the file + processing state and decides
|
|
51
|
+
* what to do when `onFile` fires. */
|
|
52
|
+
|
|
53
|
+
// Matches a file against an accept attribute string. Returns true if the
|
|
54
|
+
// file's extension or MIME type satisfies any of the comma-separated entries.
|
|
55
|
+
function fileMatchesAccept(file: File, accept: string): boolean {
|
|
56
|
+
if (!accept) return true
|
|
57
|
+
const lowerName = file.name.toLowerCase()
|
|
58
|
+
const lowerType = file.type.toLowerCase()
|
|
59
|
+
return accept
|
|
60
|
+
.split(",")
|
|
61
|
+
.map((s) => s.trim().toLowerCase())
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.some((entry) => {
|
|
64
|
+
if (entry.startsWith(".")) return lowerName.endsWith(entry)
|
|
65
|
+
if (entry.endsWith("/*")) return lowerType.startsWith(entry.slice(0, -1))
|
|
66
|
+
return lowerType === entry
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatBytes(n: number): string {
|
|
71
|
+
if (n < 1024) return `${n} B`
|
|
72
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
|
|
73
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const Dropzone = React.forwardRef<HTMLDivElement, DropzoneProps>(
|
|
77
|
+
(
|
|
78
|
+
{
|
|
79
|
+
onFile,
|
|
80
|
+
onError,
|
|
81
|
+
file,
|
|
82
|
+
processing,
|
|
83
|
+
disabled = false,
|
|
84
|
+
accept = ".csv",
|
|
85
|
+
maxSize,
|
|
86
|
+
prompt = "Drag & drop your CSV file here",
|
|
87
|
+
templateHref,
|
|
88
|
+
templateLabel = "Download Template",
|
|
89
|
+
className,
|
|
90
|
+
...props
|
|
91
|
+
},
|
|
92
|
+
ref,
|
|
93
|
+
) => {
|
|
94
|
+
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
95
|
+
// Counter (not boolean) so flicker over child elements (icon, text)
|
|
96
|
+
// doesn't toggle the highlight on/off as drag enters/leaves descendants.
|
|
97
|
+
const dragCount = React.useRef(0)
|
|
98
|
+
const [dragging, setDragging] = React.useState(false)
|
|
99
|
+
|
|
100
|
+
function validateAndForward(candidate: File) {
|
|
101
|
+
if (!fileMatchesAccept(candidate, accept)) {
|
|
102
|
+
onError?.({
|
|
103
|
+
reason: "invalid-type",
|
|
104
|
+
file: candidate,
|
|
105
|
+
message: `File type not allowed (expected ${accept}).`,
|
|
106
|
+
})
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
if (maxSize !== undefined && candidate.size > maxSize) {
|
|
110
|
+
onError?.({
|
|
111
|
+
reason: "too-large",
|
|
112
|
+
file: candidate,
|
|
113
|
+
message: `File exceeds the ${formatBytes(maxSize)} limit.`,
|
|
114
|
+
})
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
onFile(candidate)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function handleDragEnter(e: React.DragEvent<HTMLDivElement>) {
|
|
121
|
+
if (disabled) return
|
|
122
|
+
e.preventDefault()
|
|
123
|
+
dragCount.current++
|
|
124
|
+
setDragging(true)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function handleDragLeave(e: React.DragEvent<HTMLDivElement>) {
|
|
128
|
+
if (disabled) return
|
|
129
|
+
e.preventDefault()
|
|
130
|
+
dragCount.current = Math.max(0, dragCount.current - 1)
|
|
131
|
+
if (dragCount.current === 0) setDragging(false)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function handleDrop(e: React.DragEvent<HTMLDivElement>) {
|
|
135
|
+
if (disabled) return
|
|
136
|
+
e.preventDefault()
|
|
137
|
+
dragCount.current = 0
|
|
138
|
+
setDragging(false)
|
|
139
|
+
const dropped = e.dataTransfer.files[0]
|
|
140
|
+
if (dropped) validateAndForward(dropped)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
144
|
+
const selected = e.target.files?.[0]
|
|
145
|
+
if (selected) validateAndForward(selected)
|
|
146
|
+
// Reset so picking the same file twice still fires onChange.
|
|
147
|
+
e.target.value = ""
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (processing) {
|
|
151
|
+
return (
|
|
152
|
+
<div
|
|
153
|
+
ref={ref}
|
|
154
|
+
className={cn("ax:flex ax:flex-col ax:items-center ax:gap-md ax:py-2xl", className)}
|
|
155
|
+
{...props}
|
|
156
|
+
>
|
|
157
|
+
<Spinner />
|
|
158
|
+
<p className="ax:t-body-sm ax:text-muted-foreground">Processing…</p>
|
|
159
|
+
</div>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (file) {
|
|
164
|
+
return (
|
|
165
|
+
<div
|
|
166
|
+
ref={ref}
|
|
167
|
+
className={cn(
|
|
168
|
+
"ax:flex ax:items-center ax:gap-sm ax:p-md ax:rounded-md ax:border ax:border-border ax:bg-muted/30",
|
|
169
|
+
disabled && "ax:opacity-60 ax:cursor-not-allowed",
|
|
170
|
+
className,
|
|
171
|
+
)}
|
|
172
|
+
{...props}
|
|
173
|
+
>
|
|
174
|
+
<FileText className="ax:size-5 ax:text-primary ax:shrink-0" />
|
|
175
|
+
<div className="ax:flex-1 ax:min-w-0">
|
|
176
|
+
<p className="ax:t-label-sm ax:text-foreground ax:truncate">{file.name}</p>
|
|
177
|
+
<p className="ax:t-caption ax:text-muted-foreground">{formatBytes(file.size)}</p>
|
|
178
|
+
</div>
|
|
179
|
+
<Button
|
|
180
|
+
variant="ghost"
|
|
181
|
+
size="sm"
|
|
182
|
+
disabled={disabled}
|
|
183
|
+
onClick={() => inputRef.current?.click()}
|
|
184
|
+
>
|
|
185
|
+
Replace
|
|
186
|
+
</Button>
|
|
187
|
+
<input
|
|
188
|
+
ref={inputRef}
|
|
189
|
+
type="file"
|
|
190
|
+
accept={accept}
|
|
191
|
+
disabled={disabled}
|
|
192
|
+
className="ax:hidden"
|
|
193
|
+
onChange={handleChange}
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<div
|
|
201
|
+
ref={ref}
|
|
202
|
+
className={cn(
|
|
203
|
+
"ax:flex ax:flex-col ax:items-center ax:gap-md ax:py-2xl ax:px-xl",
|
|
204
|
+
"ax:border-2 ax:border-dashed ax:rounded-md ax:border-border",
|
|
205
|
+
"ax:transition-colors",
|
|
206
|
+
dragging && !disabled && "ax:border-primary ax:bg-primary/5",
|
|
207
|
+
disabled && "ax:opacity-60 ax:cursor-not-allowed ax:bg-muted/30",
|
|
208
|
+
className,
|
|
209
|
+
)}
|
|
210
|
+
onDragOver={(e) => { if (!disabled) e.preventDefault() }}
|
|
211
|
+
onDragEnter={handleDragEnter}
|
|
212
|
+
onDragLeave={handleDragLeave}
|
|
213
|
+
onDrop={handleDrop}
|
|
214
|
+
{...props}
|
|
215
|
+
>
|
|
216
|
+
<UploadCloud className="ax:size-3xl ax:text-muted-foreground" />
|
|
217
|
+
<div className="ax:text-center">
|
|
218
|
+
<p className="ax:t-body-sm ax:text-foreground ax:font-medium">{prompt}</p>
|
|
219
|
+
<p className="ax:t-caption ax:text-muted-foreground ax:mt-1">or</p>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div className="ax:flex ax:flex-col ax:items-center ax:gap-xs">
|
|
223
|
+
<Button
|
|
224
|
+
variant="outline"
|
|
225
|
+
size="sm"
|
|
226
|
+
disabled={disabled}
|
|
227
|
+
onClick={() => inputRef.current?.click()}
|
|
228
|
+
>
|
|
229
|
+
Browse File
|
|
230
|
+
</Button>
|
|
231
|
+
{templateHref && (
|
|
232
|
+
<a href={templateHref} className="ax:t-caption ax:text-primary ax:hover:underline ax:focus-visible:underline">
|
|
233
|
+
{templateLabel}
|
|
234
|
+
</a>
|
|
235
|
+
)}
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<input
|
|
239
|
+
ref={inputRef}
|
|
240
|
+
type="file"
|
|
241
|
+
accept={accept}
|
|
242
|
+
disabled={disabled}
|
|
243
|
+
className="ax:hidden"
|
|
244
|
+
onChange={handleChange}
|
|
245
|
+
/>
|
|
246
|
+
</div>
|
|
247
|
+
)
|
|
248
|
+
},
|
|
249
|
+
)
|
|
250
|
+
Dropzone.displayName = "Dropzone"
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import {
|
|
4
|
+
EmailChipsInput,
|
|
5
|
+
type EmailChip,
|
|
6
|
+
type EmailChipsInputProps,
|
|
7
|
+
} from "./email-chips-input"
|
|
8
|
+
|
|
9
|
+
const meta: Meta<typeof EmailChipsInput> = {
|
|
10
|
+
title: "Components/EmailChipsInput",
|
|
11
|
+
component: EmailChipsInput,
|
|
12
|
+
tags: ["autodocs"],
|
|
13
|
+
parameters: {
|
|
14
|
+
layout: "centered",
|
|
15
|
+
docs: {
|
|
16
|
+
description: {
|
|
17
|
+
component:
|
|
18
|
+
"Multi-email input that turns each accepted entry into a chip. Adds on Enter or `,`. Backspace on an empty input removes the last chip. Pasting a `, ; \\n \\t`-separated list adds each address as a chip.\n\nValidation is local (regex-based) and non-blocking: invalid chips render in a destructive tint with a tooltip explaining why. Consumers filter `chip.valid` before submitting.",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
argTypes: {
|
|
23
|
+
chips: { control: false },
|
|
24
|
+
onChange: { control: false },
|
|
25
|
+
onDuplicate: { control: false },
|
|
26
|
+
},
|
|
27
|
+
args: {
|
|
28
|
+
placeholder: "Add email addresses…",
|
|
29
|
+
variant: "default",
|
|
30
|
+
disabled: false,
|
|
31
|
+
invalid: false,
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default meta
|
|
36
|
+
type Story = StoryObj<typeof EmailChipsInput>
|
|
37
|
+
|
|
38
|
+
function EmailChipsInputDemo({
|
|
39
|
+
initialChips = [],
|
|
40
|
+
...props
|
|
41
|
+
}: Omit<EmailChipsInputProps, "chips" | "onChange"> & {
|
|
42
|
+
initialChips?: EmailChip[]
|
|
43
|
+
}) {
|
|
44
|
+
const [chips, setChips] = React.useState<EmailChip[]>(initialChips)
|
|
45
|
+
const validCount = chips.filter((c) => c.valid).length
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="ax:w-full ax:min-w-[20rem] ax:max-w-md ax:space-y-2">
|
|
49
|
+
<EmailChipsInput chips={chips} onChange={setChips} {...props} />
|
|
50
|
+
<p className="ax:t-caption ax:text-muted-foreground">
|
|
51
|
+
{validCount} valid · {chips.length - validCount} invalid · press Enter or comma to add
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const Default: Story = {
|
|
58
|
+
render: (args) => <EmailChipsInputDemo {...args} />,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const WithValues: Story = {
|
|
62
|
+
render: (args) => (
|
|
63
|
+
<EmailChipsInputDemo
|
|
64
|
+
{...args}
|
|
65
|
+
initialChips={[
|
|
66
|
+
{ value: "alex@example.com", valid: true },
|
|
67
|
+
{ value: "sam@example.com", valid: true },
|
|
68
|
+
]}
|
|
69
|
+
/>
|
|
70
|
+
),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const WithInvalid: Story = {
|
|
74
|
+
render: (args) => (
|
|
75
|
+
<EmailChipsInputDemo
|
|
76
|
+
{...args}
|
|
77
|
+
initialChips={[
|
|
78
|
+
{ value: "yara@adres.ae", valid: true },
|
|
79
|
+
{ value: "not-an-email", valid: false },
|
|
80
|
+
]}
|
|
81
|
+
/>
|
|
82
|
+
),
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const Primary: Story = {
|
|
86
|
+
render: (args) => (
|
|
87
|
+
<EmailChipsInputDemo
|
|
88
|
+
{...args}
|
|
89
|
+
variant="primary"
|
|
90
|
+
initialChips={[
|
|
91
|
+
{ value: "yara@adres.ae", valid: true },
|
|
92
|
+
{ value: "ali@adres.ae", valid: true },
|
|
93
|
+
]}
|
|
94
|
+
/>
|
|
95
|
+
),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const Destructive: Story = {
|
|
99
|
+
render: (args) => (
|
|
100
|
+
<EmailChipsInputDemo
|
|
101
|
+
{...args}
|
|
102
|
+
variant="destructive"
|
|
103
|
+
initialChips={[
|
|
104
|
+
{ value: "revoke@example.com", valid: true },
|
|
105
|
+
{ value: "bad-address", valid: false },
|
|
106
|
+
]}
|
|
107
|
+
/>
|
|
108
|
+
),
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const Invalid: Story = {
|
|
112
|
+
render: (args) => (
|
|
113
|
+
<EmailChipsInputDemo
|
|
114
|
+
{...args}
|
|
115
|
+
invalid
|
|
116
|
+
initialChips={[
|
|
117
|
+
{ value: "yara@adres.ae", valid: true },
|
|
118
|
+
{ value: "not-an-email", valid: false },
|
|
119
|
+
]}
|
|
120
|
+
/>
|
|
121
|
+
),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const Disabled: Story = {
|
|
125
|
+
render: (args) => (
|
|
126
|
+
<EmailChipsInputDemo
|
|
127
|
+
{...args}
|
|
128
|
+
disabled
|
|
129
|
+
initialChips={[
|
|
130
|
+
{ value: "yara@adres.ae", valid: true },
|
|
131
|
+
{ value: "ali@adres.ae", valid: true },
|
|
132
|
+
]}
|
|
133
|
+
/>
|
|
134
|
+
),
|
|
135
|
+
}
|