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,465 @@
1
+ import {
2
+ useState,
3
+ useRef,
4
+ useEffect,
5
+ useLayoutEffect,
6
+ useCallback,
7
+ type ComponentProps,
8
+ type CSSProperties,
9
+ type ReactNode,
10
+ type FC,
11
+ } from "react";
12
+ import clsx from "clsx";
13
+ import { createPortal } from "react-dom";
14
+ import s from "./popover.module.css";
15
+ import { PopoverContext, usePopover } from "./popover-context";
16
+ import { useFocusOutside } from "../../../hooks/useFocusOutside";
17
+
18
+ // Types
19
+ export type Position = "top" | "bottom" | "left" | "right";
20
+ export type Align = "start" | "center" | "end";
21
+ export type Trigger = "click" | "hover";
22
+
23
+ export type Coords = {
24
+ top: number;
25
+ left: number;
26
+ };
27
+
28
+ // Popover Component
29
+ type PopoverProps = {
30
+ children?: ReactNode;
31
+ className?: string;
32
+ style?: CSSProperties;
33
+ position?: Position;
34
+ align?: Align;
35
+ offset?: number;
36
+ animationDuration?: number;
37
+ isOpen?: boolean;
38
+ setIsOpen?: (open: boolean) => void;
39
+ closeOnClickOutside?: boolean;
40
+ closeOnPathChange?: boolean;
41
+ navigationKey?: string | number;
42
+ onClose?: () => void;
43
+ trigger?: Trigger;
44
+ // Useful when using in isolated environments like Iframe
45
+ renderOnPortal?: boolean;
46
+ };
47
+
48
+ export const Popover: FC<PopoverProps> = ({
49
+ children,
50
+ className,
51
+ style,
52
+ position = "bottom",
53
+ align = "center",
54
+ offset = 8,
55
+ animationDuration,
56
+ isOpen: controlledIsOpen,
57
+ setIsOpen: controlledSetIsOpen,
58
+ closeOnClickOutside = true,
59
+ closeOnPathChange = true,
60
+ navigationKey,
61
+ onClose,
62
+ trigger = "click",
63
+ renderOnPortal = true,
64
+ }) => {
65
+ const readLocationKey = () =>
66
+ `${window.location.pathname}${window.location.search}${window.location.hash}`;
67
+
68
+ const isControlled =
69
+ controlledIsOpen !== undefined && controlledSetIsOpen !== undefined;
70
+ const [internalIsOpen, setInternalIsOpen] = useState(false);
71
+ const isOpen = isControlled ? controlledIsOpen! : internalIsOpen;
72
+ const setIsOpen = isControlled ? controlledSetIsOpen! : setInternalIsOpen;
73
+ const [locationKey, setLocationKey] = useState<string>(
74
+ typeof window === "undefined" ? "" : readLocationKey(),
75
+ );
76
+ const closeSignal = navigationKey ?? locationKey;
77
+ const closeSignalRef = useRef<string | number>(closeSignal);
78
+
79
+ const popoverRef = useRef<HTMLDivElement>(null);
80
+ const menuRef = useRef<HTMLDivElement>(null);
81
+
82
+ // For hover trigger
83
+ const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
84
+
85
+ const [coords, setCoords] = useState<Coords>({ top: 0, left: 0 });
86
+ const [shouldRender, setShouldRender] = useState(false);
87
+ const [isMeasured, setIsMeasured] = useState(false);
88
+ const [triggerWidth, setTriggerWidth] = useState(0);
89
+
90
+ // Calculate Menu Position
91
+ const calculatePosition = useCallback(() => {
92
+ if (!popoverRef.current || !menuRef.current) return;
93
+
94
+ const triggerRect = popoverRef.current.getBoundingClientRect();
95
+ const menuRect = menuRef.current.getBoundingClientRect();
96
+ const { innerWidth, innerHeight } = window;
97
+
98
+ let top = 0;
99
+ let left = 0;
100
+
101
+ // Main axis positioning (offset applies here)
102
+ switch (position) {
103
+ case "top":
104
+ top = triggerRect.top - menuRect.height - offset;
105
+
106
+ break;
107
+ case "bottom":
108
+ top = triggerRect.bottom + offset;
109
+ break;
110
+ case "left":
111
+ left = triggerRect.left - menuRect.width - offset;
112
+ break;
113
+ case "right":
114
+ left = triggerRect.right + offset;
115
+ break;
116
+ }
117
+
118
+ // Cross axis positioning (align applies here)
119
+ switch (position) {
120
+ case "top":
121
+ case "bottom":
122
+ if (align === "start") left = triggerRect.left;
123
+ else if (align === "end") left = triggerRect.right - menuRect.width;
124
+ else left = triggerRect.left + (triggerRect.width - menuRect.width) / 2;
125
+ break;
126
+ case "left":
127
+ case "right":
128
+ if (align === "start") top = triggerRect.top;
129
+ else if (align === "end") top = triggerRect.bottom - menuRect.height;
130
+ else
131
+ top = triggerRect.top + triggerRect.height / 2 - menuRect.height / 2;
132
+ break;
133
+ }
134
+
135
+ // Collision detection
136
+ const PADDING = 8;
137
+ top = Math.max(
138
+ PADDING,
139
+ Math.min(top, innerHeight - menuRect.height - PADDING),
140
+ );
141
+ left = Math.max(
142
+ PADDING,
143
+ Math.min(left, innerWidth - menuRect.width - PADDING),
144
+ );
145
+
146
+ setCoords({ top, left });
147
+ setTriggerWidth(triggerRect.width);
148
+ setIsMeasured(true);
149
+ }, [position, align, offset]);
150
+
151
+ // Open / Close Handling
152
+ // Visibility (opacity / pointer-events) is derived from `isOpen && isMeasured`
153
+ // in PopoverMenu, so we DON'T reset isMeasured here on close - that would lose
154
+ // the position cache and break re-opens that interrupt the exit animation.
155
+ // isMeasured is reset only when the menu actually unmounts (cleanup below).
156
+ useEffect(() => {
157
+ if (isOpen) {
158
+ setShouldRender(true);
159
+ return;
160
+ }
161
+ const timer = setTimeout(() => {
162
+ setShouldRender(false);
163
+ if (onClose) onClose();
164
+ }, animationDuration || 100);
165
+ return () => clearTimeout(timer);
166
+ }, [isOpen, animationDuration, onClose]);
167
+
168
+ // Measure after menu is rendered. Cleanup resets isMeasured so the next mount
169
+ // re-runs the entry animation from a known starting state.
170
+ useLayoutEffect(() => {
171
+ if (shouldRender && menuRef.current) {
172
+ calculatePosition();
173
+
174
+ const observer = new ResizeObserver(() => calculatePosition());
175
+ observer.observe(menuRef.current);
176
+ return () => {
177
+ observer.disconnect();
178
+ setIsMeasured(false);
179
+ };
180
+ }
181
+ }, [shouldRender, calculatePosition]);
182
+
183
+ // Clear any pending hover-close timer when the Popover unmounts so the
184
+ // delayed setIsOpen never fires on a stale instance.
185
+ useEffect(() => {
186
+ return () => {
187
+ if (hoverTimeoutRef.current) {
188
+ clearTimeout(hoverTimeoutRef.current);
189
+ hoverTimeoutRef.current = null;
190
+ }
191
+ };
192
+ }, []);
193
+
194
+ // Recalculate on scroll / resize
195
+ useEffect(() => {
196
+ if (!isOpen) return;
197
+ const handler = () => {
198
+ if (isMeasured) calculatePosition();
199
+ };
200
+ window.addEventListener("scroll", handler, true);
201
+ window.addEventListener("resize", handler);
202
+ return () => {
203
+ window.removeEventListener("scroll", handler, true);
204
+ window.removeEventListener("resize", handler);
205
+ };
206
+ }, [isOpen, isMeasured, calculatePosition]);
207
+
208
+ // Track browser navigation changes when no external navigation key is provided.
209
+ useEffect(() => {
210
+ if (navigationKey !== undefined || !closeOnPathChange || !isOpen) return;
211
+ if (typeof window === "undefined") return;
212
+
213
+ let previous = readLocationKey();
214
+
215
+ const updateIfChanged = () => {
216
+ const next = readLocationKey();
217
+ if (next !== previous) {
218
+ previous = next;
219
+ setLocationKey(next);
220
+ }
221
+ };
222
+
223
+ window.addEventListener("popstate", updateIfChanged);
224
+ window.addEventListener("hashchange", updateIfChanged);
225
+ const interval = window.setInterval(updateIfChanged, 120);
226
+
227
+ return () => {
228
+ window.removeEventListener("popstate", updateIfChanged);
229
+ window.removeEventListener("hashchange", updateIfChanged);
230
+ window.clearInterval(interval);
231
+ };
232
+ }, [navigationKey, closeOnPathChange, isOpen]);
233
+
234
+ // Close on navigation signal change (external key or browser location).
235
+ useEffect(() => {
236
+ if (!closeOnPathChange) return;
237
+
238
+ if (closeSignalRef.current !== closeSignal) {
239
+ closeSignalRef.current = closeSignal;
240
+ setIsOpen(false);
241
+ return;
242
+ }
243
+
244
+ closeSignalRef.current = closeSignal;
245
+ }, [closeOnPathChange, closeSignal, setIsOpen]);
246
+
247
+ // Close on outside click
248
+ useFocusOutside([popoverRef, menuRef], () => setIsOpen(false), {
249
+ enabled: closeOnClickOutside,
250
+ });
251
+
252
+ return (
253
+ <PopoverContext.Provider
254
+ value={{
255
+ isOpen,
256
+ setIsOpen,
257
+ popoverRef,
258
+ menuRef,
259
+ coords,
260
+ isMeasured,
261
+ shouldRender,
262
+ animationDuration,
263
+ triggerWidth,
264
+ trigger,
265
+ hoverTimeoutRef,
266
+ renderOnPortal,
267
+ position,
268
+ }}
269
+ >
270
+ <div
271
+ ref={popoverRef}
272
+ className={clsx(s.container, className)}
273
+ style={style}
274
+ >
275
+ {children}
276
+ </div>
277
+ </PopoverContext.Provider>
278
+ );
279
+ };
280
+
281
+ // Trigger
282
+ type PopoverTriggerProps = ComponentProps<"button"> & {
283
+ children?: ReactNode;
284
+ className?: string;
285
+ style?: CSSProperties;
286
+ asChild?: boolean;
287
+ };
288
+
289
+ export const PopoverTrigger: FC<PopoverTriggerProps> = ({
290
+ children,
291
+ className,
292
+ style,
293
+ asChild = false,
294
+ disabled,
295
+ ...rest
296
+ }) => {
297
+ const { setIsOpen, isOpen, trigger, hoverTimeoutRef } = usePopover();
298
+
299
+ const handleClick = () => {
300
+ if (!disabled && trigger === "click") {
301
+ setIsOpen(!isOpen);
302
+ }
303
+ };
304
+
305
+ const handleMouseEnter = () => {
306
+ if (!disabled && trigger === "hover") {
307
+ if (hoverTimeoutRef.current) {
308
+ clearTimeout(hoverTimeoutRef.current);
309
+ hoverTimeoutRef.current = null;
310
+ }
311
+ setIsOpen(true);
312
+ }
313
+ };
314
+
315
+ const handleMouseLeave = () => {
316
+ if (!disabled && trigger === "hover") {
317
+ hoverTimeoutRef.current = setTimeout(() => {
318
+ setIsOpen(false);
319
+ }, 100);
320
+ }
321
+ };
322
+
323
+ if (asChild) {
324
+ return (
325
+ <div
326
+ className={clsx(s.trigger, className)}
327
+ style={style}
328
+ onClick={handleClick}
329
+ onMouseEnter={handleMouseEnter}
330
+ onMouseLeave={handleMouseLeave}
331
+ aria-expanded={isOpen}
332
+ data-disabled={disabled}
333
+ {...(rest as ComponentProps<"div">)}
334
+ >
335
+ {children}
336
+ </div>
337
+ );
338
+ }
339
+
340
+ return (
341
+ <button
342
+ type="button"
343
+ className={clsx(s.trigger, className)}
344
+ style={style}
345
+ onClick={handleClick}
346
+ onMouseEnter={handleMouseEnter}
347
+ onMouseLeave={handleMouseLeave}
348
+ aria-expanded={isOpen}
349
+ data-disabled={disabled}
350
+ {...(rest as ComponentProps<"button">)}
351
+ >
352
+ {children}
353
+ </button>
354
+ );
355
+ };
356
+
357
+ // Menu
358
+ type PopoverMenuProps = {
359
+ children?: ReactNode;
360
+ className?: string;
361
+ style?: CSSProperties;
362
+ /**
363
+ * Cap the menu's `max-width` to the trigger's width - so the menu visually
364
+ * mirrors the trigger (CustomSelect / dropdown pattern). Set to `false` for
365
+ * pickers whose trigger is intentionally smaller than the menu (icon
366
+ * buttons, small swatches). Default: true.
367
+ */
368
+ matchTriggerWidth?: boolean;
369
+ };
370
+
371
+ export const PopoverMenu: FC<PopoverMenuProps> = ({
372
+ children,
373
+ className,
374
+ style,
375
+ matchTriggerWidth = true,
376
+ }) => {
377
+ const {
378
+ isOpen,
379
+ shouldRender,
380
+ isMeasured,
381
+ coords,
382
+ menuRef,
383
+ triggerWidth,
384
+ animationDuration,
385
+ trigger,
386
+ setIsOpen,
387
+ position,
388
+ hoverTimeoutRef,
389
+ renderOnPortal,
390
+ } = usePopover();
391
+
392
+ const handleMouseEnter = () => {
393
+ if (trigger === "hover" && hoverTimeoutRef.current) {
394
+ clearTimeout(hoverTimeoutRef.current);
395
+ hoverTimeoutRef.current = null;
396
+ }
397
+ };
398
+
399
+ const handleMouseLeave = () => {
400
+ if (trigger === "hover") {
401
+ hoverTimeoutRef.current = setTimeout(() => {
402
+ setIsOpen(false);
403
+ }, 100);
404
+ }
405
+ };
406
+
407
+ if (!shouldRender) return null;
408
+
409
+ // Visibility is the boolean we want the user to *see*. Derived from both
410
+ // `isOpen` (the intent) and `isMeasured` (we know where to place it).
411
+ // Reading from `isOpen` here lets re-opens during the exit animation reverse
412
+ // the fade smoothly without losing the cached position from `isMeasured`.
413
+ const isVisible = isOpen && isMeasured;
414
+
415
+ const getTransform = (
416
+ position: Position,
417
+ isMeasured: boolean,
418
+ isOpen: boolean,
419
+ ): string => {
420
+ // Once measured, no transform - final position. While exiting (!isOpen)
421
+ // also force "none" so the element doesn't create a CSS Containing Block
422
+ // that would destroy coordinates of nested `position: fixed` popovers.
423
+ if (isMeasured || !isOpen) {
424
+ return "none";
425
+ }
426
+
427
+ const transforms: Record<Position, string> = {
428
+ top: "translateY(4px)",
429
+ bottom: "translateY(-4px)",
430
+ left: "translateX(4px)",
431
+ right: "translateX(-4px)",
432
+ };
433
+
434
+ return transforms[position];
435
+ };
436
+
437
+ const menuStyles: CSSProperties = {
438
+ position: "fixed",
439
+ top: coords.top,
440
+ left: coords.left,
441
+ zIndex: "var(--z-index-popover)",
442
+ ...(matchTriggerWidth && { maxWidth: triggerWidth }),
443
+ opacity: isVisible ? 1 : 0,
444
+ transform: getTransform(position, isMeasured, isOpen),
445
+ transition: animationDuration
446
+ ? `opacity ${animationDuration}ms cubic-bezier(0.4, 0, 0.2, 1), transform ${animationDuration}ms cubic-bezier(0.4, 0, 0.2, 1)`
447
+ : "opacity var(--transition-base), transform var(--transition-base)",
448
+ pointerEvents: isVisible ? "auto" : "none",
449
+ ...style,
450
+ };
451
+
452
+ const menu = (
453
+ <div
454
+ ref={menuRef}
455
+ className={clsx(s.menu_wrapper, className)}
456
+ onMouseEnter={handleMouseEnter}
457
+ onMouseLeave={handleMouseLeave}
458
+ style={menuStyles}
459
+ >
460
+ {children}
461
+ </div>
462
+ );
463
+
464
+ return renderOnPortal ? createPortal(menu, document.body) : menu;
465
+ };
@@ -0,0 +1,208 @@
1
+ .root {
2
+ width: 100%;
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: var(--spacing-3);
6
+ }
7
+
8
+ .item {
9
+ --accordion-fg: var(--color-foreground);
10
+ --accordion-muted-fg: var(--color-muted-foreground);
11
+ --accordion-icon: var(--accordion-fg);
12
+
13
+ width: 100%;
14
+ border-radius: var(--radius-md);
15
+ overflow: hidden;
16
+ transition:
17
+ border-color var(--transition-base),
18
+ box-shadow var(--transition-base),
19
+ background-color var(--transition-base);
20
+
21
+ &.item_disabled {
22
+ opacity: 0.65;
23
+ }
24
+
25
+ &.item_bordered {
26
+ border: var(--border-1) solid var(--color-border);
27
+ background-color: var(--color-background);
28
+ box-shadow: var(--shadow-sm);
29
+
30
+ &.item_open {
31
+ box-shadow: var(--shadow-sm);
32
+ }
33
+ }
34
+
35
+ &.item_minimal {
36
+ border: 0;
37
+ border-radius: var(--radius-md);
38
+ background: transparent;
39
+ box-shadow: none;
40
+
41
+ &.item_open {
42
+ box-shadow: none;
43
+ }
44
+ }
45
+ }
46
+
47
+ .trigger {
48
+ width: 100%;
49
+ display: flex;
50
+ align-items: center;
51
+ justify-content: space-between;
52
+ gap: var(--spacing-4);
53
+ text-align: left;
54
+ color: var(--accordion-fg);
55
+ border: none;
56
+ outline: none;
57
+ cursor: pointer;
58
+
59
+ &:disabled {
60
+ cursor: not-allowed;
61
+ }
62
+ }
63
+
64
+ .trigger_bordered,
65
+ .trigger_minimal {
66
+ width: 100%;
67
+ justify-content: space-between;
68
+ }
69
+
70
+ .trigger_bordered {
71
+ background: transparent;
72
+
73
+ &:hover:not(:disabled) {
74
+ background: var(--color-muted);
75
+ }
76
+
77
+ &:focus-visible {
78
+ box-shadow: var(--shadow-ring);
79
+ }
80
+ }
81
+
82
+ .trigger_minimal {
83
+ border-radius: inherit;
84
+
85
+ &:hover:not(:disabled) {
86
+ background: var(--color-muted);
87
+ }
88
+
89
+ &:focus-visible {
90
+ box-shadow: var(--shadow-ring);
91
+ }
92
+ }
93
+
94
+ .trigger_open {
95
+ background: var(--color-muted);
96
+ }
97
+
98
+ .trigger_sm {
99
+ min-height: var(--spacing-9);
100
+ padding: var(--spacing-3) var(--spacing-4);
101
+ }
102
+
103
+ .trigger_md {
104
+ min-height: var(--spacing-11);
105
+ padding: var(--spacing-4) var(--spacing-5);
106
+ }
107
+
108
+ .trigger_lg {
109
+ min-height: var(--spacing-12);
110
+ padding: var(--spacing-5) var(--spacing-6);
111
+ }
112
+
113
+ .title {
114
+ flex: 1;
115
+ min-width: 0;
116
+ padding-inline-end: var(--spacing-2);
117
+ font-family: var(--font-family-1);
118
+ font-weight: var(--font-weight-medium);
119
+ color: inherit;
120
+ white-space: normal;
121
+ overflow-wrap: anywhere;
122
+ word-break: break-word;
123
+ }
124
+
125
+ .title_sm {
126
+ font-size: var(--font-size-xs);
127
+ line-height: var(--line-height-xs);
128
+ }
129
+
130
+ .title_md {
131
+ font-size: var(--font-size-sm);
132
+ line-height: var(--line-height-sm);
133
+ }
134
+
135
+ .title_lg {
136
+ font-size: var(--font-size-base);
137
+ line-height: var(--line-height-base);
138
+ }
139
+
140
+ .icon {
141
+ flex-shrink: 0;
142
+ display: inline-flex;
143
+ align-items: center;
144
+ justify-content: center;
145
+ color: var(--accordion-icon);
146
+ transform: rotate(0deg);
147
+ transition: transform var(--transition-base);
148
+
149
+ &.icon_open {
150
+ transform: rotate(var(--accordion-icon-rotation, 180deg));
151
+ }
152
+ }
153
+
154
+ .panel {
155
+ height: 0;
156
+ opacity: 0;
157
+ overflow: hidden;
158
+ pointer-events: none;
159
+ transition:
160
+ height var(--transition-base),
161
+ opacity var(--transition-base);
162
+
163
+ &.panel_open {
164
+ opacity: 1;
165
+ pointer-events: auto;
166
+ }
167
+ }
168
+
169
+ .panel_inner {
170
+ min-height: 0;
171
+ color: var(--accordion-muted-fg);
172
+ }
173
+
174
+ .panel_inner_bordered {
175
+ border-top: 0;
176
+ background-color: var(--color-background);
177
+
178
+ &.panel_inner_open {
179
+ border-top: var(--border-1) solid var(--color-border);
180
+ }
181
+ }
182
+
183
+ .panel_inner_minimal {
184
+ border: 0;
185
+ background: transparent;
186
+
187
+ &.panel_inner_open {
188
+ border-top: 0;
189
+ }
190
+ }
191
+
192
+ .panel_inner_sm {
193
+ padding: var(--spacing-2) var(--spacing-4) var(--spacing-4);
194
+ font-size: var(--font-size-xs);
195
+ line-height: var(--line-height-sm);
196
+ }
197
+
198
+ .panel_inner_md {
199
+ padding: var(--spacing-3) var(--spacing-5) var(--spacing-5);
200
+ font-size: var(--font-size-sm);
201
+ line-height: var(--line-height-base);
202
+ }
203
+
204
+ .panel_inner_lg {
205
+ padding: var(--spacing-4) var(--spacing-6) var(--spacing-6);
206
+ font-size: var(--font-size-base);
207
+ line-height: var(--line-height-base);
208
+ }