@gunjo/ui 0.0.1-alpha.0 → 0.0.1-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.ja.md +90 -0
- package/README.md +52 -91
- package/package.json +47 -6
- package/src/components/display/Accordion.tsx +185 -0
- package/src/components/display/AccordionGroup.tsx +155 -0
- package/src/components/display/ActionDataTable.tsx +413 -0
- package/src/components/display/ActivityTimelineCard.tsx +483 -0
- package/src/components/display/AnalyticsCard.tsx +167 -0
- package/src/components/display/AssetCard.tsx +242 -0
- package/src/components/display/AssetGrid.tsx +164 -0
- package/src/components/display/Avatar.tsx +127 -0
- package/src/components/display/AvatarGroup.tsx +131 -0
- package/src/components/{atoms → display}/Badge.tsx +3 -3
- package/src/components/display/BarChart.tsx +247 -0
- package/src/components/{molecules → display}/Card.tsx +1 -1
- package/src/components/display/Carousel.tsx +593 -0
- package/src/components/display/ChartLegend.tsx +124 -0
- package/src/components/display/ChatMessage.tsx +382 -0
- package/src/components/display/ChoroplethMap.tsx +613 -0
- package/src/components/display/Code.tsx +42 -0
- package/src/components/display/CodeBlock.tsx +338 -0
- package/src/components/display/ColorSwatch.tsx +71 -0
- package/src/components/display/ConcentricProgressCard.tsx +545 -0
- package/src/components/display/DataTable.tsx +522 -0
- package/src/components/display/DistributionBar.tsx +102 -0
- package/src/components/display/DocNote.tsx +36 -0
- package/src/components/display/DonutChart.tsx +257 -0
- package/src/components/display/EmptyState.tsx +44 -0
- package/src/components/display/FileTree.tsx +180 -0
- package/src/components/display/GaugeChart.tsx +219 -0
- package/src/components/display/HeatmapChart.tsx +266 -0
- package/src/components/display/Icon.tsx +66 -0
- package/src/components/display/ImagePreview.tsx +140 -0
- package/src/components/{atoms → display}/Img.tsx +46 -12
- package/src/components/display/LabeledDonutCard.tsx +475 -0
- package/src/components/display/LineChart.tsx +464 -0
- package/src/components/{molecules → display}/List.tsx +20 -13
- package/src/components/display/MarkdownRenderer.tsx +157 -0
- package/src/components/display/MetadataList.tsx +81 -0
- package/src/components/display/MiniDistributionBarCard.tsx +314 -0
- package/src/components/display/PieChart.tsx +234 -0
- package/src/components/display/QuadrantMatrix.tsx +330 -0
- package/src/components/display/RadarChart.tsx +335 -0
- package/src/components/display/RadialBarChart.tsx +264 -0
- package/src/components/display/RetentionCohortCard.tsx +350 -0
- package/src/components/display/RibbonChart.tsx +618 -0
- package/src/components/display/SearchableAccordion.tsx +270 -0
- package/src/components/display/SegmentTimelineCard.tsx +452 -0
- package/src/components/display/SegmentedGaugeCard.tsx +607 -0
- package/src/components/display/Spacer.tsx +51 -0
- package/src/components/display/SparklineChart.tsx +394 -0
- package/src/components/display/StackedBarChart.tsx +393 -0
- package/src/components/display/Statistic.tsx +70 -0
- package/src/components/{molecules → display}/Table.tsx +22 -7
- package/src/components/display/Tag.tsx +80 -0
- package/src/components/display/TagEditor.tsx +141 -0
- package/src/components/display/Timeline.tsx +121 -0
- package/src/components/{atoms → display}/ToolPill.tsx +42 -18
- package/src/components/display/TreeView.tsx +226 -0
- package/src/components/display/chart-tooltip.tsx +423 -0
- package/src/components/display/chart-utils.ts +71 -0
- package/src/components/display/circular-chart-utils.ts +147 -0
- package/src/components/display/generated/default-variant-keys.ts +90 -0
- package/src/components/display/generated/variant-keys.ts +169 -0
- package/src/components/{atoms → feedback}/Alert.tsx +12 -5
- package/src/components/feedback/Banner.tsx +90 -0
- package/src/components/{molecules → feedback}/NotificationCenter.tsx +64 -31
- package/src/components/feedback/ProgressWidget.tsx +44 -0
- package/src/components/{atoms → feedback}/Spinner.tsx +2 -2
- package/src/components/{molecules → feedback}/StatusBar.tsx +4 -4
- package/src/components/feedback/StatusScreen.tsx +148 -0
- package/src/components/{molecules → feedback}/Stepper.tsx +10 -5
- package/src/components/feedback/Toast.tsx +108 -0
- package/src/components/feedback/ToastProvider.tsx +78 -0
- package/src/components/feedback/generated/default-variant-keys.ts +16 -0
- package/src/components/feedback/generated/variant-keys.ts +21 -0
- package/src/components/generated/component-manifest.ts +1568 -454
- package/src/components/generated/component-style-hints.ts +1958 -718
- package/src/components/{atoms → inputs}/ButtonVariants.ts +13 -3
- package/src/components/inputs/Calendar.tsx +212 -0
- package/src/components/inputs/ChatComposer.tsx +75 -0
- package/src/components/inputs/ChatInput.tsx +528 -0
- package/src/components/{atoms → inputs}/Checkbox.tsx +2 -2
- package/src/components/inputs/Combobox.tsx +175 -0
- package/src/components/inputs/CopyButton.tsx +187 -0
- package/src/components/inputs/DatePicker.tsx +519 -0
- package/src/components/inputs/DateRangePicker.tsx +878 -0
- package/src/components/inputs/EditableField.tsx +182 -0
- package/src/components/{organisms → inputs}/FileUploader.tsx +24 -9
- package/src/components/inputs/FilterButton.tsx +163 -0
- package/src/components/{molecules → inputs}/Form.tsx +20 -3
- package/src/components/{atoms → inputs}/Input.tsx +2 -0
- package/src/components/inputs/InputOTP.tsx +75 -0
- package/src/components/inputs/Mention.tsx +279 -0
- package/src/components/inputs/NumberInput.tsx +109 -0
- package/src/components/inputs/PasswordGroup.tsx +138 -0
- package/src/components/inputs/PasswordInput.tsx +74 -0
- package/src/components/inputs/PasswordRequirementList.tsx +96 -0
- package/src/components/inputs/PasswordStrengthMeter.tsx +93 -0
- package/src/components/inputs/PhoneInput.tsx +99 -0
- package/src/components/inputs/PostalCodeInput.tsx +98 -0
- package/src/components/inputs/RangeSlider.tsx +129 -0
- package/src/components/inputs/SearchInput.tsx +76 -0
- package/src/components/inputs/Select.tsx +39 -0
- package/src/components/{atoms → inputs}/Slider.tsx +18 -5
- package/src/components/{molecules → inputs}/SortButton.tsx +5 -2
- package/src/components/{atoms → inputs}/Switch.tsx +15 -4
- package/src/components/inputs/TagInput.tsx +114 -0
- package/src/components/{atoms → inputs}/Textarea.tsx +1 -0
- package/src/components/inputs/TimePicker.tsx +150 -0
- package/src/components/inputs/Toggle.tsx +48 -0
- package/src/components/{atoms → inputs}/ToggleGroup.tsx +2 -2
- package/src/components/inputs/TooltipButton.tsx +148 -0
- package/src/components/inputs/VoiceInputButton.tsx +317 -0
- package/src/components/inputs/calendar-holidays.ts +56 -0
- package/src/components/inputs/generated/default-variant-keys.ts +32 -0
- package/src/components/{atoms → inputs}/generated/variant-keys.ts +19 -27
- package/src/components/layout/AspectRatio.tsx +12 -0
- package/src/components/layout/AssetInspectorPanel.tsx +416 -0
- package/src/components/layout/Cluster.tsx +56 -0
- package/src/components/layout/CollapsiblePanelToggle.tsx +94 -0
- package/src/components/layout/Container.tsx +43 -0
- package/src/components/layout/DeviceFrame.tsx +227 -0
- package/src/components/layout/Grid.tsx +65 -0
- package/src/components/layout/HStack.tsx +73 -0
- package/src/components/{organisms → layout}/InspectorPanel.tsx +6 -5
- package/src/components/layout/MarqueeFrame.tsx +158 -0
- package/src/components/layout/Resizable.tsx +94 -0
- package/src/components/layout/ScrollArea.tsx +71 -0
- package/src/components/{organisms → layout}/SpatialCanvas.tsx +12 -7
- package/src/components/layout/VStack.tsx +69 -0
- package/src/components/layout/generated/default-variant-keys.ts +16 -0
- package/src/components/layout/generated/variant-keys.ts +21 -0
- package/src/components/{molecules → navigation}/Breadcrumb.tsx +5 -4
- package/src/components/navigation/Command.tsx +266 -0
- package/src/components/navigation/CommandPalette.tsx +83 -0
- package/src/components/navigation/DocumentPager.tsx +171 -0
- package/src/components/navigation/Footer.tsx +88 -0
- package/src/components/navigation/Header.tsx +80 -0
- package/src/components/{molecules → navigation}/Menubar.tsx +45 -12
- package/src/components/navigation/NavigationMenu.tsx +128 -0
- package/src/components/navigation/PageAside.tsx +84 -0
- package/src/components/{molecules → navigation}/Pagination.tsx +60 -7
- package/src/components/{organisms → navigation}/RightRail.tsx +1 -1
- package/src/components/navigation/Sidebar.tsx +223 -0
- package/src/components/navigation/SidebarItem.tsx +160 -0
- package/src/components/{molecules → navigation}/Tabs.tsx +2 -2
- package/src/components/navigation/TextLink.tsx +71 -0
- package/src/components/navigation/generated/default-variant-keys.ts +12 -0
- package/src/components/navigation/generated/variant-keys.ts +13 -0
- package/src/components/overlay/AIChatInput.tsx +5 -0
- package/src/components/overlay/AIChatMessage.tsx +6 -0
- package/src/components/overlay/AlertDialog.tsx +145 -0
- package/src/components/overlay/ChatPanel.tsx +180 -0
- package/src/components/{molecules → overlay}/ContextMenu.tsx +65 -29
- package/src/components/{molecules → overlay}/Dialog.tsx +21 -13
- package/src/components/overlay/Drawer.tsx +131 -0
- package/src/components/{molecules → overlay}/DropdownMenu.tsx +52 -17
- package/src/components/overlay/FloatingPanel.tsx +90 -0
- package/src/components/overlay/HoverCard.tsx +36 -0
- package/src/components/overlay/MediaLightbox.tsx +403 -0
- package/src/components/overlay/MediaPickerDialog.tsx +198 -0
- package/src/components/overlay/Modal.tsx +103 -0
- package/src/components/overlay/OnboardingFlow.tsx +172 -0
- package/src/components/overlay/Popover.tsx +36 -0
- package/src/components/overlay/ShareModal.tsx +324 -0
- package/src/components/{molecules → overlay}/Sheet.tsx +76 -19
- package/src/components/overlay/Tooltip.tsx +130 -0
- package/src/components/overlay/generated/default-variant-keys.ts +14 -0
- package/src/components/overlay/generated/variant-keys.ts +17 -0
- package/src/components/patterns/BlogTemplate.tsx +46 -0
- package/src/components/{templates → patterns}/DashboardTemplate.tsx +2 -2
- package/src/components/patterns/DocsTemplate.tsx +41 -0
- package/src/components/{templates → patterns}/MediaLibraryTemplate.tsx +1 -1
- package/src/components/patterns/OnboardingTemplate.tsx +32 -0
- package/src/components/patterns/PricingTemplate.tsx +106 -0
- package/src/globals.css +173 -22
- package/src/index.ts +177 -76
- package/tailwind-theme-extend.cjs +48 -3
- package/design/atoms-metadata.json +0 -82
- package/design/molecules-metadata.json +0 -130
- package/design/organisms-metadata.json +0 -38
- package/design/templates-metadata.json +0 -38
- package/src/components/atoms/Avatar.tsx +0 -57
- package/src/components/atoms/Select.tsx +0 -28
- package/src/components/atoms/generated/default-variant-keys.ts +0 -36
- package/src/components/molecules/AIChatInput.tsx +0 -140
- package/src/components/molecules/AIChatMessage.tsx +0 -109
- package/src/components/molecules/Accordion.tsx +0 -99
- package/src/components/molecules/Calendar.tsx +0 -60
- package/src/components/molecules/Carousel.tsx +0 -261
- package/src/components/molecules/Command.tsx +0 -152
- package/src/components/molecules/FilterButton.tsx +0 -133
- package/src/components/molecules/HoverCard.tsx +0 -29
- package/src/components/molecules/Modal.tsx +0 -66
- package/src/components/molecules/Popover.tsx +0 -31
- package/src/components/molecules/ProgressWidget.tsx +0 -40
- package/src/components/molecules/Resizable.tsx +0 -47
- package/src/components/molecules/ScrollArea.tsx +0 -48
- package/src/components/molecules/SidebarItem.tsx +0 -134
- package/src/components/molecules/Toast.tsx +0 -57
- package/src/components/molecules/Tooltip.tsx +0 -30
- package/src/components/molecules/generated/default-variant-keys.ts +0 -22
- package/src/components/molecules/generated/variant-keys.ts +0 -33
- package/src/components/organisms/CommandPalette.tsx +0 -58
- package/src/components/organisms/FloatingPanel.tsx +0 -46
- package/src/components/organisms/ShareModal.tsx +0 -182
- package/src/components/organisms/ToastProvider.tsx +0 -49
- /package/src/components/{atoms → display}/Kbd.tsx +0 -0
- /package/src/components/{atoms → display}/Separator.tsx +0 -0
- /package/src/components/{atoms → display}/Skeleton.tsx +0 -0
- /package/src/components/{atoms → feedback}/Progress.tsx +0 -0
- /package/src/components/{atoms → inputs}/Button.tsx +0 -0
- /package/src/components/{atoms → inputs}/Label.tsx +0 -0
- /package/src/components/{atoms → inputs}/RadioGroup.tsx +0 -0
- /package/src/components/{organisms → navigation}/AppRail.tsx +0 -0
- /package/src/components/{templates → patterns}/AuthTemplate.tsx +0 -0
- /package/src/components/{templates → patterns}/BannalyzeTemplate.tsx +0 -0
- /package/src/components/{templates → patterns}/ChatTemplate.tsx +0 -0
- /package/src/components/{templates → patterns}/EditorTemplate.tsx +0 -0
- /package/src/components/{templates → patterns}/KanbanTemplate.tsx +0 -0
- /package/src/components/{templates → patterns}/LandingTemplate.tsx +0 -0
- /package/src/components/{templates → patterns}/SettingsTemplate.tsx +0 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../lib/utils"
|
|
4
|
+
import { statusScreenDefaultVariantKey } from "./generated/default-variant-keys"
|
|
5
|
+
import type { StatusScreenVariantKey } from "./generated/variant-keys"
|
|
6
|
+
|
|
7
|
+
export type StatusScreenVariant = StatusScreenVariantKey
|
|
8
|
+
|
|
9
|
+
export interface StatusScreenProps
|
|
10
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
|
|
11
|
+
/** Selects sensible defaults for code / icon tone / title / description. */
|
|
12
|
+
variant?: StatusScreenVariant
|
|
13
|
+
/** Large code shown at the top, such as "404". Overrides the variant default. */
|
|
14
|
+
code?: React.ReactNode
|
|
15
|
+
/** Icon shown in place of the code for icon-led states. */
|
|
16
|
+
icon?: React.ReactNode
|
|
17
|
+
title?: React.ReactNode
|
|
18
|
+
description?: React.ReactNode
|
|
19
|
+
/** Primary action or action group, usually Button. */
|
|
20
|
+
action?: React.ReactNode
|
|
21
|
+
/** Optional muted detail block, such as request ID or diagnostic text. */
|
|
22
|
+
details?: React.ReactNode
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type VariantPreset = {
|
|
26
|
+
code?: string
|
|
27
|
+
tone: "default" | "destructive"
|
|
28
|
+
title: string
|
|
29
|
+
description: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const VARIANT_PRESETS: Record<StatusScreenVariantKey, VariantPreset> = {
|
|
33
|
+
"not-found": {
|
|
34
|
+
code: "404",
|
|
35
|
+
tone: "default",
|
|
36
|
+
title: "Page not found",
|
|
37
|
+
description:
|
|
38
|
+
"The page you are looking for doesn't exist or has been moved.",
|
|
39
|
+
},
|
|
40
|
+
error: {
|
|
41
|
+
code: "500",
|
|
42
|
+
tone: "destructive",
|
|
43
|
+
title: "Something went wrong",
|
|
44
|
+
description: "An unexpected error occurred. We've been notified.",
|
|
45
|
+
},
|
|
46
|
+
forbidden: {
|
|
47
|
+
code: "403",
|
|
48
|
+
tone: "destructive",
|
|
49
|
+
title: "Access denied",
|
|
50
|
+
description: "You don't have permission to view this page.",
|
|
51
|
+
},
|
|
52
|
+
offline: {
|
|
53
|
+
tone: "default",
|
|
54
|
+
title: "You're offline",
|
|
55
|
+
description: "Check your connection and try again.",
|
|
56
|
+
},
|
|
57
|
+
maintenance: {
|
|
58
|
+
tone: "default",
|
|
59
|
+
title: "Under maintenance",
|
|
60
|
+
description: "We'll be back shortly. Thanks for your patience.",
|
|
61
|
+
},
|
|
62
|
+
"coming-soon": {
|
|
63
|
+
tone: "default",
|
|
64
|
+
title: "Coming soon",
|
|
65
|
+
description: "This page isn't available yet. Check back soon.",
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Full-page status screen behind 404 / 500 / offline / 403 / maintenance /
|
|
71
|
+
* coming-soon states. Pick a variant for defaults, then override code, icon,
|
|
72
|
+
* title, description, action, or details as needed.
|
|
73
|
+
*/
|
|
74
|
+
const StatusScreen = React.forwardRef<HTMLDivElement, StatusScreenProps>(
|
|
75
|
+
(
|
|
76
|
+
{
|
|
77
|
+
className,
|
|
78
|
+
variant = statusScreenDefaultVariantKey,
|
|
79
|
+
code,
|
|
80
|
+
icon,
|
|
81
|
+
title,
|
|
82
|
+
description,
|
|
83
|
+
action,
|
|
84
|
+
details,
|
|
85
|
+
children,
|
|
86
|
+
...props
|
|
87
|
+
},
|
|
88
|
+
ref
|
|
89
|
+
) => {
|
|
90
|
+
const preset = VARIANT_PRESETS[variant] ?? VARIANT_PRESETS["not-found"]
|
|
91
|
+
const resolvedCode = code ?? preset.code
|
|
92
|
+
const resolvedTitle = title ?? preset.title
|
|
93
|
+
const resolvedDescription = description ?? preset.description
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
ref={ref}
|
|
98
|
+
data-variant={variant}
|
|
99
|
+
className={cn(
|
|
100
|
+
"flex min-h-[60vh] w-full flex-col items-center justify-center gap-4 p-8 text-center",
|
|
101
|
+
className
|
|
102
|
+
)}
|
|
103
|
+
{...props}
|
|
104
|
+
>
|
|
105
|
+
{icon ? (
|
|
106
|
+
<div
|
|
107
|
+
className={cn(
|
|
108
|
+
"flex h-16 w-16 items-center justify-center [&_svg]:h-16 [&_svg]:w-16",
|
|
109
|
+
preset.tone === "destructive"
|
|
110
|
+
? "text-destructive"
|
|
111
|
+
: "text-muted-foreground"
|
|
112
|
+
)}
|
|
113
|
+
aria-hidden="true"
|
|
114
|
+
>
|
|
115
|
+
{icon}
|
|
116
|
+
</div>
|
|
117
|
+
) : resolvedCode ? (
|
|
118
|
+
<p
|
|
119
|
+
className={cn(
|
|
120
|
+
"text-7xl font-extrabold tracking-tight",
|
|
121
|
+
preset.tone === "destructive"
|
|
122
|
+
? "text-destructive"
|
|
123
|
+
: "text-foreground"
|
|
124
|
+
)}
|
|
125
|
+
>
|
|
126
|
+
{resolvedCode}
|
|
127
|
+
</p>
|
|
128
|
+
) : null}
|
|
129
|
+
<p className="text-2xl font-semibold text-foreground">
|
|
130
|
+
{resolvedTitle}
|
|
131
|
+
</p>
|
|
132
|
+
<p className="max-w-md text-sm text-muted-foreground">
|
|
133
|
+
{resolvedDescription}
|
|
134
|
+
</p>
|
|
135
|
+
{action ? <div className="mt-2">{action}</div> : null}
|
|
136
|
+
{details ? (
|
|
137
|
+
<pre className="mt-4 max-w-xl overflow-x-auto rounded-md bg-muted p-3 text-left font-mono text-xs text-muted-foreground">
|
|
138
|
+
{details}
|
|
139
|
+
</pre>
|
|
140
|
+
) : null}
|
|
141
|
+
{children}
|
|
142
|
+
</div>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
StatusScreen.displayName = "StatusScreen"
|
|
147
|
+
|
|
148
|
+
export { StatusScreen }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
|
-
import { Check } from "
|
|
2
|
+
import { IconCheck as Check } from "@tabler/icons-react";
|
|
3
3
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
4
|
import { cn } from "../../lib/utils"
|
|
5
5
|
|
|
@@ -56,11 +56,12 @@ function StepCircle({ state, index }: { state: StepperState; index: number }) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
const Stepper = React.forwardRef<HTMLDivElement, StepperProps>(
|
|
59
|
-
({ className, orientation, steps, ...props }, ref) => {
|
|
59
|
+
({ className, orientation, steps, role = "list", ...props }, ref) => {
|
|
60
60
|
const isHorizontal = orientation !== "vertical"
|
|
61
61
|
return (
|
|
62
62
|
<div
|
|
63
63
|
ref={ref}
|
|
64
|
+
role={role}
|
|
64
65
|
className={cn(stepperVariants({ orientation }), "p-4", className)}
|
|
65
66
|
{...props}
|
|
66
67
|
>
|
|
@@ -69,15 +70,19 @@ const Stepper = React.forwardRef<HTMLDivElement, StepperProps>(
|
|
|
69
70
|
return (
|
|
70
71
|
<React.Fragment key={`${step.label}-${idx}`}>
|
|
71
72
|
<div
|
|
73
|
+
role="listitem"
|
|
74
|
+
aria-current={step.state === "current" ? "step" : undefined}
|
|
75
|
+
data-stepper-item
|
|
72
76
|
className={cn(
|
|
73
|
-
"flex items-center gap-2",
|
|
74
|
-
isHorizontal ? "flex-col" : "flex-row"
|
|
77
|
+
"flex min-w-0 items-center gap-2",
|
|
78
|
+
isHorizontal ? "w-16 shrink-0 flex-col text-center sm:w-20" : "flex-row"
|
|
75
79
|
)}
|
|
76
80
|
>
|
|
77
81
|
<StepCircle state={step.state} index={idx} />
|
|
78
82
|
<span
|
|
83
|
+
data-stepper-label
|
|
79
84
|
className={cn(
|
|
80
|
-
"text-xs font-medium",
|
|
85
|
+
"max-w-full text-xs font-medium leading-tight",
|
|
81
86
|
step.state === "upcoming"
|
|
82
87
|
? "text-muted-foreground"
|
|
83
88
|
: "text-foreground"
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import React, { useEffect, useState } from 'react';
|
|
3
|
+
import { IconAlertTriangle, IconCircleCheck, IconInfoCircle, IconX } from "@tabler/icons-react";
|
|
4
|
+
import { cn } from '../../lib/utils'; // Relative path to lib
|
|
5
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../overlay/Tooltip";
|
|
6
|
+
import type { ToastVariantKey } from "./generated/variant-keys";
|
|
7
|
+
import { toastDefaultVariantKey } from "./generated/default-variant-keys";
|
|
8
|
+
|
|
9
|
+
type TooltipContentProps = React.ComponentPropsWithoutRef<typeof TooltipContent>;
|
|
10
|
+
|
|
11
|
+
export type ToastType = ToastVariantKey;
|
|
12
|
+
|
|
13
|
+
export interface ToastProps {
|
|
14
|
+
message: string;
|
|
15
|
+
type?: ToastType;
|
|
16
|
+
isVisible: boolean;
|
|
17
|
+
onClose: () => void;
|
|
18
|
+
duration?: number;
|
|
19
|
+
placement?: "fixed" | "inline";
|
|
20
|
+
closeLabel?: string;
|
|
21
|
+
tooltipPortalContainer?: TooltipContentProps["portalContainer"];
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function Toast({
|
|
26
|
+
message,
|
|
27
|
+
type = toastDefaultVariantKey,
|
|
28
|
+
isVisible,
|
|
29
|
+
onClose,
|
|
30
|
+
duration = 3000,
|
|
31
|
+
placement = "fixed",
|
|
32
|
+
closeLabel = "Close notification",
|
|
33
|
+
tooltipPortalContainer,
|
|
34
|
+
className,
|
|
35
|
+
}: ToastProps) {
|
|
36
|
+
const [shouldRender, setShouldRender] = useState(isVisible);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (isVisible) {
|
|
40
|
+
const timer = setTimeout(() => {
|
|
41
|
+
onClose();
|
|
42
|
+
}, duration);
|
|
43
|
+
return () => clearTimeout(timer);
|
|
44
|
+
}
|
|
45
|
+
}, [isVisible, duration, onClose]);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (isVisible) {
|
|
49
|
+
setShouldRender(true);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!shouldRender) return;
|
|
54
|
+
|
|
55
|
+
const timer = setTimeout(() => {
|
|
56
|
+
setShouldRender(false);
|
|
57
|
+
}, 300);
|
|
58
|
+
|
|
59
|
+
return () => clearTimeout(timer);
|
|
60
|
+
}, [isVisible, shouldRender]);
|
|
61
|
+
|
|
62
|
+
if (!shouldRender) return null;
|
|
63
|
+
|
|
64
|
+
const icons: Record<ToastVariantKey, React.ReactNode> = {
|
|
65
|
+
success: <IconCircleCheck className="text-success" size={20} stroke={2} />,
|
|
66
|
+
error: <IconAlertTriangle className="text-destructive" size={20} stroke={2} />,
|
|
67
|
+
info: <IconInfoCircle className="text-info" size={20} stroke={2} />
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const bgColors: Record<ToastVariantKey, string> = {
|
|
71
|
+
success: 'bg-success-subtle border-success-border text-success-subtle-foreground',
|
|
72
|
+
error: 'bg-destructive-subtle border-destructive-border text-destructive-subtle-foreground',
|
|
73
|
+
info: 'bg-info-subtle border-info-border text-info-subtle-foreground'
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
role={type === "error" ? "alert" : "status"}
|
|
79
|
+
className={cn(
|
|
80
|
+
placement === "fixed" ? "fixed left-4 right-4 top-[72px] z-[9999] sm:left-auto sm:right-6" : "relative",
|
|
81
|
+
bgColors[type],
|
|
82
|
+
"flex w-full max-w-[calc(100vw-2rem)] flex-row items-start gap-3 rounded-xl border px-4 py-3 shadow-2xl duration-300 pointer-events-auto transform-gpu will-change-transform [animation-fill-mode:both] sm:w-[360px] sm:max-w-sm",
|
|
83
|
+
isVisible
|
|
84
|
+
? "animate-in slide-in-from-top-5 fade-in-0"
|
|
85
|
+
: "animate-out slide-out-to-top-5 fade-out-0",
|
|
86
|
+
className
|
|
87
|
+
)}
|
|
88
|
+
>
|
|
89
|
+
{icons[type]}
|
|
90
|
+
<p className="min-w-0 flex-1 break-words text-sm font-medium [overflow-wrap:anywhere]">{message}</p>
|
|
91
|
+
<TooltipProvider>
|
|
92
|
+
<Tooltip>
|
|
93
|
+
<TooltipTrigger asChild>
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
onClick={onClose}
|
|
97
|
+
aria-label={closeLabel}
|
|
98
|
+
className="ml-auto shrink-0 rounded-lg p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
99
|
+
>
|
|
100
|
+
<IconX size={14} stroke={2} />
|
|
101
|
+
</button>
|
|
102
|
+
</TooltipTrigger>
|
|
103
|
+
<TooltipContent portalContainer={tooltipPortalContainer}>{closeLabel}</TooltipContent>
|
|
104
|
+
</Tooltip>
|
|
105
|
+
</TooltipProvider>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import React, { createContext, useContext, useState, useCallback } from 'react';
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
import { Toast, ToastType } from './Toast';
|
|
5
|
+
|
|
6
|
+
interface ToastContextType {
|
|
7
|
+
showToast: (message: string, type: ToastType, duration?: number) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ToastContext = createContext<ToastContextType | undefined>(undefined);
|
|
11
|
+
|
|
12
|
+
export interface ToastProviderProps {
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
labels?: {
|
|
15
|
+
close?: string;
|
|
16
|
+
};
|
|
17
|
+
portalContainer?: HTMLElement | null;
|
|
18
|
+
placement?: "viewport" | "container";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const ToastProvider = ({ children, labels, portalContainer, placement = "viewport" }: ToastProviderProps) => {
|
|
22
|
+
const [toasts, setToasts] = useState<{ id: number, message: string, type: ToastType, duration: number, isVisible: boolean }[]>([]);
|
|
23
|
+
const [mounted, setMounted] = useState(false);
|
|
24
|
+
|
|
25
|
+
React.useEffect(() => {
|
|
26
|
+
setMounted(true);
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
const showToast = useCallback((message: string, type: ToastType, duration = 3000) => {
|
|
30
|
+
const id = Date.now();
|
|
31
|
+
setToasts(prev => [...prev, { id, message, type, duration, isVisible: true }]);
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
const closeToast = useCallback((id: number) => {
|
|
35
|
+
setToasts(prev => prev.map(t => t.id === id ? { ...t, isVisible: false } : t));
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
setToasts(prev => prev.filter(t => t.id !== id));
|
|
38
|
+
}, 300);
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
const target = placement === "container" ? portalContainer : portalContainer ?? (mounted ? document.body : null);
|
|
42
|
+
const toastStack = (
|
|
43
|
+
<div
|
|
44
|
+
className={
|
|
45
|
+
placement === "container"
|
|
46
|
+
? "absolute right-3 top-3 z-[100] flex w-[min(360px,calc(100%-1.5rem))] flex-col items-stretch gap-2 pointer-events-none"
|
|
47
|
+
: "fixed left-4 right-4 top-[72px] z-[100] flex flex-col items-stretch gap-2 pointer-events-none sm:left-auto sm:right-6 sm:w-[360px]"
|
|
48
|
+
}
|
|
49
|
+
>
|
|
50
|
+
{toasts.map(toast => (
|
|
51
|
+
<Toast
|
|
52
|
+
key={toast.id}
|
|
53
|
+
message={toast.message}
|
|
54
|
+
type={toast.type}
|
|
55
|
+
isVisible={toast.isVisible}
|
|
56
|
+
onClose={() => closeToast(toast.id)}
|
|
57
|
+
duration={toast.duration}
|
|
58
|
+
placement="inline"
|
|
59
|
+
closeLabel={labels?.close}
|
|
60
|
+
tooltipPortalContainer={portalContainer}
|
|
61
|
+
/>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<ToastContext.Provider value={{ showToast }}>
|
|
68
|
+
{children}
|
|
69
|
+
{target ? createPortal(toastStack, target) : null}
|
|
70
|
+
</ToastContext.Provider>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const useToast = () => {
|
|
75
|
+
const context = useContext(ToastContext);
|
|
76
|
+
if (!context) throw new Error('useToast must be used within a ToastProvider');
|
|
77
|
+
return context;
|
|
78
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// Generated by `npm run design:sync:components`. Do not edit manually.
|
|
3
|
+
|
|
4
|
+
import type { AlertVariantKey, SpinnerVariantKey, StatusScreenVariantKey, ToastVariantKey } from "./variant-keys";
|
|
5
|
+
|
|
6
|
+
export const alertDefaultVariantKey: AlertVariantKey = "default";
|
|
7
|
+
export const spinnerDefaultVariantKey: SpinnerVariantKey = "default";
|
|
8
|
+
export const statusScreenDefaultVariantKey: StatusScreenVariantKey = "not-found";
|
|
9
|
+
export const toastDefaultVariantKey: ToastVariantKey = "success";
|
|
10
|
+
|
|
11
|
+
export const feedbackDefaultVariantKeys = {
|
|
12
|
+
alert: alertDefaultVariantKey,
|
|
13
|
+
spinner: spinnerDefaultVariantKey,
|
|
14
|
+
statusScreen: statusScreenDefaultVariantKey,
|
|
15
|
+
toast: toastDefaultVariantKey,
|
|
16
|
+
} as const;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// Generated by `npm run design:sync:components`. Do not edit manually.
|
|
3
|
+
|
|
4
|
+
export const alertVariantKeys = ["default", "destructive", "info", "success", "warning"] as const;
|
|
5
|
+
export type AlertVariantKey = (typeof alertVariantKeys)[number];
|
|
6
|
+
|
|
7
|
+
export const spinnerVariantKeys = ["default", "lg", "sm"] as const;
|
|
8
|
+
export type SpinnerVariantKey = (typeof spinnerVariantKeys)[number];
|
|
9
|
+
|
|
10
|
+
export const statusScreenVariantKeys = ["coming-soon", "error", "forbidden", "maintenance", "not-found", "offline"] as const;
|
|
11
|
+
export type StatusScreenVariantKey = (typeof statusScreenVariantKeys)[number];
|
|
12
|
+
|
|
13
|
+
export const toastVariantKeys = ["error", "info", "success"] as const;
|
|
14
|
+
export type ToastVariantKey = (typeof toastVariantKeys)[number];
|
|
15
|
+
|
|
16
|
+
export const feedbackVariantKeys = {
|
|
17
|
+
alert: alertVariantKeys,
|
|
18
|
+
spinner: spinnerVariantKeys,
|
|
19
|
+
statusScreen: statusScreenVariantKeys,
|
|
20
|
+
toast: toastVariantKeys,
|
|
21
|
+
} as const;
|