@elizaos/client 1.5.5-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +350 -0
- package/dist/assets/empty-module-CLMscLYw.js +1 -0
- package/dist/assets/main-BBZ_3lkn.css +5999 -0
- package/dist/assets/main-C5zNUkXH.js +7 -0
- package/dist/assets/main-Dz64ENQg.js +614 -0
- package/dist/assets/react-vendor-DM5m98rr.js +545 -0
- package/dist/assets/ui-vendor-BQCqNqg0.js +1 -0
- package/dist/elizaos-avatar.png +0 -0
- package/dist/elizaos-icon.png +0 -0
- package/dist/elizaos-logo-light.png +0 -0
- package/dist/elizaos.webp +0 -0
- package/dist/favicon.ico +0 -0
- package/dist/images/agents/agent1.png +0 -0
- package/dist/images/agents/agent2.png +0 -0
- package/dist/images/agents/agent3.png +0 -0
- package/dist/images/agents/agent4.png +0 -0
- package/dist/images/agents/agent5.png +0 -0
- package/dist/index.html +14 -0
- package/index.html +24 -0
- package/package.json +159 -0
- package/postcss.config.js +3 -0
- package/public/elizaos-avatar.png +0 -0
- package/public/elizaos-icon.png +0 -0
- package/public/elizaos-logo-light.png +0 -0
- package/public/elizaos.webp +0 -0
- package/public/favicon.ico +0 -0
- package/public/images/agents/agent1.png +0 -0
- package/public/images/agents/agent2.png +0 -0
- package/public/images/agents/agent3.png +0 -0
- package/public/images/agents/agent4.png +0 -0
- package/public/images/agents/agent5.png +0 -0
- package/src/App.tsx +222 -0
- package/src/components/AgentDetailsPanel.tsx +147 -0
- package/src/components/ChatInputArea.tsx +196 -0
- package/src/components/ChatMessageListComponent.tsx +139 -0
- package/src/components/actionTool.tsx +186 -0
- package/src/components/add-agent-card.tsx +77 -0
- package/src/components/agent-action-viewer.tsx +816 -0
- package/src/components/agent-avatar-stack.tsx +121 -0
- package/src/components/agent-card.cy.tsx +259 -0
- package/src/components/agent-card.tsx +177 -0
- package/src/components/agent-creator.tsx +142 -0
- package/src/components/agent-log-viewer.tsx +645 -0
- package/src/components/agent-memory-edit-overlay.tsx +461 -0
- package/src/components/agent-memory-viewer.tsx +504 -0
- package/src/components/agent-settings.tsx +270 -0
- package/src/components/agent-sidebar.tsx +178 -0
- package/src/components/api-key-dialog.tsx +113 -0
- package/src/components/app-sidebar.tsx +685 -0
- package/src/components/array-input.tsx +116 -0
- package/src/components/audio-recorder.tsx +292 -0
- package/src/components/avatar-panel.tsx +141 -0
- package/src/components/character-form.tsx +1138 -0
- package/src/components/chat.tsx +1813 -0
- package/src/components/combobox.tsx +187 -0
- package/src/components/confirmation-dialog.tsx +59 -0
- package/src/components/connection-error-banner.tsx +101 -0
- package/src/components/connection-status.cy.tsx +73 -0
- package/src/components/connection-status.tsx +155 -0
- package/src/components/copy-button.tsx +35 -0
- package/src/components/delete-button.tsx +24 -0
- package/src/components/env-settings.tsx +261 -0
- package/src/components/group-card.tsx +160 -0
- package/src/components/group-panel.tsx +543 -0
- package/src/components/input-copy.tsx +21 -0
- package/src/components/logs-page.tsx +41 -0
- package/src/components/media-content.tsx +385 -0
- package/src/components/memory-graph.tsx +170 -0
- package/src/components/missing-secrets-dialog.tsx +72 -0
- package/src/components/onboarding-tour.tsx +247 -0
- package/src/components/page-title.tsx +8 -0
- package/src/components/plugins-panel.tsx +383 -0
- package/src/components/profile-card.tsx +66 -0
- package/src/components/profile-overlay.tsx +283 -0
- package/src/components/retry-button.tsx +28 -0
- package/src/components/secret-panel.tsx +1505 -0
- package/src/components/server-management.tsx +264 -0
- package/src/components/split-button.tsx +148 -0
- package/src/components/stop-agent-button.tsx +99 -0
- package/src/components/ui/alert-dialog.cy.tsx +333 -0
- package/src/components/ui/alert-dialog.tsx +115 -0
- package/src/components/ui/alert.tsx +49 -0
- package/src/components/ui/avatar.cy.tsx +180 -0
- package/src/components/ui/avatar.tsx +57 -0
- package/src/components/ui/badge.cy.tsx +146 -0
- package/src/components/ui/badge.tsx +43 -0
- package/src/components/ui/button.cy.tsx +177 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/card.cy.tsx +160 -0
- package/src/components/ui/card.tsx +73 -0
- package/src/components/ui/chat/animated-markdown.tsx +59 -0
- package/src/components/ui/chat/chat-bubble.tsx +178 -0
- package/src/components/ui/chat/chat-container.tsx +51 -0
- package/src/components/ui/chat/chat-input.cy.tsx +169 -0
- package/src/components/ui/chat/chat-input.tsx +47 -0
- package/src/components/ui/chat/chat-message-list.tsx +61 -0
- package/src/components/ui/chat/chat-tts-button.tsx +199 -0
- package/src/components/ui/chat/code-block.tsx +79 -0
- package/src/components/ui/chat/expandable-chat.tsx +131 -0
- package/src/components/ui/chat/hooks/useAutoScroll.ts +86 -0
- package/src/components/ui/chat/markdown.tsx +209 -0
- package/src/components/ui/chat/message-loading.tsx +48 -0
- package/src/components/ui/checkbox.cy.tsx +170 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.cy.tsx +283 -0
- package/src/components/ui/collapsible.tsx +9 -0
- package/src/components/ui/command.cy.tsx +313 -0
- package/src/components/ui/command.tsx +143 -0
- package/src/components/ui/dialog.cy.tsx +279 -0
- package/src/components/ui/dialog.tsx +104 -0
- package/src/components/ui/dropdown-menu.cy.tsx +273 -0
- package/src/components/ui/dropdown-menu.tsx +281 -0
- package/src/components/ui/input.cy.tsx +82 -0
- package/src/components/ui/input.tsx +27 -0
- package/src/components/ui/label.cy.tsx +157 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/resizable.tsx +42 -0
- package/src/components/ui/scroll-area.cy.tsx +242 -0
- package/src/components/ui/scroll-area.tsx +46 -0
- package/src/components/ui/select.cy.tsx +277 -0
- package/src/components/ui/select.tsx +155 -0
- package/src/components/ui/separator.cy.tsx +145 -0
- package/src/components/ui/separator.tsx +29 -0
- package/src/components/ui/sheet.cy.tsx +324 -0
- package/src/components/ui/sheet.tsx +119 -0
- package/src/components/ui/sidebar.tsx +734 -0
- package/src/components/ui/skeleton.cy.tsx +149 -0
- package/src/components/ui/skeleton.tsx +17 -0
- package/src/components/ui/split-button.cy.tsx +274 -0
- package/src/components/ui/split-button.tsx +112 -0
- package/src/components/ui/switch.tsx +28 -0
- package/src/components/ui/tabs.cy.tsx +271 -0
- package/src/components/ui/tabs.tsx +53 -0
- package/src/components/ui/textarea.cy.tsx +136 -0
- package/src/components/ui/textarea.tsx +26 -0
- package/src/components/ui/toast.cy.tsx +209 -0
- package/src/components/ui/toast.tsx +126 -0
- package/src/components/ui/toaster.tsx +29 -0
- package/src/components/ui/tooltip.cy.tsx +244 -0
- package/src/components/ui/tooltip.tsx +30 -0
- package/src/config/agent-templates.ts +349 -0
- package/src/config/voice-models.ts +181 -0
- package/src/constants.ts +23 -0
- package/src/context/AuthContext.tsx +44 -0
- package/src/context/ConnectionContext.tsx +194 -0
- package/src/entry.tsx +9 -0
- package/src/hooks/__tests__/use-agent-tab-state.test.ts +137 -0
- package/src/hooks/__tests__/use-agent-update.test.tsx +250 -0
- package/src/hooks/__tests__/use-character-convert.test.ts +102 -0
- package/src/hooks/__tests__/use-panel-width-state.test.ts +243 -0
- package/src/hooks/__tests__/use-sidebar-state.test.ts +117 -0
- package/src/hooks/use-agent-management.ts +130 -0
- package/src/hooks/use-agent-tab-state.ts +74 -0
- package/src/hooks/use-agent-update.ts +469 -0
- package/src/hooks/use-character-convert.ts +138 -0
- package/src/hooks/use-confirmation.ts +55 -0
- package/src/hooks/use-delete-agent.ts +123 -0
- package/src/hooks/use-dm-channels.ts +198 -0
- package/src/hooks/use-elevenlabs-voices.ts +83 -0
- package/src/hooks/use-file-upload.ts +224 -0
- package/src/hooks/use-mobile.tsx +19 -0
- package/src/hooks/use-onboarding.tsx +49 -0
- package/src/hooks/use-panel-width-state.ts +147 -0
- package/src/hooks/use-partial-update.ts +288 -0
- package/src/hooks/use-plugin-details.ts +462 -0
- package/src/hooks/use-plugins.ts +119 -0
- package/src/hooks/use-query-hooks.ts +1263 -0
- package/src/hooks/use-server-agents.ts +62 -0
- package/src/hooks/use-server-version.tsx +47 -0
- package/src/hooks/use-sidebar-state.ts +50 -0
- package/src/hooks/use-socket-chat.ts +264 -0
- package/src/hooks/use-toast.ts +260 -0
- package/src/hooks/use-version.tsx +64 -0
- package/src/index.css +146 -0
- package/src/lib/api-client-config.ts +53 -0
- package/src/lib/api-type-mappers.ts +196 -0
- package/src/lib/export-utils.ts +123 -0
- package/src/lib/logger.ts +19 -0
- package/src/lib/media-utils.ts +170 -0
- package/src/lib/pca.test.ts +17 -0
- package/src/lib/pca.ts +52 -0
- package/src/lib/socketio-manager.ts +664 -0
- package/src/lib/utils.ts +168 -0
- package/src/main.tsx +16 -0
- package/src/mocks/empty-module.ts +12 -0
- package/src/mocks/node-module.ts +57 -0
- package/src/polyfills.ts +37 -0
- package/src/routes/agent-detail.tsx +30 -0
- package/src/routes/agent-list.tsx +27 -0
- package/src/routes/agent-settings.tsx +48 -0
- package/src/routes/character-detail.tsx +52 -0
- package/src/routes/character-form.tsx +79 -0
- package/src/routes/character-list.tsx +38 -0
- package/src/routes/chat.tsx +128 -0
- package/src/routes/createAgent.tsx +13 -0
- package/src/routes/group-new.tsx +50 -0
- package/src/routes/group.tsx +29 -0
- package/src/routes/home.tsx +218 -0
- package/src/routes/not-found.tsx +71 -0
- package/src/test/setup.ts +154 -0
- package/src/types/crypto-browserify.d.ts +4 -0
- package/src/types/index.ts +13 -0
- package/src/types/rooms.ts +8 -0
- package/src/types.ts +84 -0
- package/src/vite-env.d.ts +40 -0
- package/tailwind.config.ts +90 -0
- package/tsconfig.json +10 -0
- package/vite.config.ts +102 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/// <reference types="cypress" />
|
|
2
|
+
/// <reference path="../../../cypress/support/types.d.ts" />
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { Skeleton } from './skeleton';
|
|
6
|
+
|
|
7
|
+
describe('Skeleton Component', () => {
|
|
8
|
+
it('renders correctly with default props', () => {
|
|
9
|
+
cy.mount(<Skeleton />);
|
|
10
|
+
|
|
11
|
+
cy.get('[data-testid="skeleton"]').should('exist');
|
|
12
|
+
cy.get('[data-testid="skeleton"]').should('have.class', 'animate-pulse');
|
|
13
|
+
cy.get('[data-testid="skeleton"]').should('have.class', 'rounded-md');
|
|
14
|
+
cy.get('[data-testid="skeleton"]').should('have.class', 'bg-primary/10');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('applies custom className', () => {
|
|
18
|
+
cy.mount(<Skeleton className="w-full h-20" />);
|
|
19
|
+
|
|
20
|
+
cy.get('[data-testid="skeleton"]').should('have.class', 'w-full').should('have.class', 'h-20');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('renders multiple skeletons for loading lists', () => {
|
|
24
|
+
cy.mount(
|
|
25
|
+
<div className="space-y-2">
|
|
26
|
+
<Skeleton className="h-4 w-full" />
|
|
27
|
+
<Skeleton className="h-4 w-3/4" />
|
|
28
|
+
<Skeleton className="h-4 w-1/2" />
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
cy.get('[data-testid="skeleton"]').should('have.length', 3);
|
|
33
|
+
cy.get('[data-testid="skeleton"]').first().should('have.class', 'w-full');
|
|
34
|
+
cy.get('[data-testid="skeleton"]').eq(1).should('have.class', 'w-3/4');
|
|
35
|
+
cy.get('[data-testid="skeleton"]').last().should('have.class', 'w-1/2');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('renders card skeleton layout', () => {
|
|
39
|
+
cy.mount(
|
|
40
|
+
<div className="rounded-lg border p-4">
|
|
41
|
+
<div className="flex items-center space-x-4">
|
|
42
|
+
<Skeleton className="h-12 w-12 rounded-full" />
|
|
43
|
+
<div className="space-y-2">
|
|
44
|
+
<Skeleton className="h-4 w-[250px]" />
|
|
45
|
+
<Skeleton className="h-4 w-[200px]" />
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
cy.get('[data-testid="skeleton"]').should('have.length', 3);
|
|
52
|
+
cy.get('.rounded-full').should('exist');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('supports different shapes', () => {
|
|
56
|
+
cy.mount(
|
|
57
|
+
<div className="space-y-4">
|
|
58
|
+
<Skeleton className="h-12 w-12 rounded-full" />
|
|
59
|
+
<Skeleton className="h-20 w-full rounded-lg" />
|
|
60
|
+
<Skeleton className="h-4 w-32" />
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
cy.get('[data-testid="skeleton"]').first().should('have.class', 'rounded-full');
|
|
65
|
+
cy.get('[data-testid="skeleton"]').eq(1).should('have.class', 'rounded-lg');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('works for text loading', () => {
|
|
69
|
+
cy.mount(
|
|
70
|
+
<div className="space-y-2">
|
|
71
|
+
<h2 className="text-2xl font-bold">
|
|
72
|
+
<Skeleton className="h-8 w-48" />
|
|
73
|
+
</h2>
|
|
74
|
+
<p>
|
|
75
|
+
<Skeleton className="h-4 w-full" />
|
|
76
|
+
<Skeleton className="h-4 w-full" />
|
|
77
|
+
<Skeleton className="h-4 w-3/4" />
|
|
78
|
+
</p>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
cy.get('h2').find('[data-testid="skeleton"]').should('exist');
|
|
83
|
+
cy.get('p').find('[data-testid="skeleton"]').should('have.length', 3);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('maintains proper sizing', () => {
|
|
87
|
+
cy.mount(<Skeleton className="h-32 w-64" />);
|
|
88
|
+
|
|
89
|
+
cy.get('[data-testid="skeleton"]').should('have.class', 'h-32').should('have.class', 'w-64');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('works for image placeholders', () => {
|
|
93
|
+
cy.mount(
|
|
94
|
+
<div className="grid grid-cols-3 gap-4">
|
|
95
|
+
<Skeleton className="aspect-square w-full rounded-lg" />
|
|
96
|
+
<Skeleton className="aspect-square w-full rounded-lg" />
|
|
97
|
+
<Skeleton className="aspect-square w-full rounded-lg" />
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
cy.get('[data-testid="skeleton"]').should('have.length', 3);
|
|
102
|
+
cy.get('[data-testid="skeleton"]').each(($el) => {
|
|
103
|
+
cy.wrap($el).should('have.class', 'aspect-square');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('works for form field loading', () => {
|
|
108
|
+
cy.mount(
|
|
109
|
+
<div className="space-y-4">
|
|
110
|
+
<div className="space-y-2">
|
|
111
|
+
<Skeleton className="h-4 w-24" /> {/* Label */}
|
|
112
|
+
<Skeleton className="h-10 w-full rounded-md" /> {/* Input */}
|
|
113
|
+
</div>
|
|
114
|
+
<div className="space-y-2">
|
|
115
|
+
<Skeleton className="h-4 w-32" /> {/* Label */}
|
|
116
|
+
<Skeleton className="h-24 w-full rounded-md" /> {/* Textarea */}
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
cy.get('[data-testid="skeleton"]').should('have.length', 4);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('supports data attributes', () => {
|
|
125
|
+
cy.mount(
|
|
126
|
+
<Skeleton data-testid="custom-skeleton" data-loading="true" className="h-20 w-full" />
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
cy.get('[data-testid="custom-skeleton"]')
|
|
130
|
+
.should('exist')
|
|
131
|
+
.should('have.attr', 'data-loading', 'true');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('animation is present', () => {
|
|
135
|
+
cy.mount(<Skeleton />);
|
|
136
|
+
|
|
137
|
+
cy.get('[data-testid="skeleton"]').should('have.class', 'animate-pulse');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('works in dark mode', () => {
|
|
141
|
+
cy.mount(
|
|
142
|
+
<div className="dark bg-gray-900 p-4">
|
|
143
|
+
<Skeleton className="h-20 w-full" />
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
cy.get('[data-testid="skeleton"]').should('be.visible');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils';
|
|
2
|
+
|
|
3
|
+
interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
'data-testid'?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function Skeleton({ className, ...props }: SkeletonProps) {
|
|
8
|
+
return (
|
|
9
|
+
<div
|
|
10
|
+
className={cn('animate-pulse rounded-md bg-primary/10', className)}
|
|
11
|
+
data-testid={props['data-testid'] || 'skeleton'}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { Skeleton };
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/// <reference types="cypress" />
|
|
2
|
+
/// <reference path="../../../cypress/support/types.d.ts" />
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { SplitButton } from './split-button';
|
|
6
|
+
|
|
7
|
+
describe('SplitButton Component', () => {
|
|
8
|
+
it('renders basic split button', () => {
|
|
9
|
+
cy.mount(
|
|
10
|
+
<SplitButton
|
|
11
|
+
mainAction={{ label: 'Save', onClick: () => {} }}
|
|
12
|
+
actions={[
|
|
13
|
+
{ label: 'Save as Draft', onClick: () => {} },
|
|
14
|
+
{ label: 'Save and Exit', onClick: () => {} },
|
|
15
|
+
]}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
cy.contains('button', 'Save').should('be.visible');
|
|
20
|
+
cy.get('button[aria-haspopup="menu"]').should('be.visible');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('handles main button click', () => {
|
|
24
|
+
const onClick = cy.stub();
|
|
25
|
+
|
|
26
|
+
cy.mount(
|
|
27
|
+
<SplitButton
|
|
28
|
+
mainAction={{ label: 'Submit', onClick }}
|
|
29
|
+
actions={[{ label: 'Submit and Close', onClick: () => {} }]}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
cy.contains('button', 'Submit').click();
|
|
34
|
+
cy.wrap(onClick).should('have.been.called');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('opens dropdown menu', () => {
|
|
38
|
+
cy.mount(
|
|
39
|
+
<SplitButton
|
|
40
|
+
mainAction={{ label: 'Export', onClick: () => {} }}
|
|
41
|
+
actions={[
|
|
42
|
+
{ label: 'Export as PDF', onClick: () => {} },
|
|
43
|
+
{ label: 'Export as CSV', onClick: () => {} },
|
|
44
|
+
{ label: 'Export as Excel', onClick: () => {} },
|
|
45
|
+
]}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Initially dropdown is closed
|
|
50
|
+
cy.contains('Export as PDF').should('not.exist');
|
|
51
|
+
|
|
52
|
+
// Click dropdown trigger
|
|
53
|
+
cy.get('button[aria-haspopup="menu"]').click();
|
|
54
|
+
|
|
55
|
+
// All options should be visible
|
|
56
|
+
cy.contains('Export as PDF').should('be.visible');
|
|
57
|
+
cy.contains('Export as CSV').should('be.visible');
|
|
58
|
+
cy.contains('Export as Excel').should('be.visible');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('handles dropdown action selection', () => {
|
|
62
|
+
const mainAction = cy.stub();
|
|
63
|
+
const draftAction = cy.stub();
|
|
64
|
+
const templateAction = cy.stub();
|
|
65
|
+
|
|
66
|
+
cy.mount(
|
|
67
|
+
<SplitButton
|
|
68
|
+
mainAction={{ label: 'Save', onClick: mainAction }}
|
|
69
|
+
actions={[
|
|
70
|
+
{ label: 'Save as Draft', onClick: draftAction },
|
|
71
|
+
{ label: 'Save as Template', onClick: templateAction },
|
|
72
|
+
]}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Open dropdown
|
|
77
|
+
cy.get('button[aria-haspopup="menu"]').click();
|
|
78
|
+
|
|
79
|
+
// Select draft option
|
|
80
|
+
cy.contains('Save as Draft').click();
|
|
81
|
+
cy.wrap(draftAction).should('have.been.called');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('supports different variants', () => {
|
|
85
|
+
cy.mount(
|
|
86
|
+
<div className="space-x-4">
|
|
87
|
+
<SplitButton
|
|
88
|
+
mainAction={{ label: 'Default', onClick: () => {} }}
|
|
89
|
+
actions={[{ label: 'Option', onClick: () => {} }]}
|
|
90
|
+
variant="default"
|
|
91
|
+
/>
|
|
92
|
+
<SplitButton
|
|
93
|
+
mainAction={{ label: 'Destructive', onClick: () => {} }}
|
|
94
|
+
actions={[{ label: 'Delete All', onClick: () => {} }]}
|
|
95
|
+
variant="destructive"
|
|
96
|
+
/>
|
|
97
|
+
<SplitButton
|
|
98
|
+
mainAction={{ label: 'Outline', onClick: () => {} }}
|
|
99
|
+
actions={[{ label: 'More', onClick: () => {} }]}
|
|
100
|
+
variant="outline"
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
cy.contains('button', 'Default').should('be.visible');
|
|
106
|
+
cy.contains('button', 'Destructive').should('have.class', 'bg-red-800');
|
|
107
|
+
cy.contains('button', 'Outline').should('have.class', 'border');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('supports different sizes', () => {
|
|
111
|
+
cy.mount(
|
|
112
|
+
<div className="space-x-4">
|
|
113
|
+
<SplitButton
|
|
114
|
+
mainAction={{ label: 'Small', onClick: () => {} }}
|
|
115
|
+
actions={[{ label: 'Option', onClick: () => {} }]}
|
|
116
|
+
size="sm"
|
|
117
|
+
/>
|
|
118
|
+
<SplitButton
|
|
119
|
+
mainAction={{ label: 'Default', onClick: () => {} }}
|
|
120
|
+
actions={[{ label: 'Option', onClick: () => {} }]}
|
|
121
|
+
/>
|
|
122
|
+
<SplitButton
|
|
123
|
+
mainAction={{ label: 'Large', onClick: () => {} }}
|
|
124
|
+
actions={[{ label: 'Option', onClick: () => {} }]}
|
|
125
|
+
size="lg"
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
cy.contains('button', 'Small').should('have.class', 'h-8');
|
|
131
|
+
cy.contains('button', 'Large').should('have.class', 'h-10');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('can be disabled', () => {
|
|
135
|
+
const onClick = cy.stub();
|
|
136
|
+
|
|
137
|
+
cy.mount(
|
|
138
|
+
<SplitButton
|
|
139
|
+
mainAction={{ label: 'Disabled', onClick }}
|
|
140
|
+
actions={[{ label: 'Option 1', onClick: () => {} }]}
|
|
141
|
+
disabled
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Both buttons should be disabled
|
|
146
|
+
cy.contains('button', 'Disabled').should('be.disabled');
|
|
147
|
+
cy.get('button[aria-haspopup="menu"]').should('be.disabled');
|
|
148
|
+
|
|
149
|
+
// Try clicking
|
|
150
|
+
cy.contains('button', 'Disabled').click({ force: true });
|
|
151
|
+
cy.wrap(onClick).should('not.have.been.called');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('supports individually disabled actions', () => {
|
|
155
|
+
const enabledAction = cy.stub();
|
|
156
|
+
const disabledAction = cy.stub();
|
|
157
|
+
|
|
158
|
+
cy.mount(
|
|
159
|
+
<SplitButton
|
|
160
|
+
mainAction={{ label: 'Print', onClick: () => {} }}
|
|
161
|
+
actions={[
|
|
162
|
+
{ label: 'Print Preview', onClick: enabledAction },
|
|
163
|
+
{ label: 'Print to PDF', onClick: disabledAction, disabled: true },
|
|
164
|
+
{ label: 'Print All', onClick: enabledAction },
|
|
165
|
+
]}
|
|
166
|
+
/>
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
cy.get('button[aria-haspopup="menu"]').click();
|
|
170
|
+
|
|
171
|
+
// Check disabled item exists
|
|
172
|
+
cy.contains('Print to PDF').should('exist');
|
|
173
|
+
// Disabled dropdown items behavior might vary, just verify enabled action works
|
|
174
|
+
|
|
175
|
+
// Click enabled item
|
|
176
|
+
cy.contains('Print All').click();
|
|
177
|
+
cy.wrap(enabledAction).should('have.been.called');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('closes dropdown after selection', () => {
|
|
181
|
+
cy.mount(
|
|
182
|
+
<SplitButton
|
|
183
|
+
mainAction={{ label: 'Share', onClick: () => {} }}
|
|
184
|
+
actions={[
|
|
185
|
+
{ label: 'Share via Email', onClick: () => {} },
|
|
186
|
+
{ label: 'Share via Link', onClick: () => {} },
|
|
187
|
+
]}
|
|
188
|
+
/>
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// Open dropdown
|
|
192
|
+
cy.get('button[aria-haspopup="menu"]').click();
|
|
193
|
+
cy.contains('Share via Email').should('be.visible');
|
|
194
|
+
|
|
195
|
+
// Select item
|
|
196
|
+
cy.contains('Share via Email').click();
|
|
197
|
+
|
|
198
|
+
// Dropdown should close
|
|
199
|
+
cy.contains('Share via Email').should('not.exist');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('supports custom className', () => {
|
|
203
|
+
cy.mount(
|
|
204
|
+
<SplitButton
|
|
205
|
+
mainAction={{ label: 'Custom', onClick: () => {} }}
|
|
206
|
+
actions={[{ label: 'Option', onClick: () => {} }]}
|
|
207
|
+
className="custom-split-button"
|
|
208
|
+
/>
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
cy.get('.custom-split-button').should('exist');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('maintains visual consistency', () => {
|
|
215
|
+
cy.mount(
|
|
216
|
+
<SplitButton
|
|
217
|
+
mainAction={{ label: 'Consistent', onClick: () => {} }}
|
|
218
|
+
actions={[
|
|
219
|
+
{ label: 'Option 1', onClick: () => {} },
|
|
220
|
+
{ label: 'Option 2', onClick: () => {} },
|
|
221
|
+
]}
|
|
222
|
+
variant="secondary"
|
|
223
|
+
/>
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// Both parts should have consistent styling
|
|
227
|
+
cy.contains('button', 'Consistent').should('be.visible');
|
|
228
|
+
cy.get('button[aria-haspopup="menu"]').should('be.visible');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('supports icons in actions', () => {
|
|
232
|
+
cy.mount(
|
|
233
|
+
<SplitButton
|
|
234
|
+
mainAction={{
|
|
235
|
+
label: 'Save',
|
|
236
|
+
onClick: () => {},
|
|
237
|
+
icon: <span>💾</span>,
|
|
238
|
+
}}
|
|
239
|
+
actions={[
|
|
240
|
+
{
|
|
241
|
+
label: 'Draft',
|
|
242
|
+
onClick: () => {},
|
|
243
|
+
icon: <span>📝</span>,
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
label: 'Template',
|
|
247
|
+
onClick: () => {},
|
|
248
|
+
icon: <span>📋</span>,
|
|
249
|
+
},
|
|
250
|
+
]}
|
|
251
|
+
/>
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
cy.contains('💾').should('be.visible');
|
|
255
|
+
cy.get('button[aria-haspopup="menu"]').click();
|
|
256
|
+
cy.contains('📝').should('be.visible');
|
|
257
|
+
cy.contains('📋').should('be.visible');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('supports destructive variant in dropdown', () => {
|
|
261
|
+
cy.mount(
|
|
262
|
+
<SplitButton
|
|
263
|
+
mainAction={{ label: 'Actions', onClick: () => {} }}
|
|
264
|
+
actions={[
|
|
265
|
+
{ label: 'Edit', onClick: () => {} },
|
|
266
|
+
{ label: 'Delete', onClick: () => {}, variant: 'destructive' },
|
|
267
|
+
]}
|
|
268
|
+
/>
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
cy.get('button[aria-haspopup="menu"]').click();
|
|
272
|
+
cy.contains('Delete').should('have.class', 'text-destructive');
|
|
273
|
+
});
|
|
274
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Button } from '@/components/ui/button';
|
|
3
|
+
import {
|
|
4
|
+
DropdownMenu,
|
|
5
|
+
DropdownMenuContent,
|
|
6
|
+
DropdownMenuItem,
|
|
7
|
+
DropdownMenuSeparator,
|
|
8
|
+
DropdownMenuTrigger,
|
|
9
|
+
} from '@/components/ui/dropdown-menu';
|
|
10
|
+
import { ChevronDown } from 'lucide-react';
|
|
11
|
+
import { cn } from '@/lib/utils';
|
|
12
|
+
|
|
13
|
+
export interface SplitButtonAction {
|
|
14
|
+
label: React.ReactNode;
|
|
15
|
+
onClick: () => void;
|
|
16
|
+
icon?: React.ReactNode;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
variant?: 'default' | 'destructive';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SplitButtonProps {
|
|
22
|
+
mainAction: SplitButtonAction;
|
|
23
|
+
actions: SplitButtonAction[];
|
|
24
|
+
variant?: 'default' | 'outline' | 'secondary' | 'ghost' | 'link' | 'destructive';
|
|
25
|
+
size?: 'default' | 'sm' | 'lg' | 'icon';
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
className?: string;
|
|
28
|
+
mainButtonClassName?: string;
|
|
29
|
+
dropdownButtonClassName?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const SplitButton = React.forwardRef<HTMLDivElement, SplitButtonProps>(
|
|
33
|
+
(
|
|
34
|
+
{
|
|
35
|
+
mainAction,
|
|
36
|
+
actions,
|
|
37
|
+
variant = 'default',
|
|
38
|
+
size = 'default',
|
|
39
|
+
disabled,
|
|
40
|
+
className,
|
|
41
|
+
mainButtonClassName,
|
|
42
|
+
dropdownButtonClassName,
|
|
43
|
+
},
|
|
44
|
+
ref
|
|
45
|
+
) => {
|
|
46
|
+
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
47
|
+
const [menuWidth, setMenuWidth] = React.useState<number>();
|
|
48
|
+
|
|
49
|
+
React.useImperativeHandle(ref, () => containerRef.current!, []);
|
|
50
|
+
|
|
51
|
+
React.useLayoutEffect(() => {
|
|
52
|
+
if (containerRef.current) {
|
|
53
|
+
setMenuWidth(containerRef.current.offsetWidth);
|
|
54
|
+
}
|
|
55
|
+
}, [actions.length, mainAction.label]);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div ref={containerRef} className={cn('flex w-full', className)}>
|
|
59
|
+
<Button
|
|
60
|
+
type="button"
|
|
61
|
+
variant={variant}
|
|
62
|
+
size={size}
|
|
63
|
+
onClick={mainAction.onClick}
|
|
64
|
+
disabled={disabled || mainAction.disabled}
|
|
65
|
+
className={cn(
|
|
66
|
+
'rounded-r-none flex-1',
|
|
67
|
+
variant === 'destructive' ? 'border-r border-red-700' : 'border-r-0',
|
|
68
|
+
mainButtonClassName
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
{mainAction.icon && <span className="mr-2">{mainAction.icon}</span>}
|
|
72
|
+
{mainAction.label}
|
|
73
|
+
</Button>
|
|
74
|
+
|
|
75
|
+
<DropdownMenu>
|
|
76
|
+
<DropdownMenuTrigger asChild>
|
|
77
|
+
<Button
|
|
78
|
+
type="button"
|
|
79
|
+
variant={variant}
|
|
80
|
+
size={size}
|
|
81
|
+
disabled={disabled}
|
|
82
|
+
className={cn('rounded-l-none px-2 flex-shrink-0', dropdownButtonClassName)}
|
|
83
|
+
>
|
|
84
|
+
<ChevronDown className="h-4 w-4" />
|
|
85
|
+
<span className="sr-only">More options</span>
|
|
86
|
+
</Button>
|
|
87
|
+
</DropdownMenuTrigger>
|
|
88
|
+
<DropdownMenuContent align="end" style={{ width: menuWidth }}>
|
|
89
|
+
{actions.map((action, index) => (
|
|
90
|
+
<React.Fragment key={index}>
|
|
91
|
+
<DropdownMenuItem
|
|
92
|
+
onClick={action.onClick}
|
|
93
|
+
disabled={action.disabled}
|
|
94
|
+
className={cn(
|
|
95
|
+
action.variant === 'destructive' &&
|
|
96
|
+
'text-destructive focus:text-destructive hover:bg-red-50 dark:hover:bg-red-950/50'
|
|
97
|
+
)}
|
|
98
|
+
>
|
|
99
|
+
{action.icon}
|
|
100
|
+
{action.label}
|
|
101
|
+
</DropdownMenuItem>
|
|
102
|
+
{index < actions.length - 1 && <DropdownMenuSeparator />}
|
|
103
|
+
</React.Fragment>
|
|
104
|
+
))}
|
|
105
|
+
</DropdownMenuContent>
|
|
106
|
+
</DropdownMenu>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
SplitButton.displayName = 'SplitButton';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import * as SwitchPrimitive from '@radix-ui/react-switch';
|
|
5
|
+
|
|
6
|
+
import { cn } from '@/lib/utils';
|
|
7
|
+
|
|
8
|
+
function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
|
9
|
+
return (
|
|
10
|
+
<SwitchPrimitive.Root
|
|
11
|
+
data-slot="switch"
|
|
12
|
+
className={cn(
|
|
13
|
+
'peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-4 w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer',
|
|
14
|
+
className
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
>
|
|
18
|
+
<SwitchPrimitive.Thumb
|
|
19
|
+
data-slot="switch-thumb"
|
|
20
|
+
className={cn(
|
|
21
|
+
'bg-background dark:data-[state=unchecked]:bg-primary-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block h-2.5 w-2.5 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%+10px)] data-[state=unchecked]:translate-x-0'
|
|
22
|
+
)}
|
|
23
|
+
/>
|
|
24
|
+
</SwitchPrimitive.Root>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { Switch };
|