aplance-ui 1.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/LICENSE +21 -0
- package/README.md +29 -0
- package/dist/api.d.ts +175 -0
- package/dist/api.js +1954 -0
- package/dist/api.js.map +1 -0
- package/dist/frameworks/astro/astro.config.snippet.md +23 -0
- package/dist/frameworks/astro/postcss.config.mjs.template +20 -0
- package/dist/frameworks/astro/vite-inline.snippet.md +22 -0
- package/dist/frameworks/electron-vite/vite.config.snippet.md +44 -0
- package/dist/frameworks/next/layout-import.snippet.md +19 -0
- package/dist/frameworks/next/next.config.snippet.md +15 -0
- package/dist/frameworks/next/postcss.config.js.template +16 -0
- package/dist/frameworks/tanstack-start/postcss.config.ts.template +17 -0
- package/dist/frameworks/tanstack-start/vite-inline.snippet.md +22 -0
- package/dist/frameworks/vite-react/postcss.config.ts.template +16 -0
- package/dist/frameworks/vite-react/vite-inline.snippet.md +20 -0
- package/dist/frameworks/vite-react/vite.config.ts.template +48 -0
- package/dist/index.js +2047 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
- package/registry/components/primitives/icon/icon.module.css +63 -0
- package/registry/components/primitives/icon/icon.tsx +53 -0
- package/registry/components/primitives/modal/modal-context.ts +19 -0
- package/registry/components/primitives/modal/modal.module.css +415 -0
- package/registry/components/primitives/modal/modal.tsx +550 -0
- package/registry/components/primitives/popover/popover-context.ts +28 -0
- package/registry/components/primitives/popover/popover.module.css +28 -0
- package/registry/components/primitives/popover/popover.tsx +465 -0
- package/registry/components/ui/accordion/accordion.module.css +208 -0
- package/registry/components/ui/accordion/accordion.tsx +327 -0
- package/registry/components/ui/avatar/avatar.module.css +32 -0
- package/registry/components/ui/avatar/avatar.tsx +37 -0
- package/registry/components/ui/badge/badge.module.css +103 -0
- package/registry/components/ui/badge/badge.tsx +29 -0
- package/registry/components/ui/button/button.module.css +216 -0
- package/registry/components/ui/button/button.tsx +133 -0
- package/registry/components/ui/chart/chart.module.css +168 -0
- package/registry/components/ui/chart/chart.tsx +1293 -0
- package/registry/components/ui/chart/types.ts +210 -0
- package/registry/components/ui/chart/utils.ts +59 -0
- package/registry/components/ui/copy-button/copy-button.module.css +21 -0
- package/registry/components/ui/copy-button/copy-button.tsx +123 -0
- package/registry/components/ui/countdown/countdown.module.css +5 -0
- package/registry/components/ui/countdown/countdown.tsx +152 -0
- package/registry/components/ui/data-table/checkbox-column.tsx +59 -0
- package/registry/components/ui/data-table/data-table.module.css +594 -0
- package/registry/components/ui/data-table/data-table.tsx +1109 -0
- package/registry/components/ui/data-table/filter-bar.tsx +33 -0
- package/registry/components/ui/data-table/pagination-bar.tsx +125 -0
- package/registry/components/ui/data-table/types.ts +594 -0
- package/registry/components/ui/data-table/useDataTable.tsx +573 -0
- package/registry/components/ui/data-table/utils.ts +445 -0
- package/registry/components/ui/empty-state/empty-state.module.css +26 -0
- package/registry/components/ui/empty-state/empty-state.tsx +32 -0
- package/registry/components/ui/input-fields/color-picker/color-palette.module.css +102 -0
- package/registry/components/ui/input-fields/color-picker/color-palette.tsx +92 -0
- package/registry/components/ui/input-fields/color-picker/color-picker.module.css +29 -0
- package/registry/components/ui/input-fields/color-picker/color-picker.tsx +168 -0
- package/registry/components/ui/input-fields/common/field-message/field-message.module.css +28 -0
- package/registry/components/ui/input-fields/common/field-message/field-message.tsx +39 -0
- package/registry/components/ui/input-fields/common/input-field-common.module.css +318 -0
- package/registry/components/ui/input-fields/common/input-field-native.module.css +267 -0
- package/registry/components/ui/input-fields/common/picker-popover-common.module.css +40 -0
- package/registry/components/ui/input-fields/common/select-button/select-button.module.css +9 -0
- package/registry/components/ui/input-fields/common/select-button/select-button.tsx +171 -0
- package/registry/components/ui/input-fields/common/types.ts +38 -0
- package/registry/components/ui/input-fields/custom-select/custom-select.module.css +56 -0
- package/registry/components/ui/input-fields/custom-select/custom-select.tsx +403 -0
- package/registry/components/ui/input-fields/date-picker/calendar.module.css +311 -0
- package/registry/components/ui/input-fields/date-picker/calendar.tsx +186 -0
- package/registry/components/ui/input-fields/date-picker/date-picker.module.css +57 -0
- package/registry/components/ui/input-fields/date-picker/date-picker.tsx +945 -0
- package/registry/components/ui/input-fields/date-picker/types.ts +102 -0
- package/registry/components/ui/input-fields/date-picker/utils.ts +236 -0
- package/registry/components/ui/input-fields/emoji-picker/emoji-palette.module.css +90 -0
- package/registry/components/ui/input-fields/emoji-picker/emoji-palette.tsx +168 -0
- package/registry/components/ui/input-fields/emoji-picker/emoji-picker.module.css +8 -0
- package/registry/components/ui/input-fields/emoji-picker/emoji-picker.tsx +102 -0
- package/registry/components/ui/input-fields/emoji-picker/utils.ts +505 -0
- package/registry/components/ui/input-fields/field-group/field-group.module.css +86 -0
- package/registry/components/ui/input-fields/field-group/field-group.tsx +86 -0
- package/registry/components/ui/input-fields/input/input.tsx +319 -0
- package/registry/components/ui/input-fields/media-input/kind-icons.tsx +62 -0
- package/registry/components/ui/input-fields/media-input/media-input.module.css +294 -0
- package/registry/components/ui/input-fields/media-input/media-input.tsx +327 -0
- package/registry/components/ui/input-fields/media-input/media-preview.tsx +101 -0
- package/registry/components/ui/input-fields/media-input/types.ts +129 -0
- package/registry/components/ui/input-fields/media-input/useMediaUpload.tsx +302 -0
- package/registry/components/ui/input-fields/media-input/utils.ts +325 -0
- package/registry/components/ui/input-fields/otp-input/otp-input.module.css +24 -0
- package/registry/components/ui/input-fields/otp-input/otp-input.tsx +291 -0
- package/registry/components/ui/input-fields/select/select.tsx +118 -0
- package/registry/components/ui/input-fields/switch/switch.module.css +70 -0
- package/registry/components/ui/input-fields/switch/switch.tsx +59 -0
- package/registry/components/ui/input-fields/textarea/textarea.tsx +133 -0
- package/registry/components/ui/input-fields/time-picker/time-picker.module.css +57 -0
- package/registry/components/ui/input-fields/time-picker/time-picker.tsx +437 -0
- package/registry/components/ui/loader/loader.module.css +156 -0
- package/registry/components/ui/loader/loader.tsx +102 -0
- package/registry/components/ui/stepper/stepper.module.css +294 -0
- package/registry/components/ui/stepper/stepper.tsx +197 -0
- package/registry/components/ui/tabs/tabs.module.css +150 -0
- package/registry/components/ui/tabs/tabs.tsx +134 -0
- package/registry/components/ui/toast/toast.module.css +159 -0
- package/registry/components/ui/toast/toast.tsx +104 -0
- package/registry/components/ui/tooltip/tooltip.module.css +82 -0
- package/registry/components/ui/tooltip/tooltip.tsx +72 -0
- package/registry/global.d.ts +4 -0
- package/registry/hooks/useFocusOutside.tsx +33 -0
- package/registry/hooks/useResizeObserver.tsx +32 -0
- package/registry/lib/dom.ts +25 -0
- package/registry/lib/string.ts +23 -0
- package/registry/registry.json +719 -0
- package/registry/styles/app.css +240 -0
- package/registry/styles/components.css +78 -0
- package/registry/styles/fonts/geist-regular.woff2 +0 -0
- package/registry/styles/globals.css +7 -0
- package/registry/styles/media.css +25 -0
- package/registry/styles/utilities.css +90 -0
- package/registry/themes/colors/blue.css +37 -0
- package/registry/themes/colors/green.css +37 -0
- package/registry/themes/colors/rose.css +37 -0
- package/registry/themes/colors/slate.css +37 -0
- package/registry/themes/colors/stone.css +37 -0
- package/registry/themes/colors/violet.css +37 -0
- package/registry/themes/colors/zinc.css +37 -0
- package/registry/themes/radius/pill.css +8 -0
- package/registry/themes/radius/rounded.css +8 -0
- package/registry/themes/radius/sharp.css +8 -0
- package/registry/themes/spacing/compact.css +17 -0
- package/registry/themes/spacing/spacious.css +17 -0
- package/registry/tsconfig.json +25 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/* ─────────────────────────────────────────────
|
|
2
|
+
Tabs – root
|
|
3
|
+
───────────────────────────────────────────── */
|
|
4
|
+
.root {
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
gap: var(--spacing-6);
|
|
8
|
+
width: 100%;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* ─────────────────────────────────────────────
|
|
12
|
+
Tab list
|
|
13
|
+
───────────────────────────────────────────── */
|
|
14
|
+
.list {
|
|
15
|
+
position: relative;
|
|
16
|
+
display: inline-flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
|
|
19
|
+
/* style1 – pill container */
|
|
20
|
+
&.list_style1 {
|
|
21
|
+
background: var(--color-muted);
|
|
22
|
+
border-radius: var(--radius-md);
|
|
23
|
+
padding: var(--spacing-1);
|
|
24
|
+
gap: var(--spacing-1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* style2 – underline container */
|
|
28
|
+
&.list_style2 {
|
|
29
|
+
border-bottom: var(--border-1) solid var(--color-border);
|
|
30
|
+
gap: var(--spacing-6);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* ─────────────────────────────────────────────
|
|
35
|
+
Animated sliding indicator
|
|
36
|
+
───────────────────────────────────────────── */
|
|
37
|
+
.indicator {
|
|
38
|
+
position: absolute;
|
|
39
|
+
top: 0;
|
|
40
|
+
height: 100%;
|
|
41
|
+
pointer-events: none;
|
|
42
|
+
z-index: 0;
|
|
43
|
+
transition:
|
|
44
|
+
left var(--transition-base),
|
|
45
|
+
width var(--transition-base);
|
|
46
|
+
will-change: left, width;
|
|
47
|
+
|
|
48
|
+
/* style1 – filled pill */
|
|
49
|
+
&.indicator_style1 {
|
|
50
|
+
top: var(--spacing-1);
|
|
51
|
+
height: calc(100% - var(--spacing-1) * 2);
|
|
52
|
+
border-radius: calc(var(--radius-md) - var(--spacing-1));
|
|
53
|
+
background: var(--color-background);
|
|
54
|
+
box-shadow: var(--shadow-sm);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* style2 – bottom line */
|
|
58
|
+
&.indicator_style2 {
|
|
59
|
+
top: auto;
|
|
60
|
+
bottom: -1px;
|
|
61
|
+
height: var(--border-2);
|
|
62
|
+
border-radius: var(--radius-full);
|
|
63
|
+
background: var(--color-foreground);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* ─────────────────────────────────────────────
|
|
68
|
+
Tab button – shared base
|
|
69
|
+
───────────────────────────────────────────── */
|
|
70
|
+
.tab {
|
|
71
|
+
position: relative;
|
|
72
|
+
z-index: 1;
|
|
73
|
+
display: inline-flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
justify-content: center;
|
|
76
|
+
font-family: var(--font-family-1);
|
|
77
|
+
font-size: var(--font-size-sm);
|
|
78
|
+
line-height: var(--line-height-sm);
|
|
79
|
+
color: var(--color-muted-foreground);
|
|
80
|
+
background: transparent;
|
|
81
|
+
cursor: pointer;
|
|
82
|
+
border: none;
|
|
83
|
+
outline: none;
|
|
84
|
+
white-space: nowrap;
|
|
85
|
+
transition: color var(--transition-base);
|
|
86
|
+
user-select: none;
|
|
87
|
+
-webkit-user-select: none;
|
|
88
|
+
|
|
89
|
+
&:disabled {
|
|
90
|
+
opacity: 0.5;
|
|
91
|
+
cursor: not-allowed;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* style1 – pill tab */
|
|
95
|
+
&.tab_style1 {
|
|
96
|
+
padding: var(--spacing-3) var(--spacing-6);
|
|
97
|
+
border-radius: calc(var(--radius-md) - var(--spacing-1));
|
|
98
|
+
min-height: var(--spacing-8);
|
|
99
|
+
|
|
100
|
+
&:focus-visible {
|
|
101
|
+
box-shadow: var(--shadow-ring);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
&:hover:not(:disabled):not(.tab_active) {
|
|
105
|
+
color: var(--color-foreground);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
&.tab_active {
|
|
109
|
+
color: var(--color-foreground);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* style2 – underline tab */
|
|
114
|
+
&.tab_style2 {
|
|
115
|
+
padding: var(--spacing-3) var(--spacing-1);
|
|
116
|
+
min-height: var(--spacing-11);
|
|
117
|
+
|
|
118
|
+
&:focus-visible {
|
|
119
|
+
border-radius: var(--radius-sm);
|
|
120
|
+
box-shadow: var(--shadow-ring);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
&:hover:not(:disabled):not(.tab_active) {
|
|
124
|
+
color: var(--color-foreground);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
&.tab_active {
|
|
128
|
+
color: var(--color-foreground);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* ─────────────────────────────────────────────
|
|
134
|
+
Tab panel
|
|
135
|
+
───────────────────────────────────────────── */
|
|
136
|
+
.panel {
|
|
137
|
+
outline: none;
|
|
138
|
+
animation: panel_fade_in var(--transition-base) ease both;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@keyframes panel_fade_in {
|
|
142
|
+
from {
|
|
143
|
+
opacity: 0;
|
|
144
|
+
transform: translateY(4px);
|
|
145
|
+
}
|
|
146
|
+
to {
|
|
147
|
+
opacity: 1;
|
|
148
|
+
transform: translateY(0);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useState,
|
|
3
|
+
useRef,
|
|
4
|
+
useEffect,
|
|
5
|
+
useId,
|
|
6
|
+
useCallback,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
type ComponentProps,
|
|
9
|
+
} from "react";
|
|
10
|
+
import clsx from "clsx";
|
|
11
|
+
import { useResizeObserver } from "../../../hooks/useResizeObserver";
|
|
12
|
+
import s from "./tabs.module.css";
|
|
13
|
+
|
|
14
|
+
// Variants & Types
|
|
15
|
+
export const variants = ["style1", "style2"] as const;
|
|
16
|
+
export type Variant = (typeof variants)[number];
|
|
17
|
+
|
|
18
|
+
export type TabItem = {
|
|
19
|
+
id: string;
|
|
20
|
+
label: ReactNode;
|
|
21
|
+
content?: ReactNode;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type TabsProps = {
|
|
26
|
+
tabs: TabItem[];
|
|
27
|
+
defaultTab?: string;
|
|
28
|
+
activeTab?: string;
|
|
29
|
+
onTabChange?: (id: string) => void;
|
|
30
|
+
variant?: Variant;
|
|
31
|
+
className?: string;
|
|
32
|
+
} & Omit<ComponentProps<"div">, "onChange">;
|
|
33
|
+
|
|
34
|
+
export const Tabs = ({
|
|
35
|
+
tabs,
|
|
36
|
+
defaultTab,
|
|
37
|
+
activeTab: controlledActive,
|
|
38
|
+
onTabChange,
|
|
39
|
+
variant = "style1",
|
|
40
|
+
className,
|
|
41
|
+
...rest
|
|
42
|
+
}: TabsProps) => {
|
|
43
|
+
const uid = useId();
|
|
44
|
+
const isControlled = controlledActive !== undefined;
|
|
45
|
+
|
|
46
|
+
const [internalActive, setInternalActive] = useState<string>(
|
|
47
|
+
defaultTab ?? tabs.find((t) => !t.disabled)?.id ?? "",
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const activeId = isControlled ? controlledActive : internalActive;
|
|
51
|
+
|
|
52
|
+
// Sliding indicator state
|
|
53
|
+
const listRef = useRef<HTMLDivElement>(null);
|
|
54
|
+
const [indicator, setIndicator] = useState({ left: 0, width: 0 });
|
|
55
|
+
|
|
56
|
+
const updateIndicator = useCallback((id: string) => {
|
|
57
|
+
const list = listRef.current;
|
|
58
|
+
if (!list) return;
|
|
59
|
+
const btn = list.querySelector<HTMLButtonElement>(`[data-tab-id="${id}"]`);
|
|
60
|
+
if (!btn) return;
|
|
61
|
+
const listRect = list.getBoundingClientRect();
|
|
62
|
+
const btnRect = btn.getBoundingClientRect();
|
|
63
|
+
setIndicator({
|
|
64
|
+
left: btnRect.left - listRect.left,
|
|
65
|
+
width: btnRect.width,
|
|
66
|
+
});
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
updateIndicator(activeId);
|
|
71
|
+
}, [activeId, updateIndicator]);
|
|
72
|
+
|
|
73
|
+
useResizeObserver(listRef, () => updateIndicator(activeId));
|
|
74
|
+
const handleSelect = (id: string) => {
|
|
75
|
+
if (!isControlled) setInternalActive(id);
|
|
76
|
+
onTabChange?.(id);
|
|
77
|
+
};
|
|
78
|
+
const activeContent = tabs.find((t) => t.id === activeId)?.content;
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className={clsx(s.root, className)} {...rest}>
|
|
82
|
+
{/* Tab List */}
|
|
83
|
+
<div
|
|
84
|
+
ref={listRef}
|
|
85
|
+
role="tablist"
|
|
86
|
+
className={clsx(s.list, s[`list_${variant}`])}
|
|
87
|
+
aria-orientation="horizontal"
|
|
88
|
+
>
|
|
89
|
+
{/* Animated indicator */}
|
|
90
|
+
<span
|
|
91
|
+
className={clsx(s.indicator, s[`indicator_${variant}`])}
|
|
92
|
+
style={{ left: indicator.left, width: indicator.width }}
|
|
93
|
+
aria-hidden="true"
|
|
94
|
+
/>
|
|
95
|
+
|
|
96
|
+
{tabs.map((tab) => {
|
|
97
|
+
const isActive = tab.id === activeId;
|
|
98
|
+
return (
|
|
99
|
+
<button
|
|
100
|
+
key={tab.id}
|
|
101
|
+
role="tab"
|
|
102
|
+
id={`${uid}-tab-${tab.id}`}
|
|
103
|
+
aria-controls={`${uid}-panel-${tab.id}`}
|
|
104
|
+
aria-selected={isActive}
|
|
105
|
+
disabled={tab.disabled}
|
|
106
|
+
data-tab-id={tab.id}
|
|
107
|
+
className={clsx(
|
|
108
|
+
s.tab,
|
|
109
|
+
s[`tab_${variant}`],
|
|
110
|
+
isActive && s.tab_active,
|
|
111
|
+
)}
|
|
112
|
+
onClick={() => !tab.disabled && handleSelect(tab.id)}
|
|
113
|
+
>
|
|
114
|
+
{tab.label}
|
|
115
|
+
</button>
|
|
116
|
+
);
|
|
117
|
+
})}
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* Tab Panel */}
|
|
121
|
+
{activeContent !== undefined && (
|
|
122
|
+
<div
|
|
123
|
+
role="tabpanel"
|
|
124
|
+
id={`${uid}-panel-${activeId}`}
|
|
125
|
+
aria-labelledby={`${uid}-tab-${activeId}`}
|
|
126
|
+
className={s.panel}
|
|
127
|
+
key={activeId}
|
|
128
|
+
>
|
|
129
|
+
{activeContent}
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/* Toast - sonner runs with `unstyled: true` and is left on its default
|
|
2
|
+
`theme="light"`, so none of sonner's dark-only rules ever match. All
|
|
3
|
+
surface styling is ours and adapts to the host app's `[data-theme]` via
|
|
4
|
+
our CSS tokens; sonner only drives positioning, mount/swipe and stacking. */
|
|
5
|
+
|
|
6
|
+
.item {
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
width: 100%;
|
|
10
|
+
gap: var(--spacing-6);
|
|
11
|
+
padding: var(--spacing-7) var(--spacing-8);
|
|
12
|
+
background: var(--color-background);
|
|
13
|
+
color: var(--color-foreground);
|
|
14
|
+
border: var(--border-1) solid var(--color-border);
|
|
15
|
+
border-radius: var(--radius-md);
|
|
16
|
+
box-shadow: var(--shadow-md);
|
|
17
|
+
font-family: var(--font-family-1);
|
|
18
|
+
font-size: var(--font-size-sm);
|
|
19
|
+
line-height: var(--line-height-md);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.icon {
|
|
23
|
+
position: relative;
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
flex-shrink: 0;
|
|
28
|
+
width: var(--spacing-9);
|
|
29
|
+
height: var(--spacing-9);
|
|
30
|
+
color: var(--color-muted-foreground);
|
|
31
|
+
/* Sonner's loading bars use `var(--gray11)`; remap locally so the spinner
|
|
32
|
+
tracks the icon's text colour (and any variant override below). */
|
|
33
|
+
--gray11: currentColor;
|
|
34
|
+
|
|
35
|
+
svg {
|
|
36
|
+
width: 100%;
|
|
37
|
+
height: 100%;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Sonner centres the loader at icon centre with 0×0 size, then anchors a
|
|
41
|
+
fixed 16px loading-wrapper at top-left - visually the spinner ends up
|
|
42
|
+
in the icon's bottom-right quadrant. Make the loader fill `.icon` and
|
|
43
|
+
re-centre the wrapper at our icon size. */
|
|
44
|
+
:global(.sonner-loader) {
|
|
45
|
+
inset: 0;
|
|
46
|
+
transform: none;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
:global(.sonner-loading-wrapper) {
|
|
50
|
+
--size: var(--spacing-9);
|
|
51
|
+
inset: auto;
|
|
52
|
+
top: 50%;
|
|
53
|
+
left: 50%;
|
|
54
|
+
transform: translate(-50%, -50%);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.content {
|
|
59
|
+
display: flex;
|
|
60
|
+
flex-direction: column;
|
|
61
|
+
gap: var(--spacing-2);
|
|
62
|
+
flex: 1;
|
|
63
|
+
min-width: 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.title {
|
|
67
|
+
font-weight: var(--font-weight-medium);
|
|
68
|
+
color: var(--color-foreground);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.description {
|
|
72
|
+
font-size: var(--font-size-xs);
|
|
73
|
+
line-height: var(--line-height-md);
|
|
74
|
+
color: var(--color-foreground);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.action_button {
|
|
78
|
+
font-family: var(--font-family-1);
|
|
79
|
+
font-size: var(--font-size-xs);
|
|
80
|
+
font-weight: var(--font-weight-medium);
|
|
81
|
+
height: var(--spacing-10);
|
|
82
|
+
padding: 0 var(--spacing-6);
|
|
83
|
+
border-radius: var(--radius-sm);
|
|
84
|
+
border: var(--border-0);
|
|
85
|
+
background: var(--color-foreground);
|
|
86
|
+
color: var(--color-background);
|
|
87
|
+
cursor: pointer;
|
|
88
|
+
flex-shrink: 0;
|
|
89
|
+
transition: opacity var(--transition-base);
|
|
90
|
+
|
|
91
|
+
&:hover {
|
|
92
|
+
opacity: 0.85;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.cancel_button {
|
|
97
|
+
font-family: var(--font-family-1);
|
|
98
|
+
font-size: var(--font-size-xs);
|
|
99
|
+
height: var(--spacing-10);
|
|
100
|
+
padding: 0 var(--spacing-6);
|
|
101
|
+
border-radius: var(--radius-sm);
|
|
102
|
+
border: var(--border-0);
|
|
103
|
+
background: var(--color-muted);
|
|
104
|
+
color: var(--color-foreground);
|
|
105
|
+
cursor: pointer;
|
|
106
|
+
flex-shrink: 0;
|
|
107
|
+
transition: background var(--transition-base);
|
|
108
|
+
|
|
109
|
+
&:hover {
|
|
110
|
+
background: var(--color-secondary-muted);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.close_button {
|
|
115
|
+
position: absolute;
|
|
116
|
+
top: 0;
|
|
117
|
+
left: var(--toast-close-button-start, 0);
|
|
118
|
+
right: var(--toast-close-button-end, unset);
|
|
119
|
+
width: var(--spacing-10);
|
|
120
|
+
height: var(--spacing-10);
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
justify-content: center;
|
|
124
|
+
padding: 0;
|
|
125
|
+
background: var(--color-background);
|
|
126
|
+
color: var(--color-muted-foreground);
|
|
127
|
+
border: var(--border-1) solid var(--color-border);
|
|
128
|
+
border-radius: var(--radius-full);
|
|
129
|
+
transform: var(--toast-close-button-transform, translate(-35%, -35%));
|
|
130
|
+
cursor: pointer;
|
|
131
|
+
z-index: 1;
|
|
132
|
+
transition:
|
|
133
|
+
color var(--transition-base),
|
|
134
|
+
background var(--transition-base),
|
|
135
|
+
border-color var(--transition-base);
|
|
136
|
+
|
|
137
|
+
&:hover {
|
|
138
|
+
background: var(--color-muted);
|
|
139
|
+
color: var(--color-foreground);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Variants - colour the icon only; the toast surface stays neutral so both
|
|
144
|
+
themes render consistently without rich-colour tinting. */
|
|
145
|
+
.variant_success .icon {
|
|
146
|
+
color: var(--color-success-foreground);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.variant_error .icon {
|
|
150
|
+
color: var(--color-error-foreground);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.variant_warning .icon {
|
|
154
|
+
color: var(--color-warning-foreground);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.variant_info .icon {
|
|
158
|
+
color: var(--color-info-foreground);
|
|
159
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Toaster as SonnerToaster,
|
|
3
|
+
toast as sonnerToast,
|
|
4
|
+
type ExternalToast,
|
|
5
|
+
} from "sonner";
|
|
6
|
+
import s from "./toast.module.css";
|
|
7
|
+
|
|
8
|
+
export const variants = ["default", "success", "error", "warning", "info"] as const;
|
|
9
|
+
export type ToastVariant = (typeof variants)[number];
|
|
10
|
+
|
|
11
|
+
export const positions = [
|
|
12
|
+
"top-left",
|
|
13
|
+
"top-right",
|
|
14
|
+
"bottom-left",
|
|
15
|
+
"bottom-right",
|
|
16
|
+
"top-center",
|
|
17
|
+
"bottom-center",
|
|
18
|
+
] as const;
|
|
19
|
+
export type ToastPosition = (typeof positions)[number];
|
|
20
|
+
|
|
21
|
+
export type ToasterProps = {
|
|
22
|
+
position?: ToastPosition;
|
|
23
|
+
visibleToasts?: number;
|
|
24
|
+
closeButton?: boolean;
|
|
25
|
+
expand?: boolean;
|
|
26
|
+
duration?: number;
|
|
27
|
+
gap?: number;
|
|
28
|
+
offset?: string | number;
|
|
29
|
+
className?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Visual theming follows the host app's `[data-theme]` attribute through our
|
|
34
|
+
* CSS tokens. We don't forward sonner's `theme` prop - leaving sonner on its
|
|
35
|
+
* default ("light") means its dark-only rules for description and close
|
|
36
|
+
* button never match, so our class styling is the single source of truth.
|
|
37
|
+
*/
|
|
38
|
+
export const Toaster = ({
|
|
39
|
+
position = "bottom-right",
|
|
40
|
+
visibleToasts = 3,
|
|
41
|
+
closeButton = true,
|
|
42
|
+
expand = false,
|
|
43
|
+
duration = 4000,
|
|
44
|
+
gap = 8,
|
|
45
|
+
offset,
|
|
46
|
+
className,
|
|
47
|
+
}: ToasterProps) => (
|
|
48
|
+
<SonnerToaster
|
|
49
|
+
position={position}
|
|
50
|
+
visibleToasts={visibleToasts}
|
|
51
|
+
closeButton={closeButton}
|
|
52
|
+
expand={expand}
|
|
53
|
+
duration={duration}
|
|
54
|
+
gap={gap}
|
|
55
|
+
offset={offset}
|
|
56
|
+
className={className}
|
|
57
|
+
toastOptions={{
|
|
58
|
+
unstyled: true,
|
|
59
|
+
classNames: {
|
|
60
|
+
toast: s.item,
|
|
61
|
+
title: s.title,
|
|
62
|
+
description: s.description,
|
|
63
|
+
icon: s.icon,
|
|
64
|
+
content: s.content,
|
|
65
|
+
actionButton: s.action_button,
|
|
66
|
+
cancelButton: s.cancel_button,
|
|
67
|
+
closeButton: s.close_button,
|
|
68
|
+
success: s.variant_success,
|
|
69
|
+
error: s.variant_error,
|
|
70
|
+
warning: s.variant_warning,
|
|
71
|
+
info: s.variant_info,
|
|
72
|
+
},
|
|
73
|
+
}}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
type ToastOptions = ExternalToast & { variant?: ToastVariant };
|
|
78
|
+
type ToastMessage = string | React.ReactNode;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Show a toast notification.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* toast("Copied to clipboard");
|
|
85
|
+
* toast.success("Profile saved");
|
|
86
|
+
* toast.error("Upload failed", { description: "File is too large" });
|
|
87
|
+
* toast.promise(savePromise, { loading: "Saving...", success: "Saved", error: "Save failed" });
|
|
88
|
+
*/
|
|
89
|
+
function toast(message: ToastMessage, options?: ToastOptions): string | number {
|
|
90
|
+
const { variant, ...rest } = options ?? {};
|
|
91
|
+
if (variant && variant !== "default") return sonnerToast[variant](message, rest);
|
|
92
|
+
return sonnerToast(message, rest);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
toast.success = sonnerToast.success;
|
|
96
|
+
toast.error = sonnerToast.error;
|
|
97
|
+
toast.warning = sonnerToast.warning;
|
|
98
|
+
toast.info = sonnerToast.info;
|
|
99
|
+
toast.promise = sonnerToast.promise;
|
|
100
|
+
toast.custom = sonnerToast.custom;
|
|
101
|
+
toast.dismiss = sonnerToast.dismiss;
|
|
102
|
+
toast.loading = sonnerToast.loading;
|
|
103
|
+
|
|
104
|
+
export { toast };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/* extends popover primitive
|
|
2
|
+
not including the popover selectors because it's not at all required
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
.container {
|
|
6
|
+
width: 100%;
|
|
7
|
+
min-width: fit-content;
|
|
8
|
+
max-width: 250px;
|
|
9
|
+
background-color: var(--color-secondary);
|
|
10
|
+
color: var(--color-secondary-foreground);
|
|
11
|
+
border-radius: var(--radius-sm);
|
|
12
|
+
padding: var(--spacing-2) var(--spacing-3);
|
|
13
|
+
font-size: var(--font-size-xs);
|
|
14
|
+
line-height: 1.2;
|
|
15
|
+
word-wrap: break-word;
|
|
16
|
+
box-shadow: var(--shadow-sm);
|
|
17
|
+
border: 1px solid var(--color-border);
|
|
18
|
+
z-index: 1002;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.arrow {
|
|
22
|
+
position: absolute;
|
|
23
|
+
width: 8px;
|
|
24
|
+
height: 8px;
|
|
25
|
+
background-color: var(--color-secondary);
|
|
26
|
+
transform: rotate(45deg);
|
|
27
|
+
border: 1px solid var(--color-border);
|
|
28
|
+
|
|
29
|
+
/* Top/Bottom positions */
|
|
30
|
+
&[data-position="top"],
|
|
31
|
+
&[data-position="bottom"] {
|
|
32
|
+
&[data-align="center"] {
|
|
33
|
+
left: 50%;
|
|
34
|
+
margin-left: -4px;
|
|
35
|
+
}
|
|
36
|
+
&[data-align="start"] {
|
|
37
|
+
left: var(--spacing-5);
|
|
38
|
+
}
|
|
39
|
+
&[data-align="end"] {
|
|
40
|
+
right: var(--spacing-5);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
&[data-position="top"] {
|
|
45
|
+
bottom: -4px;
|
|
46
|
+
border-top: none;
|
|
47
|
+
border-left: none;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
&[data-position="bottom"] {
|
|
51
|
+
top: -4px;
|
|
52
|
+
border-bottom: none;
|
|
53
|
+
border-right: none;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Left/Right positions */
|
|
57
|
+
&[data-position="left"],
|
|
58
|
+
&[data-position="right"] {
|
|
59
|
+
&[data-align="center"] {
|
|
60
|
+
top: 50%;
|
|
61
|
+
margin-top: -4px;
|
|
62
|
+
}
|
|
63
|
+
&[data-align="start"] {
|
|
64
|
+
top: var(--spacing-5);
|
|
65
|
+
}
|
|
66
|
+
&[data-align="end"] {
|
|
67
|
+
bottom: var(--spacing-5);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
&[data-position="left"] {
|
|
72
|
+
right: -4px;
|
|
73
|
+
border-left: none;
|
|
74
|
+
border-bottom: none;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
&[data-position="right"] {
|
|
78
|
+
left: -4px;
|
|
79
|
+
border-right: none;
|
|
80
|
+
border-top: none;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type ReactNode, type FC } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Popover,
|
|
4
|
+
PopoverTrigger,
|
|
5
|
+
PopoverMenu,
|
|
6
|
+
} from "../../primitives/popover/popover";
|
|
7
|
+
import type { ComponentProps } from "react";
|
|
8
|
+
import s from "./tooltip.module.css";
|
|
9
|
+
import clsx from "clsx";
|
|
10
|
+
|
|
11
|
+
type TooltipProps = {
|
|
12
|
+
content: ReactNode;
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
showArrow?: boolean;
|
|
15
|
+
position?: "top" | "bottom" | "left" | "right";
|
|
16
|
+
animationDuration?: number;
|
|
17
|
+
offset?: number;
|
|
18
|
+
align?: "start" | "center" | "end";
|
|
19
|
+
|
|
20
|
+
// Nested props for popover components
|
|
21
|
+
popover?: Omit<
|
|
22
|
+
ComponentProps<typeof Popover>,
|
|
23
|
+
"children" | "closeOnClickOutside"
|
|
24
|
+
>;
|
|
25
|
+
popoverTrigger?: ComponentProps<typeof PopoverTrigger>;
|
|
26
|
+
popoverMenu?: ComponentProps<typeof PopoverMenu>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const Tooltip: FC<TooltipProps> = ({
|
|
30
|
+
content,
|
|
31
|
+
children,
|
|
32
|
+
showArrow = true,
|
|
33
|
+
position = "top",
|
|
34
|
+
animationDuration = 150,
|
|
35
|
+
offset = 8,
|
|
36
|
+
align = "center",
|
|
37
|
+
|
|
38
|
+
// Destructure nested props
|
|
39
|
+
popover,
|
|
40
|
+
popoverTrigger,
|
|
41
|
+
popoverMenu,
|
|
42
|
+
}) => {
|
|
43
|
+
return (
|
|
44
|
+
<Popover
|
|
45
|
+
trigger="hover"
|
|
46
|
+
position={position}
|
|
47
|
+
animationDuration={animationDuration}
|
|
48
|
+
offset={offset}
|
|
49
|
+
closeOnClickOutside={true}
|
|
50
|
+
align={align}
|
|
51
|
+
{...popover}
|
|
52
|
+
>
|
|
53
|
+
<PopoverTrigger asChild {...popoverTrigger}>
|
|
54
|
+
{children}
|
|
55
|
+
</PopoverTrigger>
|
|
56
|
+
{/* Using menu as a direct content wrapper */}
|
|
57
|
+
<PopoverMenu
|
|
58
|
+
className={clsx(s.container, popoverMenu?.className)}
|
|
59
|
+
{...popoverMenu}
|
|
60
|
+
>
|
|
61
|
+
{showArrow && (
|
|
62
|
+
<div
|
|
63
|
+
className={s.arrow}
|
|
64
|
+
data-position={position}
|
|
65
|
+
data-align={align}
|
|
66
|
+
/>
|
|
67
|
+
)}
|
|
68
|
+
{content}
|
|
69
|
+
</PopoverMenu>
|
|
70
|
+
</Popover>
|
|
71
|
+
);
|
|
72
|
+
};
|