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.
Files changed (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +29 -0
  3. package/dist/api.d.ts +175 -0
  4. package/dist/api.js +1954 -0
  5. package/dist/api.js.map +1 -0
  6. package/dist/frameworks/astro/astro.config.snippet.md +23 -0
  7. package/dist/frameworks/astro/postcss.config.mjs.template +20 -0
  8. package/dist/frameworks/astro/vite-inline.snippet.md +22 -0
  9. package/dist/frameworks/electron-vite/vite.config.snippet.md +44 -0
  10. package/dist/frameworks/next/layout-import.snippet.md +19 -0
  11. package/dist/frameworks/next/next.config.snippet.md +15 -0
  12. package/dist/frameworks/next/postcss.config.js.template +16 -0
  13. package/dist/frameworks/tanstack-start/postcss.config.ts.template +17 -0
  14. package/dist/frameworks/tanstack-start/vite-inline.snippet.md +22 -0
  15. package/dist/frameworks/vite-react/postcss.config.ts.template +16 -0
  16. package/dist/frameworks/vite-react/vite-inline.snippet.md +20 -0
  17. package/dist/frameworks/vite-react/vite.config.ts.template +48 -0
  18. package/dist/index.js +2047 -0
  19. package/dist/index.js.map +1 -0
  20. package/package.json +63 -0
  21. package/registry/components/primitives/icon/icon.module.css +63 -0
  22. package/registry/components/primitives/icon/icon.tsx +53 -0
  23. package/registry/components/primitives/modal/modal-context.ts +19 -0
  24. package/registry/components/primitives/modal/modal.module.css +415 -0
  25. package/registry/components/primitives/modal/modal.tsx +550 -0
  26. package/registry/components/primitives/popover/popover-context.ts +28 -0
  27. package/registry/components/primitives/popover/popover.module.css +28 -0
  28. package/registry/components/primitives/popover/popover.tsx +465 -0
  29. package/registry/components/ui/accordion/accordion.module.css +208 -0
  30. package/registry/components/ui/accordion/accordion.tsx +327 -0
  31. package/registry/components/ui/avatar/avatar.module.css +32 -0
  32. package/registry/components/ui/avatar/avatar.tsx +37 -0
  33. package/registry/components/ui/badge/badge.module.css +103 -0
  34. package/registry/components/ui/badge/badge.tsx +29 -0
  35. package/registry/components/ui/button/button.module.css +216 -0
  36. package/registry/components/ui/button/button.tsx +133 -0
  37. package/registry/components/ui/chart/chart.module.css +168 -0
  38. package/registry/components/ui/chart/chart.tsx +1293 -0
  39. package/registry/components/ui/chart/types.ts +210 -0
  40. package/registry/components/ui/chart/utils.ts +59 -0
  41. package/registry/components/ui/copy-button/copy-button.module.css +21 -0
  42. package/registry/components/ui/copy-button/copy-button.tsx +123 -0
  43. package/registry/components/ui/countdown/countdown.module.css +5 -0
  44. package/registry/components/ui/countdown/countdown.tsx +152 -0
  45. package/registry/components/ui/data-table/checkbox-column.tsx +59 -0
  46. package/registry/components/ui/data-table/data-table.module.css +594 -0
  47. package/registry/components/ui/data-table/data-table.tsx +1109 -0
  48. package/registry/components/ui/data-table/filter-bar.tsx +33 -0
  49. package/registry/components/ui/data-table/pagination-bar.tsx +125 -0
  50. package/registry/components/ui/data-table/types.ts +594 -0
  51. package/registry/components/ui/data-table/useDataTable.tsx +573 -0
  52. package/registry/components/ui/data-table/utils.ts +445 -0
  53. package/registry/components/ui/empty-state/empty-state.module.css +26 -0
  54. package/registry/components/ui/empty-state/empty-state.tsx +32 -0
  55. package/registry/components/ui/input-fields/color-picker/color-palette.module.css +102 -0
  56. package/registry/components/ui/input-fields/color-picker/color-palette.tsx +92 -0
  57. package/registry/components/ui/input-fields/color-picker/color-picker.module.css +29 -0
  58. package/registry/components/ui/input-fields/color-picker/color-picker.tsx +168 -0
  59. package/registry/components/ui/input-fields/common/field-message/field-message.module.css +28 -0
  60. package/registry/components/ui/input-fields/common/field-message/field-message.tsx +39 -0
  61. package/registry/components/ui/input-fields/common/input-field-common.module.css +318 -0
  62. package/registry/components/ui/input-fields/common/input-field-native.module.css +267 -0
  63. package/registry/components/ui/input-fields/common/picker-popover-common.module.css +40 -0
  64. package/registry/components/ui/input-fields/common/select-button/select-button.module.css +9 -0
  65. package/registry/components/ui/input-fields/common/select-button/select-button.tsx +171 -0
  66. package/registry/components/ui/input-fields/common/types.ts +38 -0
  67. package/registry/components/ui/input-fields/custom-select/custom-select.module.css +56 -0
  68. package/registry/components/ui/input-fields/custom-select/custom-select.tsx +403 -0
  69. package/registry/components/ui/input-fields/date-picker/calendar.module.css +311 -0
  70. package/registry/components/ui/input-fields/date-picker/calendar.tsx +186 -0
  71. package/registry/components/ui/input-fields/date-picker/date-picker.module.css +57 -0
  72. package/registry/components/ui/input-fields/date-picker/date-picker.tsx +945 -0
  73. package/registry/components/ui/input-fields/date-picker/types.ts +102 -0
  74. package/registry/components/ui/input-fields/date-picker/utils.ts +236 -0
  75. package/registry/components/ui/input-fields/emoji-picker/emoji-palette.module.css +90 -0
  76. package/registry/components/ui/input-fields/emoji-picker/emoji-palette.tsx +168 -0
  77. package/registry/components/ui/input-fields/emoji-picker/emoji-picker.module.css +8 -0
  78. package/registry/components/ui/input-fields/emoji-picker/emoji-picker.tsx +102 -0
  79. package/registry/components/ui/input-fields/emoji-picker/utils.ts +505 -0
  80. package/registry/components/ui/input-fields/field-group/field-group.module.css +86 -0
  81. package/registry/components/ui/input-fields/field-group/field-group.tsx +86 -0
  82. package/registry/components/ui/input-fields/input/input.tsx +319 -0
  83. package/registry/components/ui/input-fields/media-input/kind-icons.tsx +62 -0
  84. package/registry/components/ui/input-fields/media-input/media-input.module.css +294 -0
  85. package/registry/components/ui/input-fields/media-input/media-input.tsx +327 -0
  86. package/registry/components/ui/input-fields/media-input/media-preview.tsx +101 -0
  87. package/registry/components/ui/input-fields/media-input/types.ts +129 -0
  88. package/registry/components/ui/input-fields/media-input/useMediaUpload.tsx +302 -0
  89. package/registry/components/ui/input-fields/media-input/utils.ts +325 -0
  90. package/registry/components/ui/input-fields/otp-input/otp-input.module.css +24 -0
  91. package/registry/components/ui/input-fields/otp-input/otp-input.tsx +291 -0
  92. package/registry/components/ui/input-fields/select/select.tsx +118 -0
  93. package/registry/components/ui/input-fields/switch/switch.module.css +70 -0
  94. package/registry/components/ui/input-fields/switch/switch.tsx +59 -0
  95. package/registry/components/ui/input-fields/textarea/textarea.tsx +133 -0
  96. package/registry/components/ui/input-fields/time-picker/time-picker.module.css +57 -0
  97. package/registry/components/ui/input-fields/time-picker/time-picker.tsx +437 -0
  98. package/registry/components/ui/loader/loader.module.css +156 -0
  99. package/registry/components/ui/loader/loader.tsx +102 -0
  100. package/registry/components/ui/stepper/stepper.module.css +294 -0
  101. package/registry/components/ui/stepper/stepper.tsx +197 -0
  102. package/registry/components/ui/tabs/tabs.module.css +150 -0
  103. package/registry/components/ui/tabs/tabs.tsx +134 -0
  104. package/registry/components/ui/toast/toast.module.css +159 -0
  105. package/registry/components/ui/toast/toast.tsx +104 -0
  106. package/registry/components/ui/tooltip/tooltip.module.css +82 -0
  107. package/registry/components/ui/tooltip/tooltip.tsx +72 -0
  108. package/registry/global.d.ts +4 -0
  109. package/registry/hooks/useFocusOutside.tsx +33 -0
  110. package/registry/hooks/useResizeObserver.tsx +32 -0
  111. package/registry/lib/dom.ts +25 -0
  112. package/registry/lib/string.ts +23 -0
  113. package/registry/registry.json +719 -0
  114. package/registry/styles/app.css +240 -0
  115. package/registry/styles/components.css +78 -0
  116. package/registry/styles/fonts/geist-regular.woff2 +0 -0
  117. package/registry/styles/globals.css +7 -0
  118. package/registry/styles/media.css +25 -0
  119. package/registry/styles/utilities.css +90 -0
  120. package/registry/themes/colors/blue.css +37 -0
  121. package/registry/themes/colors/green.css +37 -0
  122. package/registry/themes/colors/rose.css +37 -0
  123. package/registry/themes/colors/slate.css +37 -0
  124. package/registry/themes/colors/stone.css +37 -0
  125. package/registry/themes/colors/violet.css +37 -0
  126. package/registry/themes/colors/zinc.css +37 -0
  127. package/registry/themes/radius/pill.css +8 -0
  128. package/registry/themes/radius/rounded.css +8 -0
  129. package/registry/themes/radius/sharp.css +8 -0
  130. package/registry/themes/spacing/compact.css +17 -0
  131. package/registry/themes/spacing/spacious.css +17 -0
  132. 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
+ };
@@ -0,0 +1,4 @@
1
+ declare module "*.module.css" {
2
+ const classes: Record<string, string>;
3
+ export default classes;
4
+ }