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.
- package/README.md +100 -7
- package/dist/components/ai/CodeBlock/CodeBlock.js +1 -1
- package/dist/components/ai/PromptInput/PromptInput.js +14 -0
- package/dist/components/ai/StreamingResponse/MarkdownRenderer.js +1 -1
- package/dist/components/ai/index.js +27 -0
- package/dist/components/core/ContextMenu/ContextMenu.js +90 -0
- package/dist/components/core/ContextMenu/ContextMenu.theme.js +27 -0
- package/dist/components/core/ContextMenu/ContextMenuContent.js +194 -0
- package/dist/components/core/ContextMenu/ContextMenuContext.js +22 -0
- package/dist/components/core/ContextMenu/ContextMenuItem.js +35 -0
- package/dist/components/core/ContextMenu/ContextMenuLabel.js +14 -0
- package/dist/components/core/ContextMenu/ContextMenuSeparator.js +14 -0
- package/dist/components/core/ContextMenu/ContextMenuSub.js +40 -0
- package/dist/components/core/ContextMenu/ContextMenuSubContent.js +103 -0
- package/dist/components/core/ContextMenu/ContextMenuSubTrigger.js +61 -0
- package/dist/components/core/ContextMenu/ContextMenuTrigger.js +49 -0
- package/dist/components/core/DescriptionList/DescriptionList.js +43 -0
- package/dist/components/core/DescriptionList/DescriptionList.theme.js +8 -0
- package/dist/components/core/Notification/Notification.js +4 -0
- package/dist/components/core/Pill/Pill.animations.js +25 -0
- package/dist/components/core/Pill/Pill.js +119 -0
- package/dist/components/core/Pill/Pill.theme.js +44 -0
- package/dist/components/core/ProgressiveBlur/ProgressiveBlur.js +5 -6
- package/dist/components/core/index.d.ts +3 -0
- package/dist/components/core/index.js +5 -0
- package/dist/components/forms/ColorPicker/ColorPicker.animations.js +34 -0
- package/dist/components/forms/ColorPicker/ColorPicker.js +175 -0
- package/dist/components/forms/ColorPicker/ColorPicker.theme.js +20 -0
- package/dist/components/forms/ColorPicker/ColorPicker2DCanvas.js +91 -0
- package/dist/components/forms/ColorPicker/ColorPickerContent.js +72 -0
- package/dist/components/forms/ColorPicker/ColorPickerEyeDropper.js +50 -0
- package/dist/components/forms/ColorPicker/ColorPickerFormatSelector.js +27 -0
- package/dist/components/forms/ColorPicker/ColorPickerInput.js +103 -0
- package/dist/components/forms/ColorPicker/ColorPickerPresets.js +57 -0
- package/dist/components/forms/ColorPicker/ColorPickerSliders.js +45 -0
- package/dist/components/forms/ColorPicker/ColorPickerSwatch.js +38 -0
- package/dist/components/forms/ColorPicker/ColorPickerTrigger.js +38 -0
- package/dist/components/forms/ColorPicker/colorUtils.js +324 -0
- package/dist/components/forms/FileUpload/FileUpload.js +4 -0
- package/dist/components/forms/TimePicker/TimePickerContent.js +4 -0
- package/dist/components/forms/index.d.ts +1 -0
- package/dist/components/forms/index.js +15 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +16 -24
- package/package.json +1 -11
- package/dist/cli/commands/init.d.ts +0 -7
- package/dist/cli/commands/init.js +0 -82
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.js +0 -17
- package/dist/cli/utils/detectProject.d.ts +0 -9
- package/dist/cli/utils/detectProject.js +0 -126
- package/dist/cli/utils/injectCSS.d.ts +0 -8
- package/dist/cli/utils/injectCSS.js +0 -82
- package/dist/cli/utils/installDeps.d.ts +0 -2
- package/dist/cli/utils/installDeps.js +0 -44
- package/dist/cli/utils/logger.d.ts +0 -9
- package/dist/cli/utils/logger.js +0 -35
- package/dist/cli/utils/setupTailwind.d.ts +0 -7
- 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
|
-
|
|
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-
|
|
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 };
|