flikkui 0.2.0-beta.6 → 0.2.0-beta.8

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 (59) hide show
  1. package/README.md +100 -7
  2. package/dist/components/ai/CodeBlock/CodeBlock.js +1 -1
  3. package/dist/components/ai/PromptInput/PromptInput.js +14 -0
  4. package/dist/components/ai/StreamingResponse/MarkdownRenderer.js +1 -1
  5. package/dist/components/ai/index.js +27 -0
  6. package/dist/components/core/ContextMenu/ContextMenu.js +90 -0
  7. package/dist/components/core/ContextMenu/ContextMenu.theme.js +27 -0
  8. package/dist/components/core/ContextMenu/ContextMenuContent.js +194 -0
  9. package/dist/components/core/ContextMenu/ContextMenuContext.js +22 -0
  10. package/dist/components/core/ContextMenu/ContextMenuItem.js +35 -0
  11. package/dist/components/core/ContextMenu/ContextMenuLabel.js +14 -0
  12. package/dist/components/core/ContextMenu/ContextMenuSeparator.js +14 -0
  13. package/dist/components/core/ContextMenu/ContextMenuSub.js +40 -0
  14. package/dist/components/core/ContextMenu/ContextMenuSubContent.js +103 -0
  15. package/dist/components/core/ContextMenu/ContextMenuSubTrigger.js +61 -0
  16. package/dist/components/core/ContextMenu/ContextMenuTrigger.js +49 -0
  17. package/dist/components/core/DescriptionList/DescriptionList.js +43 -0
  18. package/dist/components/core/DescriptionList/DescriptionList.theme.js +8 -0
  19. package/dist/components/core/Notification/Notification.js +4 -0
  20. package/dist/components/core/Pill/Pill.animations.js +25 -0
  21. package/dist/components/core/Pill/Pill.js +119 -0
  22. package/dist/components/core/Pill/Pill.theme.js +44 -0
  23. package/dist/components/core/ProgressiveBlur/ProgressiveBlur.js +5 -6
  24. package/dist/components/core/index.d.ts +3 -0
  25. package/dist/components/core/index.js +5 -0
  26. package/dist/components/forms/ColorPicker/ColorPicker.animations.js +34 -0
  27. package/dist/components/forms/ColorPicker/ColorPicker.js +175 -0
  28. package/dist/components/forms/ColorPicker/ColorPicker.theme.js +20 -0
  29. package/dist/components/forms/ColorPicker/ColorPicker2DCanvas.js +91 -0
  30. package/dist/components/forms/ColorPicker/ColorPickerContent.js +72 -0
  31. package/dist/components/forms/ColorPicker/ColorPickerEyeDropper.js +50 -0
  32. package/dist/components/forms/ColorPicker/ColorPickerFormatSelector.js +27 -0
  33. package/dist/components/forms/ColorPicker/ColorPickerInput.js +103 -0
  34. package/dist/components/forms/ColorPicker/ColorPickerPresets.js +57 -0
  35. package/dist/components/forms/ColorPicker/ColorPickerSliders.js +45 -0
  36. package/dist/components/forms/ColorPicker/ColorPickerSwatch.js +38 -0
  37. package/dist/components/forms/ColorPicker/ColorPickerTrigger.js +38 -0
  38. package/dist/components/forms/ColorPicker/colorUtils.js +324 -0
  39. package/dist/components/forms/FileUpload/FileUpload.js +4 -0
  40. package/dist/components/forms/TimePicker/TimePickerContent.js +4 -0
  41. package/dist/components/forms/index.d.ts +1 -0
  42. package/dist/components/forms/index.js +15 -0
  43. package/dist/index.d.ts +0 -1
  44. package/dist/index.js +16 -24
  45. package/package.json +1 -11
  46. package/dist/cli/commands/init.d.ts +0 -7
  47. package/dist/cli/commands/init.js +0 -82
  48. package/dist/cli/index.d.ts +0 -2
  49. package/dist/cli/index.js +0 -17
  50. package/dist/cli/utils/detectProject.d.ts +0 -9
  51. package/dist/cli/utils/detectProject.js +0 -126
  52. package/dist/cli/utils/injectCSS.d.ts +0 -8
  53. package/dist/cli/utils/injectCSS.js +0 -82
  54. package/dist/cli/utils/installDeps.d.ts +0 -2
  55. package/dist/cli/utils/installDeps.js +0 -44
  56. package/dist/cli/utils/logger.d.ts +0 -9
  57. package/dist/cli/utils/logger.js +0 -35
  58. package/dist/cli/utils/setupTailwind.d.ts +0 -7
  59. package/dist/cli/utils/setupTailwind.js +0 -98
package/README.md CHANGED
@@ -1,16 +1,44 @@
1
1
  # Flikkui
2
2
 
3
- A modern React component library built with TypeScript, Tailwind CSS v4, and Framer Motion.
3
+ A modern React component library built with TypeScript, Tailwind CSS v4, and Framer Motion. Follows the shadcn philosophy with complete `className` override support.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
8
  npm install flikkui
9
+ # or
10
+ yarn add flikkui
9
11
  ```
10
12
 
11
13
  ## Setup
12
14
 
13
- Import styles in your main app file:
15
+ The quickest way to get started is with the CLI init command. It auto-detects your project type (Next.js, Vite, Remix, CRA), installs peer dependencies, and injects the CSS import into your entry file:
16
+
17
+ ```bash
18
+ npx flikkui init
19
+ ```
20
+
21
+ Options:
22
+
23
+ | Flag | Description |
24
+ |------|-------------|
25
+ | `--tailwind`, `-t` | Set up Tailwind CSS integration (adds preset and content paths) |
26
+ | `--yes`, `-y` | Skip prompts and use defaults |
27
+ | `--skip-install` | Skip installing dependencies (prints manual command instead) |
28
+
29
+ ### Manual Setup
30
+
31
+ If you prefer to set things up yourself:
32
+
33
+ 1. Install peer dependencies:
34
+
35
+ ```bash
36
+ npm install react react-dom tailwindcss motion clsx tailwind-merge @heroicons/react
37
+ # or
38
+ yarn add react react-dom tailwindcss motion clsx tailwind-merge @heroicons/react
39
+ ```
40
+
41
+ 2. Import styles in your main app file:
14
42
 
15
43
  ```tsx
16
44
  import 'flikkui/styles.css'
@@ -22,10 +50,21 @@ import 'flikkui/styles.css'
22
50
  import { Button } from 'flikkui'
23
51
 
24
52
  function App() {
25
- return <Button variant="primary">Click me</Button>
53
+ return <Button variant="filled" color="primary">Click me</Button>
26
54
  }
27
55
  ```
28
56
 
57
+ ### Subpath Imports
58
+
59
+ Import from specific categories to optimize bundle size:
60
+
61
+ ```tsx
62
+ import { Button, Badge, Accordion } from 'flikkui/core'
63
+ import { Input, Select, Checkbox } from 'flikkui/forms'
64
+ import { AreaChart, BarChart } from 'flikkui/charts'
65
+ import { GlassEffect } from 'flikkui/effects'
66
+ ```
67
+
29
68
  ## Customizing the Theme
30
69
 
31
70
  Flikkui uses CSS variables for all theming. Override any variable in your own CSS to customize colors, spacing, border radius, and more.
@@ -55,6 +94,19 @@ Override semantic color palettes by redefining the CSS variables after importing
55
94
 
56
95
  Available color palettes: `primary`, `danger`, `success`, `warning`. Each has shades from `50` to `950`.
57
96
 
97
+ ### Text Colors
98
+
99
+ ```css
100
+ :root {
101
+ --color-text-primary: var(--color-neutral-950);
102
+ --color-text-secondary: var(--color-neutral-800);
103
+ --color-text-placeholder: var(--color-neutral-600);
104
+ --color-text-muted: var(--color-neutral-500);
105
+ --color-text-disabled: var(--color-neutral-400);
106
+ --color-text-inverse: var(--color-neutral-50);
107
+ }
108
+ ```
109
+
58
110
  ### Backgrounds & Borders
59
111
 
60
112
  ```css
@@ -62,19 +114,53 @@ Available color palettes: `primary`, `danger`, `success`, `warning`. Each has sh
62
114
  --color-background: oklch(98.5% 0.002 247.839);
63
115
  --color-background-secondary: oklch(95% 0.002 247.839);
64
116
  --color-background-tertiary: oklch(90% 0.002 247.839);
117
+ --color-background-quaternary: oklch(85% 0.002 247.839);
118
+ --color-background-quinary: oklch(80% 0.002 247.839);
119
+ --color-background-disabled: var(--color-neutral-100);
65
120
  --color-border: oklch(92.2% 0 0);
66
121
  }
67
122
  ```
68
123
 
124
+ ### Border Radius System
125
+
126
+ All border radii derive from a single `--radius-base` token. Change it once to update the entire UI:
127
+
128
+ ```css
129
+ :root {
130
+ --radius-base: 0.5rem; /* 8px - change this to update all radii */
131
+
132
+ /* Forms & Controls (1x base) */
133
+ --form-radius: var(--radius-base);
134
+ --button-radius: var(--form-radius);
135
+ --badge-radius: var(--form-radius);
136
+ --nav-item-radius: var(--radius-base);
137
+
138
+ /* Overlays (1.5x base) */
139
+ --dropdown-radius: calc(var(--radius-base) * 1.5);
140
+ --tooltip-radius: var(--radius-base);
141
+ --popover-radius: calc(var(--radius-base) * 1.5);
142
+
143
+ /* Feedback surfaces */
144
+ --alert-radius: calc(var(--radius-base) * 1.5);
145
+ --toast-radius: calc(var(--radius-base) * 2);
146
+ --drawer-radius: calc(var(--radius-base) * 2);
147
+
148
+ /* Surfaces (3x base) */
149
+ --card-radius: calc(var(--radius-base) * 3);
150
+ --modal-radius: calc(var(--radius-base) * 3);
151
+
152
+ /* Special shapes */
153
+ --segmented-radius: 99px;
154
+ --avatar-radius: 50%;
155
+ }
156
+ ```
157
+
69
158
  ### Form Elements
70
159
 
71
160
  Control sizing, padding, and border radius for all form components:
72
161
 
73
162
  ```css
74
163
  :root {
75
- /* Border radius */
76
- --form-rounded: 0.5rem;
77
-
78
164
  /* Heights */
79
165
  --form-min-h-sm: 2rem;
80
166
  --form-min-h-md: 2.5rem;
@@ -85,6 +171,11 @@ Control sizing, padding, and border radius for all form components:
85
171
  --form-px-md: 0.875rem;
86
172
  --form-px-lg: 1rem;
87
173
 
174
+ /* Vertical padding */
175
+ --form-py-sm: 0.5rem;
176
+ --form-py-md: 0.625rem;
177
+ --form-py-lg: 0.75rem;
178
+
88
179
  /* Font sizes */
89
180
  --form-text-sm: 0.75rem;
90
181
  --form-text-md: 0.875rem;
@@ -92,7 +183,7 @@ Control sizing, padding, and border radius for all form components:
92
183
  }
93
184
  ```
94
185
 
95
- Button, badge, segmented, and other component variables inherit from these form defaults but can be overridden individually (e.g. `--button-rounded`, `--badge-rounded`).
186
+ Button, badge, segmented, and other component variables inherit from these form defaults but can be overridden individually (e.g. `--button-radius`, `--badge-radius`, `--button-px-*`).
96
187
 
97
188
  ### Dark Mode
98
189
 
@@ -111,6 +202,8 @@ The library ships with sensible dark mode defaults. To customize dark mode color
111
202
  --color-background: oklch(14.5% 0 0);
112
203
  --color-border: oklch(30% 0 0);
113
204
  --color-primary: var(--color-primary-400);
205
+ --color-text-primary: var(--color-neutral-50);
206
+ --color-text-secondary: var(--color-neutral-400);
114
207
  }
115
208
  ```
116
209
 
@@ -1,6 +1,6 @@
1
1
  import React__default, { useState, useMemo, useCallback } from 'react';
2
2
  import { Prism } from 'react-syntax-highlighter';
3
- import { oneDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
3
+ import { oneDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism/index.js';
4
4
  import { DocumentTextIcon, DocumentCheckIcon, DocumentDuplicateIcon } from '@heroicons/react/24/outline';
5
5
  import { codeBlockTheme } from './CodeBlock.theme.js';
6
6
  import { LanguageIcon } from './LanguageIcon.js';
@@ -81,6 +81,10 @@ import '../../core/Sidebar/SidebarNav.js';
81
81
  import '../../core/Sidebar/SidebarNavGroup.js';
82
82
  import '../../core/Sidebar/SidebarToggle.js';
83
83
  import '../../core/Sidebar/SidebarContext.js';
84
+ import '../../core/ContextMenu/ContextMenu.js';
85
+ import '../../core/ContextMenu/ContextMenu.theme.js';
86
+ import '../../core/DescriptionList/DescriptionList.js';
87
+ import '../../core/Pill/Pill.js';
84
88
  import '../../forms/TimePicker/WheelColumn.js';
85
89
  import '../../forms/TimePicker/TimePicker.theme.js';
86
90
  import '../../forms/Slider/Slider.js';
@@ -101,6 +105,16 @@ import '../../forms/InputTag/InputTag.js';
101
105
  import '../../forms/InputTag/InputTag.theme.js';
102
106
  import '../../forms/Combobox/Combobox.js';
103
107
  import '../../forms/Combobox/Combobox.theme.js';
108
+ import '../../forms/ColorPicker/ColorPicker.js';
109
+ import '../../forms/ColorPicker/ColorPickerTrigger.js';
110
+ import '../../forms/ColorPicker/ColorPickerContent.js';
111
+ import '../../forms/ColorPicker/ColorPicker2DCanvas.js';
112
+ import '../../forms/ColorPicker/ColorPickerSwatch.js';
113
+ import '../../forms/ColorPicker/ColorPickerSliders.js';
114
+ import '../../forms/ColorPicker/ColorPickerInput.js';
115
+ import '../../forms/ColorPicker/ColorPickerFormatSelector.js';
116
+ import '../../forms/ColorPicker/ColorPickerEyeDropper.js';
117
+ import '../../forms/ColorPicker/ColorPickerPresets.js';
104
118
  import { promptInputTheme } from './PromptInput.theme.js';
105
119
 
106
120
  /**
@@ -1,7 +1,7 @@
1
1
  import React__default from 'react';
2
2
  import ReactMarkdown from 'react-markdown';
3
3
  import { Prism } from 'react-syntax-highlighter';
4
- import { oneDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
4
+ import { oneDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism/index.js';
5
5
  import remarkGfm from 'remark-gfm';
6
6
 
7
7
  /**
@@ -0,0 +1,27 @@
1
+ export { ChatInterface } from './ChatInterface/ChatInterface.js';
2
+ export { chatInterfaceTheme } from './ChatInterface/ChatInterface.theme.js';
3
+ export { useChatInterfaceContext } from './ChatInterface/ChatInterface.types.js';
4
+ export { Message } from '../core/Message/Message.js';
5
+ export { messageTheme } from '../core/Message/Message.theme.js';
6
+ import '../core/Message/TypeWriter.js';
7
+ export { MessageHistory } from './MessageHistory/MessageHistory.js';
8
+ export { messageHistoryTheme } from './MessageHistory/MessageHistory.theme.js';
9
+ export { PromptInput } from './PromptInput/PromptInput.js';
10
+ export { promptInputTheme } from './PromptInput/PromptInput.theme.js';
11
+ export { ThinkingIndicator } from './ThinkingIndicator/ThinkingIndicator.js';
12
+ export { thinkingIndicatorTheme } from './ThinkingIndicator/ThinkingIndicator.theme.js';
13
+ export { TokenCounter } from './TokenCounter/TokenCounter.js';
14
+ export { tokenCounterTheme } from './TokenCounter/TokenCounter.theme.js';
15
+ export { MODEL_CONFIGS, calculateCost, estimateTokens, formatCost, formatTokenCount } from './TokenCounter/tokenUtils.js';
16
+ export { PromptSuggestion } from './PromptSuggestions/PromptSuggestion.js';
17
+ export { promptSuggestionTheme } from './PromptSuggestions/PromptSuggestion.theme.js';
18
+ export { StreamingResponse } from './StreamingResponse/StreamingResponse.js';
19
+ export { ErrorDisplay } from './StreamingResponse/ErrorDisplay.js';
20
+ export { MarkdownRenderer } from './StreamingResponse/MarkdownRenderer.js';
21
+ export { streamingResponseTheme } from './StreamingResponse/StreamingResponse.theme.js';
22
+ export { CodeBlock } from './CodeBlock/CodeBlock.js';
23
+ export { codeBlockTheme } from './CodeBlock/CodeBlock.theme.js';
24
+ export { ApprovalCard } from './ApprovalCard/ApprovalCard.js';
25
+ export { approvalCardTheme } from './ApprovalCard/ApprovalCard.theme.js';
26
+ export { ArtifactContainer } from './ArtifactContainer/ArtifactContainer.js';
27
+ export { artifactContainerTheme, handleSizes } from './ArtifactContainer/ArtifactContainer.theme.js';
@@ -0,0 +1,90 @@
1
+ import React__default, { useState, useRef, useCallback, useEffect } from 'react';
2
+ import { cn } from '../../../utils/cn.js';
3
+ import { ContextMenuContext } from './ContextMenuContext.js';
4
+ import { ContextMenuTrigger } from './ContextMenuTrigger.js';
5
+ import { ContextMenuContent } from './ContextMenuContent.js';
6
+ import { ContextMenuItem } from './ContextMenuItem.js';
7
+ import { ContextMenuSub } from './ContextMenuSub.js';
8
+ import { ContextMenuSubTrigger } from './ContextMenuSubTrigger.js';
9
+ import { ContextMenuSubContent } from './ContextMenuSubContent.js';
10
+ import { ContextMenuSeparator } from './ContextMenuSeparator.js';
11
+ import { ContextMenuLabel } from './ContextMenuLabel.js';
12
+ import { contextMenuTheme } from './ContextMenu.theme.js';
13
+
14
+ /**
15
+ * ContextMenu component for right-click interactions
16
+ *
17
+ * @component
18
+ * @example
19
+ * ```tsx
20
+ * <ContextMenu>
21
+ * <ContextMenu.Trigger>
22
+ * <div>Right click here</div>
23
+ * </ContextMenu.Trigger>
24
+ * <ContextMenu.Content>
25
+ * <ContextMenu.Item shortcut="⌘R">Reload</ContextMenu.Item>
26
+ * <ContextMenu.Separator />
27
+ * <ContextMenu.Item checked={true}>Show Bookmarks</ContextMenu.Item>
28
+ * </ContextMenu.Content>
29
+ * </ContextMenu>
30
+ * ```
31
+ */
32
+ const ContextMenuRoot = ({ children, className, onOpenChange, theme: themeOverrides, ...props }) => {
33
+ const [isOpen, setIsOpen] = useState(false);
34
+ const [cursorPosition, setCursorPosition] = useState(null);
35
+ const contentRef = useRef(null);
36
+ const triggerRef = useRef(null);
37
+ // Handle open state changes
38
+ const handleOpenChange = useCallback((value) => {
39
+ setIsOpen(value);
40
+ onOpenChange === null || onOpenChange === void 0 ? void 0 : onOpenChange(value);
41
+ // Get cursor position from trigger element when opening
42
+ if (value && triggerRef.current) {
43
+ const storedPosition = triggerRef.current.__cursorPosition;
44
+ if (storedPosition) {
45
+ setCursorPosition(storedPosition);
46
+ }
47
+ }
48
+ }, [onOpenChange]);
49
+ // Merge theme with overrides
50
+ const theme = {
51
+ ...contextMenuTheme,
52
+ ...(themeOverrides || {}),
53
+ };
54
+ // Context value
55
+ const contextValue = {
56
+ isOpen,
57
+ onOpenChange: handleOpenChange,
58
+ cursorPosition,
59
+ contentRef,
60
+ triggerRef,
61
+ theme,
62
+ };
63
+ // Prevent browser context menu on the entire component
64
+ useEffect(() => {
65
+ const preventContextMenu = (e) => {
66
+ var _a;
67
+ if ((_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.contains(e.target)) {
68
+ e.preventDefault();
69
+ }
70
+ };
71
+ document.addEventListener("contextmenu", preventContextMenu);
72
+ return () => document.removeEventListener("contextmenu", preventContextMenu);
73
+ }, []);
74
+ return (React__default.createElement(ContextMenuContext.Provider, { value: contextValue },
75
+ React__default.createElement("div", { className: cn("context-menu", className), "data-state": isOpen ? "open" : "closed", ...props }, children)));
76
+ };
77
+ ContextMenuRoot.displayName = "ContextMenu";
78
+ // Export as a unified component with sub-components
79
+ const ContextMenu = Object.assign(ContextMenuRoot, {
80
+ Trigger: ContextMenuTrigger,
81
+ Content: ContextMenuContent,
82
+ Item: ContextMenuItem,
83
+ Sub: ContextMenuSub,
84
+ SubTrigger: ContextMenuSubTrigger,
85
+ SubContent: ContextMenuSubContent,
86
+ Separator: ContextMenuSeparator,
87
+ Label: ContextMenuLabel,
88
+ });
89
+
90
+ export { ContextMenu };
@@ -0,0 +1,27 @@
1
+ import { dropdownTheme } from '../Dropdown/Dropdown.theme.js';
2
+
3
+ const contextMenuTheme = {
4
+ // Inherit all Dropdown styles for consistency
5
+ ...dropdownTheme,
6
+ // Content uses the same styling as Dropdown menu
7
+ contentStyle: dropdownTheme.menuStyle.replace("flex", "flex-col p-1"),
8
+ // Items use exact same styling as Dropdown items (with space for check icon)
9
+ itemStyle: `${dropdownTheme.itemBaseStyle} data-[checked=true]:pl-8`,
10
+ // Checked state - tick icon positioning (absolute left)
11
+ itemCheckedStyle: "absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--color-text-primary)] " +
12
+ "dark:text-[var(--color-neutral-200)]",
13
+ // Submenu trigger - based on Dropdown item style with chevron space
14
+ subTriggerStyle: `${dropdownTheme.itemBaseStyle} pr-8 data-[state=open]:bg-[var(--color-background-tertiary)] ` +
15
+ "dark:data-[state=open]:bg-[var(--color-neutral-700)]",
16
+ // Submenu content - similar to main menu but slightly smaller
17
+ subContentStyle: "min-w-[180px] max-h-[400px] overflow-y-auto rounded-[var(--dropdown-radius)] border border-[var(--color-border)] " +
18
+ "bg-[var(--color-background-secondary)] shadow-xl flex flex-col p-1 focus:outline-none " +
19
+ "dark:bg-[var(--color-neutral-800)] dark:border-[var(--color-neutral-700)]",
20
+ // Separator - horizontal divider
21
+ separatorStyle: "h-px bg-[var(--color-border)] my-1 -mx-1 " +
22
+ "dark:bg-[var(--color-neutral-700)]",
23
+ // Label - section header (matches Dropdown section title)
24
+ labelStyle: dropdownTheme.sectionTitleStyle,
25
+ };
26
+
27
+ export { contextMenuTheme };
@@ -0,0 +1,194 @@
1
+ import React__default, { useRef, useState, useCallback, useEffect } from 'react';
2
+ import { useReducedMotion, AnimatePresence, motion } from 'motion/react';
3
+ import { createPortal } from 'react-dom';
4
+ import { cn } from '../../../utils/cn.js';
5
+ import { useContextMenuContext } from './ContextMenuContext.js';
6
+
7
+ /**
8
+ * ContextMenuContent - Renders the menu at cursor position with keyboard navigation
9
+ */
10
+ const ContextMenuContent = React__default.forwardRef(({ children, className, animation = true, "aria-label": ariaLabel, ...props }, ref) => {
11
+ const { isOpen, onOpenChange, cursorPosition, contentRef, triggerRef, theme, } = useContextMenuContext();
12
+ const shouldReduceMotion = useReducedMotion();
13
+ const localRef = useRef(null);
14
+ const [focusedIndex, setFocusedIndex] = useState(-1);
15
+ // Initialize position from cursorPosition if available, otherwise use 0,0
16
+ const [position, setPosition] = useState(() => cursorPosition || { x: 0, y: 0 });
17
+ // Get all menu items (not separators or labels)
18
+ const getMenuItems = useCallback(() => {
19
+ if (!localRef.current)
20
+ return [];
21
+ return Array.from(localRef.current.querySelectorAll('[role="menuitem"]:not([data-disabled="true"])'));
22
+ }, []);
23
+ // Focus a menu item by index
24
+ const focusItem = useCallback((index) => {
25
+ var _a;
26
+ const items = getMenuItems();
27
+ if (index >= 0 && index < items.length) {
28
+ (_a = items[index]) === null || _a === void 0 ? void 0 : _a.focus();
29
+ setFocusedIndex(index);
30
+ }
31
+ }, [getMenuItems]);
32
+ // Keyboard navigation handler
33
+ const handleKeyDown = useCallback((e) => {
34
+ const items = getMenuItems();
35
+ const currentIndex = focusedIndex;
36
+ switch (e.key) {
37
+ case "ArrowDown":
38
+ e.preventDefault();
39
+ const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
40
+ focusItem(nextIndex);
41
+ break;
42
+ case "ArrowUp":
43
+ e.preventDefault();
44
+ const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
45
+ focusItem(prevIndex);
46
+ break;
47
+ case "Home":
48
+ e.preventDefault();
49
+ focusItem(0);
50
+ break;
51
+ case "End":
52
+ e.preventDefault();
53
+ focusItem(items.length - 1);
54
+ break;
55
+ case "Escape":
56
+ e.preventDefault();
57
+ onOpenChange(false);
58
+ break;
59
+ case "Tab":
60
+ e.preventDefault();
61
+ onOpenChange(false);
62
+ break;
63
+ }
64
+ }, [focusedIndex, focusItem, getMenuItems, onOpenChange]);
65
+ // Click outside to close - memoize handler to prevent unnecessary re-renders
66
+ const handleClickOutside = useCallback((e) => {
67
+ var _a;
68
+ // Skip if clicking on the trigger element
69
+ if (e.target === triggerRef.current ||
70
+ ((_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.contains(e.target))) {
71
+ return;
72
+ }
73
+ onOpenChange(false);
74
+ }, [onOpenChange, triggerRef]);
75
+ // Only attach click outside listener when menu is open
76
+ useEffect(() => {
77
+ if (!isOpen)
78
+ return;
79
+ const listener = (event) => {
80
+ const el = localRef.current;
81
+ if (!el || el.contains(event.target)) {
82
+ return;
83
+ }
84
+ handleClickOutside(event);
85
+ };
86
+ // Small delay to prevent immediate closing from the same click that opened the menu
87
+ const timeoutId = setTimeout(() => {
88
+ document.addEventListener("mousedown", listener);
89
+ document.addEventListener("touchstart", listener);
90
+ }, 0);
91
+ return () => {
92
+ clearTimeout(timeoutId);
93
+ document.removeEventListener("mousedown", listener);
94
+ document.removeEventListener("touchstart", listener);
95
+ };
96
+ }, [isOpen, handleClickOutside]);
97
+ // Update position when menu opens
98
+ useEffect(() => {
99
+ if (isOpen && cursorPosition) {
100
+ // Get cursor position from trigger
101
+ const { x, y } = cursorPosition;
102
+ // Adjust position to keep menu in viewport
103
+ const menuWidth = 220; // min-w-[220px]
104
+ const menuHeight = 400; // max estimated height
105
+ let adjustedX = x;
106
+ let adjustedY = y;
107
+ // Check right edge
108
+ if (x + menuWidth > window.innerWidth) {
109
+ adjustedX = window.innerWidth - menuWidth - 10;
110
+ }
111
+ // Check bottom edge
112
+ if (y + menuHeight > window.innerHeight) {
113
+ adjustedY = window.innerHeight - menuHeight - 10;
114
+ }
115
+ // Keep minimum padding from edges
116
+ adjustedX = Math.max(10, adjustedX);
117
+ adjustedY = Math.max(10, adjustedY);
118
+ setPosition({ x: adjustedX, y: adjustedY });
119
+ // Focus first item after a small delay
120
+ requestAnimationFrame(() => {
121
+ const items = getMenuItems();
122
+ if (items.length > 0) {
123
+ focusItem(0);
124
+ }
125
+ });
126
+ }
127
+ else {
128
+ setFocusedIndex(-1);
129
+ }
130
+ }, [isOpen, cursorPosition, getMenuItems, focusItem]);
131
+ // Close menu on scroll
132
+ useEffect(() => {
133
+ if (!isOpen)
134
+ return;
135
+ const handleScroll = () => {
136
+ onOpenChange(false);
137
+ };
138
+ // Use capture phase to catch all scroll events
139
+ window.addEventListener("scroll", handleScroll, true);
140
+ return () => window.removeEventListener("scroll", handleScroll, true);
141
+ }, [isOpen, onOpenChange]);
142
+ // Combine refs
143
+ const handleRef = useCallback((el) => {
144
+ localRef.current = el;
145
+ if (typeof ref === "function") {
146
+ ref(el);
147
+ }
148
+ else if (ref) {
149
+ ref.current = el;
150
+ }
151
+ if (contentRef) {
152
+ contentRef.current = el;
153
+ }
154
+ }, [ref, contentRef]);
155
+ // Animation variants
156
+ const menuVariants = {
157
+ hidden: {
158
+ opacity: 0,
159
+ scale: 0.95,
160
+ },
161
+ visible: {
162
+ opacity: 1,
163
+ scale: 1,
164
+ },
165
+ exit: {
166
+ opacity: 0,
167
+ scale: 0.95,
168
+ },
169
+ };
170
+ // Extract DOM props to avoid conflicts with Framer Motion
171
+ const { onAnimationStart, onAnimationEnd, onDrag, onDragStart, onDragEnd, ...domProps } = props;
172
+ // Detect dark mode from document
173
+ const isDarkMode = typeof document !== "undefined" &&
174
+ (document.documentElement.classList.contains("dark") ||
175
+ document.body.classList.contains("dark") ||
176
+ document.querySelector(".dark") !== null);
177
+ const menuContent = (React__default.createElement(AnimatePresence, null, isOpen && (React__default.createElement(motion.div, { ref: handleRef, className: cn("fixed z-50", isDarkMode && "dark", theme.contentStyle, className), style: {
178
+ top: `${position.y}px`,
179
+ left: `${position.x}px`,
180
+ }, role: "menu", "aria-label": ariaLabel || "Context menu", tabIndex: -1, onKeyDown: handleKeyDown, variants: shouldReduceMotion || !animation ? {} : menuVariants, initial: shouldReduceMotion || !animation ? undefined : "hidden", animate: shouldReduceMotion || !animation ? undefined : "visible", exit: shouldReduceMotion || !animation ? undefined : "exit", transition: shouldReduceMotion || !animation
181
+ ? { duration: 0 }
182
+ : {
183
+ type: "spring",
184
+ stiffness: 500,
185
+ damping: 30,
186
+ }, "data-state": isOpen ? "open" : "closed", ...domProps }, children))));
187
+ // Always use portal for context menus
188
+ return typeof document !== "undefined"
189
+ ? createPortal(menuContent, document.body)
190
+ : null;
191
+ });
192
+ ContextMenuContent.displayName = "ContextMenuContent";
193
+
194
+ export { ContextMenuContent };
@@ -0,0 +1,22 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ // Main context menu context
4
+ const ContextMenuContext = createContext(null);
5
+ const useContextMenuContext = () => {
6
+ const context = useContext(ContextMenuContext);
7
+ if (!context) {
8
+ throw new Error("ContextMenu compound components must be used within a ContextMenu component");
9
+ }
10
+ return context;
11
+ };
12
+ // Submenu context (for nested menus)
13
+ const SubMenuContext = createContext(null);
14
+ const useSubMenuContext = () => {
15
+ const context = useContext(SubMenuContext);
16
+ if (!context) {
17
+ throw new Error("ContextMenu.Sub compound components must be used within a ContextMenu.Sub component");
18
+ }
19
+ return context;
20
+ };
21
+
22
+ export { ContextMenuContext, SubMenuContext, useContextMenuContext, useSubMenuContext };
@@ -0,0 +1,35 @@
1
+ import React__default, { useCallback } from 'react';
2
+ import { CheckIcon } from '@heroicons/react/24/outline';
3
+ import { cn } from '../../../utils/cn.js';
4
+ import { useContextMenuContext } from './ContextMenuContext.js';
5
+ import { MenuItem } from '../MenuItem/MenuItem.js';
6
+
7
+ /**
8
+ * ContextMenuItem - Individual menu item with optional check indicator.
9
+ * Built on shared MenuItem, adds checked state and ContextMenu context integration.
10
+ */
11
+ const ContextMenuItem = React__default.forwardRef(({ children, className, checked = false, disabled = false, isDanger = false, shortcut, onSelect, startContent, endContent, description, ...props }, ref) => {
12
+ const { onOpenChange, theme } = useContextMenuContext();
13
+ const handleActivate = useCallback(() => {
14
+ if (disabled)
15
+ return;
16
+ onSelect === null || onSelect === void 0 ? void 0 : onSelect();
17
+ onOpenChange(false);
18
+ }, [disabled, onSelect, onOpenChange]);
19
+ // Combine checked icon with startContent (CheckIcon uses absolute positioning)
20
+ const hasPrefix = checked || !!startContent;
21
+ const resolvedStartContent = hasPrefix ? (React__default.createElement(React__default.Fragment, null,
22
+ checked && (React__default.createElement(CheckIcon, { className: theme.itemCheckedStyle, "aria-hidden": "true" })),
23
+ startContent)) : undefined;
24
+ // Build end content: user's endContent OR shortcut display
25
+ const resolvedEndContent = endContent ||
26
+ (shortcut ? (React__default.createElement("span", { className: "text-xs text-[var(--color-text-muted)] font-mono" }, shortcut)) : undefined);
27
+ return (React__default.createElement(MenuItem, { ref: ref, role: "menuitem", disabled: disabled, isDanger: isDanger, startContent: resolvedStartContent, endContent: resolvedEndContent, description: description, onActivate: handleActivate, "data-checked": checked, "aria-checked": checked ? "true" : undefined, className: cn(checked && "pl-8", className), theme: {
28
+ baseStyle: theme.itemStyle,
29
+ dangerStyle: theme.itemDangerStyle,
30
+ focusStyle: theme.itemFocusStyle,
31
+ }, ...props }, children));
32
+ });
33
+ ContextMenuItem.displayName = "ContextMenuItem";
34
+
35
+ export { ContextMenuItem };
@@ -0,0 +1,14 @@
1
+ import React__default from 'react';
2
+ import { cn } from '../../../utils/cn.js';
3
+ import { useContextMenuContext } from './ContextMenuContext.js';
4
+
5
+ /**
6
+ * ContextMenuLabel - Non-interactive section header
7
+ */
8
+ const ContextMenuLabel = React__default.forwardRef(({ children, className, ...props }, ref) => {
9
+ const { theme } = useContextMenuContext();
10
+ return (React__default.createElement("div", { ref: ref, className: cn(theme.labelStyle, className), ...props }, children));
11
+ });
12
+ ContextMenuLabel.displayName = "ContextMenuLabel";
13
+
14
+ export { ContextMenuLabel };
@@ -0,0 +1,14 @@
1
+ import React__default from 'react';
2
+ import { cn } from '../../../utils/cn.js';
3
+ import { useContextMenuContext } from './ContextMenuContext.js';
4
+
5
+ /**
6
+ * ContextMenuSeparator - Visual divider between menu items
7
+ */
8
+ const ContextMenuSeparator = React__default.forwardRef(({ className, ...props }, ref) => {
9
+ const { theme } = useContextMenuContext();
10
+ return (React__default.createElement("div", { ref: ref, role: "separator", "aria-orientation": "horizontal", className: cn(theme.separatorStyle, className), ...props }));
11
+ });
12
+ ContextMenuSeparator.displayName = "ContextMenuSeparator";
13
+
14
+ export { ContextMenuSeparator };