@nextclaw/ui 0.14.1-beta.0 → 0.14.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/assets/{api-C6EDHBDR.js → api-dQ6uQSZG.js} +3 -3
- package/dist/assets/{channels-list-page-DKj6m1Hc.js → channels-list-page-DfpjB8XB.js} +2 -2
- package/dist/assets/chat-page-D3QaEqjs.js +105 -0
- package/dist/assets/{config-split-page-CjlmJy8X.js → config-split-page-CeFfbaqc.js} +1 -1
- package/dist/assets/{confirm-dialog-B39X0pYp.js → confirm-dialog-C95Y4QPC.js} +1 -1
- package/dist/assets/{desktop-update-config-DfUQ8nly.js → desktop-update-config-CcK6jiEb.js} +1 -1
- package/dist/assets/{dist-CmMro3nV.js → dist-DTGzeil6.js} +1 -1
- package/dist/assets/{dist-BIYV2-Ia.js → dist-fGogBmPA.js} +1 -1
- package/dist/assets/doc-browser-CuBTcBEZ.js +1 -0
- package/dist/assets/{doc-browser-CDtUtcim.js → doc-browser-DVHhEb96.js} +1 -1
- package/dist/assets/{doc-browser-context-DZbC9zVB.js → doc-browser-context-BfMXwnrz.js} +1 -1
- package/dist/assets/{ellipsis-DDpgTzgx.js → ellipsis-D2F4KMiU.js} +1 -1
- package/dist/assets/{external-link-D2cBOCqn.js → external-link-CcgtBK26.js} +1 -1
- package/dist/assets/index-ByJCbQqr.css +1 -0
- package/dist/assets/index-DjXqiloY.js +103 -0
- package/dist/assets/mcp-marketplace-page-98-V__3u.js +1 -0
- package/dist/assets/mcp-marketplace-page-BNq_PNmO.js +40 -0
- package/dist/assets/model-config-page-aAUkZkSX.js +1 -0
- package/dist/assets/plus-1Rxl5FlV.js +1 -0
- package/dist/assets/provider-scoped-model-input-we4_Z4gP.js +1 -0
- package/dist/assets/providers-config-page-CtMkPSyu.js +1 -0
- package/dist/assets/{react-DYQXXGbH.js → react-CTqdhFMk.js} +1 -1
- package/dist/assets/remote-fQ7N_r8u.js +1 -0
- package/dist/assets/rotate-cw-CzfIeAJj.js +1 -0
- package/dist/assets/runtime-config-page-TD1UrTbG.js +1 -0
- package/dist/assets/{save-BNVTgBVn.js → save-C9fKWfVB.js} +1 -1
- package/dist/assets/{search-CKCzwLId.js → search-D5FQ5q0X.js} +1 -1
- package/dist/assets/{search-config-page-DG5Q2hF1.js → search-config-page-AMEJJNIS.js} +1 -1
- package/dist/assets/secrets-config-page-CcgJnvBO.js +3 -0
- package/dist/assets/{select-CIRwTNP4.js → select-Cu5n0E-3.js} +2 -2
- package/dist/assets/{tag-chip-DKhVFtTE.js → tag-chip-C5aOtjxu.js} +1 -1
- package/dist/assets/use-config-D8TESeo6.js +1 -0
- package/dist/assets/x-1qETdQ70.js +1 -0
- package/dist/index.html +14 -14
- package/package.json +9 -9
- package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +12 -63
- package/src/features/chat/components/conversation/chat-conversation-content.tsx +11 -50
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +15 -2
- package/src/features/chat/features/input/components/chat-input-bar.container.tsx +84 -6
- package/src/features/chat/features/input/hooks/use-chat-input-bar-query-state.ts +2 -0
- package/src/features/chat/features/input/hooks/use-chat-model-favorites.ts +87 -0
- package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +64 -24
- package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +1 -0
- package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +21 -12
- package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +80 -25
- package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +310 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message-test.utils.ts +43 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +1 -346
- package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +10 -9
- package/src/features/chat/features/message/utils/file-operation/diff.utils.ts +39 -36
- package/src/features/chat/features/message/utils/file-operation/line-builder.utils.ts +46 -38
- package/src/features/chat/features/message/utils/file-operation/parsed-block.utils.ts +18 -15
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-badge.test.tsx +25 -25
- package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +15 -2
- package/src/features/chat/features/session-type/components/__tests__/chat-sidebar-create-menu.test.tsx +24 -0
- package/src/features/chat/features/session-type/components/chat-sidebar-create-menu.tsx +10 -1
- package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +193 -0
- package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +123 -0
- package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +104 -0
- package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +72 -0
- package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +54 -0
- package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +132 -0
- package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +82 -0
- package/src/features/chat/features/welcome/components/chat-welcome.tsx +121 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +53 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-selection.utils.test.ts +63 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-visibility.utils.test.ts +34 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +71 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-selection.utils.ts +79 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-visibility.utils.ts +17 -0
- package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +65 -0
- package/src/features/chat/managers/chat-input.manager.ts +24 -2
- package/src/features/chat/stores/chat-input.store.ts +2 -0
- package/src/features/chat/types/chat-input-bar.types.ts +5 -0
- package/src/features/marketplace/utils/marketplace-installed-cache.utils.ts +27 -23
- package/src/features/settings/components/config/provider-form.tsx +284 -387
- package/src/features/settings/components/config/provider-status-badge.tsx +4 -4
- package/src/features/settings/components/config/secrets-config-form.tsx +11 -135
- package/src/features/settings/hooks/use-provider-auth-flow.ts +140 -0
- package/src/features/settings/pages/__tests__/model-config-page.test.tsx +16 -0
- package/src/features/settings/pages/secrets-config-page.tsx +2 -1
- package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +60 -1
- package/src/features/settings/utils/__tests__/secrets-config-form.utils.test.ts +66 -0
- package/src/features/settings/utils/provider-form-context.utils.ts +131 -0
- package/src/features/settings/utils/provider-form-model.utils.ts +101 -0
- package/src/features/settings/utils/provider-form-support.utils.ts +102 -1
- package/src/features/settings/utils/secrets-config-form.utils.ts +139 -0
- package/src/features/system-status/components/config/runtime-agent-list-card.tsx +17 -11
- package/src/features/system-status/components/config/runtime-binding-list-card.tsx +14 -9
- package/src/features/system-status/components/config/runtime-config-editor.tsx +8 -5
- package/src/features/system-status/components/config/runtime-entry-list-card.tsx +14 -9
- package/src/features/system-status/components/config/runtime-settings-card.tsx +16 -7
- package/src/features/system-status/components/runtime-security-card.tsx +79 -25
- package/src/shared/components/common/__tests__/searchable-model-input.test.tsx +34 -0
- package/src/shared/components/common/provider-scoped-model-input.tsx +1 -0
- package/src/shared/components/common/searchable-model-input.tsx +102 -87
- package/src/shared/components/ui/README.md +7 -0
- package/src/shared/components/ui/popover.tsx +19 -3
- package/src/shared/components/ui/select.tsx +14 -4
- package/src/shared/hooks/use-infinite-scroll-loader.ts +14 -10
- package/src/shared/lib/api/index.ts +2 -0
- package/src/shared/lib/api/preferences/preference.types.ts +22 -0
- package/src/shared/lib/api/preferences/preference.utils.ts +40 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +16 -1
- package/src/shared/lib/i18n/locales/en-US/core.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +16 -1
- package/src/shared/lib/i18n/locales/zh-CN/core.json +1 -0
- package/dist/assets/chat-page-BXngLJY6.js +0 -105
- package/dist/assets/doc-browser-B6Wc_TIY.js +0 -1
- package/dist/assets/index-DTb_XVIH.js +0 -103
- package/dist/assets/index-ayiPYhf1.css +0 -1
- package/dist/assets/mcp-marketplace-page-DZ9H0v05.js +0 -1
- package/dist/assets/mcp-marketplace-page-cnVWf7QQ.js +0 -40
- package/dist/assets/model-config-page-Bcb35AVv.js +0 -1
- package/dist/assets/plus-CYE1onU2.js +0 -1
- package/dist/assets/provider-scoped-model-input-kdoNp4HG.js +0 -1
- package/dist/assets/providers-config-page-CQSibSwO.js +0 -1
- package/dist/assets/remote-BlJDbXOI.js +0 -1
- package/dist/assets/rotate-cw-BWbvFDAX.js +0 -1
- package/dist/assets/runtime-config-page-Tn2yLQ0c.js +0 -1
- package/dist/assets/secrets-config-page-BXn5Uuwp.js +0 -3
- package/dist/assets/use-config-DgKM9VIU.js +0 -1
- package/dist/assets/x-Cm3JjYut.js +0 -1
- package/src/features/chat/components/__tests__/chat-welcome.test.tsx +0 -45
- package/src/features/chat/components/chat-welcome.tsx +0 -102
|
@@ -40,7 +40,29 @@ function SecurityCardFrame({ children, contentClassName = 'space-y-6' }: { child
|
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
type RuntimeSecuritySetupSectionProps = {
|
|
44
|
+
setupUsername: string;
|
|
45
|
+
setSetupUsername: (value: string) => void;
|
|
46
|
+
setupPassword: string;
|
|
47
|
+
setSetupPassword: (value: string) => void;
|
|
48
|
+
setupConfirmPassword: string;
|
|
49
|
+
setSetupConfirmPassword: (value: string) => void;
|
|
50
|
+
canSubmitSetup: boolean;
|
|
51
|
+
isPending: boolean;
|
|
52
|
+
onSubmit: () => void;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function RuntimeSecuritySetupSection({
|
|
56
|
+
canSubmitSetup,
|
|
57
|
+
isPending,
|
|
58
|
+
onSubmit,
|
|
59
|
+
setSetupConfirmPassword,
|
|
60
|
+
setSetupPassword,
|
|
61
|
+
setSetupUsername,
|
|
62
|
+
setupConfirmPassword,
|
|
63
|
+
setupPassword,
|
|
64
|
+
setupUsername
|
|
65
|
+
}: RuntimeSecuritySetupSectionProps) {
|
|
44
66
|
return (
|
|
45
67
|
<SecurityCardFrame contentClassName="space-y-5">
|
|
46
68
|
<div className="rounded-xl border border-dashed border-gray-200 bg-gray-50/70 p-4">
|
|
@@ -52,8 +74,8 @@ function RuntimeSecuritySetupSection(props: { setupUsername: string; setSetupUse
|
|
|
52
74
|
<Label htmlFor="auth-setup-username">{t('authUsername')}</Label>
|
|
53
75
|
<Input
|
|
54
76
|
id="auth-setup-username"
|
|
55
|
-
value={
|
|
56
|
-
onChange={(event) =>
|
|
77
|
+
value={setupUsername}
|
|
78
|
+
onChange={(event) => setSetupUsername(event.target.value)}
|
|
57
79
|
placeholder={t('authUsernamePlaceholder')}
|
|
58
80
|
/>
|
|
59
81
|
</div>
|
|
@@ -62,8 +84,8 @@ function RuntimeSecuritySetupSection(props: { setupUsername: string; setSetupUse
|
|
|
62
84
|
<Input
|
|
63
85
|
id="auth-setup-password"
|
|
64
86
|
type="password"
|
|
65
|
-
value={
|
|
66
|
-
onChange={(event) =>
|
|
87
|
+
value={setupPassword}
|
|
88
|
+
onChange={(event) => setSetupPassword(event.target.value)}
|
|
67
89
|
placeholder={t('authPasswordPlaceholder')}
|
|
68
90
|
/>
|
|
69
91
|
</div>
|
|
@@ -73,43 +95,75 @@ function RuntimeSecuritySetupSection(props: { setupUsername: string; setSetupUse
|
|
|
73
95
|
<Input
|
|
74
96
|
id="auth-setup-confirm"
|
|
75
97
|
type="password"
|
|
76
|
-
value={
|
|
77
|
-
onChange={(event) =>
|
|
98
|
+
value={setupConfirmPassword}
|
|
99
|
+
onChange={(event) => setSetupConfirmPassword(event.target.value)}
|
|
78
100
|
placeholder={t('authConfirmPasswordPlaceholder')}
|
|
79
101
|
/>
|
|
80
102
|
<p className="text-xs text-gray-500">{t('authPasswordMinLengthHint')}</p>
|
|
81
103
|
</div>
|
|
82
|
-
<Button type="button" disabled={!
|
|
83
|
-
{
|
|
104
|
+
<Button type="button" disabled={!canSubmitSetup} onClick={() => void onSubmit()}>
|
|
105
|
+
{isPending ? t('authSettingUp') : t('authSetupAction')}
|
|
84
106
|
</Button>
|
|
85
107
|
</SecurityCardFrame>
|
|
86
108
|
);
|
|
87
109
|
}
|
|
88
110
|
|
|
89
|
-
|
|
111
|
+
type RuntimeSecurityConfiguredSectionProps = {
|
|
112
|
+
authenticated: boolean;
|
|
113
|
+
enabled: boolean;
|
|
114
|
+
username: string | null | undefined;
|
|
115
|
+
nextPassword: string;
|
|
116
|
+
setNextPassword: (value: string) => void;
|
|
117
|
+
nextConfirmPassword: string;
|
|
118
|
+
setNextConfirmPassword: (value: string) => void;
|
|
119
|
+
canUpdatePassword: boolean;
|
|
120
|
+
isUpdatingEnabled: boolean;
|
|
121
|
+
isUpdatingPassword: boolean;
|
|
122
|
+
isLoggingOut: boolean;
|
|
123
|
+
onEnabledChange: (enabled: boolean) => void;
|
|
124
|
+
onPasswordUpdate: () => void;
|
|
125
|
+
onLogout: () => void;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
function RuntimeSecurityConfiguredSection({
|
|
129
|
+
authenticated,
|
|
130
|
+
canUpdatePassword,
|
|
131
|
+
enabled,
|
|
132
|
+
isLoggingOut,
|
|
133
|
+
isUpdatingEnabled,
|
|
134
|
+
isUpdatingPassword,
|
|
135
|
+
nextConfirmPassword,
|
|
136
|
+
nextPassword,
|
|
137
|
+
onEnabledChange,
|
|
138
|
+
onLogout,
|
|
139
|
+
onPasswordUpdate,
|
|
140
|
+
setNextConfirmPassword,
|
|
141
|
+
setNextPassword,
|
|
142
|
+
username
|
|
143
|
+
}: RuntimeSecurityConfiguredSectionProps) {
|
|
90
144
|
return (
|
|
91
145
|
<SecurityCardFrame>
|
|
92
146
|
<div className="rounded-xl border border-gray-200 p-4">
|
|
93
147
|
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
|
94
148
|
<div className="space-y-1">
|
|
95
149
|
<p className="text-sm font-medium text-gray-900">{t('authStatusLabel')}</p>
|
|
96
|
-
<p className="text-sm text-gray-600">{t('authStatusConfiguredUser').replace('{username}',
|
|
150
|
+
<p className="text-sm text-gray-600">{t('authStatusConfiguredUser').replace('{username}', username ?? '')}</p>
|
|
97
151
|
<p className="text-xs text-gray-500">{t('authUsernameFixedHelp')}</p>
|
|
98
152
|
</div>
|
|
99
153
|
<span className="inline-flex items-center rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700">
|
|
100
|
-
{
|
|
154
|
+
{enabled ? t('enabled') : t('disabled')}
|
|
101
155
|
</span>
|
|
102
156
|
</div>
|
|
103
157
|
<div className="mt-4 flex flex-col gap-4 border-t border-gray-200 pt-4 md:flex-row md:items-center md:justify-between">
|
|
104
158
|
<div className="space-y-1">
|
|
105
159
|
<p className="text-sm font-medium text-gray-900">{t('authEnableLabel')}</p>
|
|
106
|
-
<p className="text-xs text-gray-500">{
|
|
160
|
+
<p className="text-xs text-gray-500">{enabled ? t('authEnableOnHelp') : t('authEnableOffHelp')}</p>
|
|
107
161
|
</div>
|
|
108
162
|
<Switch
|
|
109
|
-
checked={
|
|
110
|
-
disabled={
|
|
163
|
+
checked={enabled}
|
|
164
|
+
disabled={isUpdatingEnabled}
|
|
111
165
|
onCheckedChange={(checked) => {
|
|
112
|
-
void
|
|
166
|
+
void onEnabledChange(checked);
|
|
113
167
|
}}
|
|
114
168
|
/>
|
|
115
169
|
</div>
|
|
@@ -125,8 +179,8 @@ function RuntimeSecurityConfiguredSection(props: { authenticated: boolean; enabl
|
|
|
125
179
|
<Input
|
|
126
180
|
id="auth-password-next"
|
|
127
181
|
type="password"
|
|
128
|
-
value={
|
|
129
|
-
onChange={(event) =>
|
|
182
|
+
value={nextPassword}
|
|
183
|
+
onChange={(event) => setNextPassword(event.target.value)}
|
|
130
184
|
placeholder={t('authPasswordPlaceholder')}
|
|
131
185
|
/>
|
|
132
186
|
</div>
|
|
@@ -135,19 +189,19 @@ function RuntimeSecurityConfiguredSection(props: { authenticated: boolean; enabl
|
|
|
135
189
|
<Input
|
|
136
190
|
id="auth-password-confirm"
|
|
137
191
|
type="password"
|
|
138
|
-
value={
|
|
139
|
-
onChange={(event) =>
|
|
192
|
+
value={nextConfirmPassword}
|
|
193
|
+
onChange={(event) => setNextConfirmPassword(event.target.value)}
|
|
140
194
|
placeholder={t('authConfirmPasswordPlaceholder')}
|
|
141
195
|
/>
|
|
142
196
|
</div>
|
|
143
197
|
</div>
|
|
144
198
|
<div className="flex flex-wrap items-center gap-3">
|
|
145
|
-
<Button type="button" disabled={!
|
|
146
|
-
{
|
|
199
|
+
<Button type="button" disabled={!canUpdatePassword} onClick={() => void onPasswordUpdate()}>
|
|
200
|
+
{isUpdatingPassword ? t('authPasswordUpdating') : t('authPasswordAction')}
|
|
147
201
|
</Button>
|
|
148
|
-
{
|
|
149
|
-
<Button type="button" variant="outline" disabled={
|
|
150
|
-
{
|
|
202
|
+
{enabled && authenticated ? (
|
|
203
|
+
<Button type="button" variant="outline" disabled={isLoggingOut} onClick={() => void onLogout()}>
|
|
204
|
+
{isLoggingOut ? t('authLoggingOut') : t('authLogoutAction')}
|
|
151
205
|
</Button>
|
|
152
206
|
) : null}
|
|
153
207
|
</div>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { SearchableModelInput } from '@/shared/components/common/searchable-model-input';
|
|
4
|
+
import { createPopoverAvailableHeightLimit } from '@/shared/components/ui/popover';
|
|
5
|
+
|
|
6
|
+
describe('SearchableModelInput', () => {
|
|
7
|
+
it('keeps the model option panel bounded with an internal scroll region', async () => {
|
|
8
|
+
render(
|
|
9
|
+
<SearchableModelInput
|
|
10
|
+
value=""
|
|
11
|
+
onChange={vi.fn()}
|
|
12
|
+
options={Array.from({ length: 40 }, (_, index) => `model-${index}`)}
|
|
13
|
+
placeholder="Model"
|
|
14
|
+
emptyText="No models"
|
|
15
|
+
createText="Use {value}"
|
|
16
|
+
toggleLabel="Toggle models"
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
fireEvent.focus(screen.getByPlaceholderText('Model'));
|
|
21
|
+
|
|
22
|
+
const firstOption = await screen.findByRole('button', { name: 'model-0' });
|
|
23
|
+
const panel = firstOption.closest('[data-state="open"]') as HTMLElement | null;
|
|
24
|
+
const scrollRegion = firstOption.closest('.overflow-y-auto');
|
|
25
|
+
|
|
26
|
+
expect(panel?.style.maxHeight).toBe(createPopoverAvailableHeightLimit('15rem'));
|
|
27
|
+
expect(panel?.style.maxHeight).toContain('max(0px');
|
|
28
|
+
expect(panel?.style.maxHeight).toContain('100vh');
|
|
29
|
+
expect(panel?.style.maxHeight).toContain('2rem');
|
|
30
|
+
expect(panel?.className).toContain('flex-col');
|
|
31
|
+
expect(scrollRegion?.className).toContain('flex-1');
|
|
32
|
+
expect(scrollRegion?.className).toContain('overflow-y-auto');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -151,6 +151,7 @@ export function ProviderScopedModelInput({
|
|
|
151
151
|
inputClassName="h-10 rounded-xl"
|
|
152
152
|
emptyText={t('modelPickerNoOptions')}
|
|
153
153
|
createText={t('modelPickerUseCustom')}
|
|
154
|
+
toggleLabel={t('modelPickerToggleOptions')}
|
|
154
155
|
/>
|
|
155
156
|
</div>
|
|
156
157
|
<p className="mt-2 text-xs text-gray-500">{t('modelInputCustomHint')}</p>
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import { Check, ChevronsUpDown } from 'lucide-react';
|
|
3
3
|
import { Input } from '@/shared/components/ui/input';
|
|
4
|
+
import {
|
|
5
|
+
Popover,
|
|
6
|
+
PopoverAnchor,
|
|
7
|
+
PopoverContent,
|
|
8
|
+
createPopoverAvailableHeightLimit,
|
|
9
|
+
} from '@/shared/components/ui/popover';
|
|
4
10
|
import { cn } from '@/shared/lib/utils';
|
|
5
11
|
|
|
6
12
|
type SearchableModelInputProps = {
|
|
@@ -12,12 +18,15 @@ type SearchableModelInputProps = {
|
|
|
12
18
|
placeholder?: string;
|
|
13
19
|
className?: string;
|
|
14
20
|
inputClassName?: string;
|
|
15
|
-
emptyText
|
|
16
|
-
createText
|
|
21
|
+
emptyText: string;
|
|
22
|
+
createText: string;
|
|
23
|
+
toggleLabel: string;
|
|
17
24
|
maxItems?: number;
|
|
18
25
|
onEnter?: () => void;
|
|
19
26
|
};
|
|
20
27
|
|
|
28
|
+
const SEARCHABLE_MODEL_MENU_MAX_HEIGHT = createPopoverAvailableHeightLimit('15rem');
|
|
29
|
+
|
|
21
30
|
function normalizeOptions(options: string[]): string[] {
|
|
22
31
|
const deduped = new Set<string>();
|
|
23
32
|
for (const option of options) {
|
|
@@ -40,6 +49,7 @@ export function SearchableModelInput({
|
|
|
40
49
|
inputClassName,
|
|
41
50
|
emptyText,
|
|
42
51
|
createText,
|
|
52
|
+
toggleLabel,
|
|
43
53
|
maxItems = Number.POSITIVE_INFINITY,
|
|
44
54
|
onEnter
|
|
45
55
|
}: SearchableModelInputProps) {
|
|
@@ -77,97 +87,102 @@ export function SearchableModelInput({
|
|
|
77
87
|
}, [normalizedOptions, query, maxItems]);
|
|
78
88
|
|
|
79
89
|
const hasExactMatch = value.trim().length > 0 && normalizedOptions.some((option) => option === value.trim());
|
|
90
|
+
const closeMenu = () => setOpen(false);
|
|
80
91
|
|
|
81
92
|
return (
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
setTimeout(() => setOpen(false), 120);
|
|
86
|
-
}}
|
|
93
|
+
<Popover
|
|
94
|
+
open={open && !disabled}
|
|
95
|
+
onOpenChange={(nextOpen) => setOpen(!disabled && nextOpen)}
|
|
87
96
|
>
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
event.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
>
|
|
126
|
-
<ChevronsUpDown className="h-4 w-4" />
|
|
127
|
-
</button>
|
|
128
|
-
|
|
129
|
-
{open && !disabled && (
|
|
130
|
-
<div className="absolute z-20 mt-1 w-full overflow-hidden rounded-xl border border-gray-200 bg-white shadow-lg">
|
|
131
|
-
<div className="max-h-60 overflow-y-auto py-1">
|
|
132
|
-
{!hasExactMatch && value.trim().length > 0 && (
|
|
133
|
-
<button
|
|
134
|
-
type="button"
|
|
135
|
-
onMouseDown={(event) => event.preventDefault()}
|
|
136
|
-
onClick={() => {
|
|
137
|
-
onChange(value.trim());
|
|
138
|
-
setOpen(false);
|
|
139
|
-
}}
|
|
140
|
-
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-gray-50"
|
|
141
|
-
>
|
|
142
|
-
<Check className="h-4 w-4 text-transparent" />
|
|
143
|
-
<span className="truncate text-gray-700">
|
|
144
|
-
{createText ? createText.replace('{value}', value.trim()) : value.trim()}
|
|
145
|
-
</span>
|
|
146
|
-
</button>
|
|
97
|
+
<PopoverAnchor asChild>
|
|
98
|
+
<div className={cn('relative', className)}>
|
|
99
|
+
<Input
|
|
100
|
+
id={id}
|
|
101
|
+
value={value}
|
|
102
|
+
disabled={disabled}
|
|
103
|
+
onFocus={() => {
|
|
104
|
+
if (!disabled) {
|
|
105
|
+
setOpen(true);
|
|
106
|
+
}
|
|
107
|
+
}}
|
|
108
|
+
onChange={(event) => {
|
|
109
|
+
onChange(event.target.value);
|
|
110
|
+
if (!open && !disabled) {
|
|
111
|
+
setOpen(true);
|
|
112
|
+
}
|
|
113
|
+
}}
|
|
114
|
+
onKeyDown={(event) => {
|
|
115
|
+
if (event.key === 'Enter') {
|
|
116
|
+
if (onEnter) {
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
onEnter();
|
|
119
|
+
}
|
|
120
|
+
closeMenu();
|
|
121
|
+
}
|
|
122
|
+
}}
|
|
123
|
+
placeholder={placeholder}
|
|
124
|
+
className={cn('pr-10', inputClassName)}
|
|
125
|
+
/>
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
129
|
+
onClick={() => setOpen((prev) => !prev)}
|
|
130
|
+
disabled={disabled}
|
|
131
|
+
className={cn(
|
|
132
|
+
'absolute inset-y-0 right-0 inline-flex w-10 items-center justify-center',
|
|
133
|
+
disabled ? 'cursor-not-allowed text-gray-300' : 'text-gray-400 hover:text-gray-600'
|
|
147
134
|
)}
|
|
135
|
+
aria-label={toggleLabel}
|
|
136
|
+
>
|
|
137
|
+
<ChevronsUpDown className="h-4 w-4" />
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
140
|
+
</PopoverAnchor>
|
|
141
|
+
<PopoverContent
|
|
142
|
+
align="start"
|
|
143
|
+
className="flex w-[var(--radix-popover-trigger-width)] min-w-[12rem] flex-col overflow-hidden rounded-xl border border-gray-200 p-0 shadow-lg"
|
|
144
|
+
onOpenAutoFocus={(event) => event.preventDefault()}
|
|
145
|
+
style={{ maxHeight: SEARCHABLE_MODEL_MENU_MAX_HEIGHT }}
|
|
146
|
+
>
|
|
147
|
+
<div className="custom-scrollbar min-h-0 flex-1 overflow-y-auto py-1">
|
|
148
|
+
{!hasExactMatch && value.trim().length > 0 && (
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
152
|
+
onClick={() => {
|
|
153
|
+
onChange(value.trim());
|
|
154
|
+
closeMenu();
|
|
155
|
+
}}
|
|
156
|
+
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-gray-50"
|
|
157
|
+
>
|
|
158
|
+
<Check className="h-4 w-4 text-transparent" />
|
|
159
|
+
<span className="truncate text-gray-700">
|
|
160
|
+
{createText.replace('{value}', value.trim())}
|
|
161
|
+
</span>
|
|
162
|
+
</button>
|
|
163
|
+
)}
|
|
148
164
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
{orderedOptions.map((option) => (
|
|
166
|
+
<button
|
|
167
|
+
key={option}
|
|
168
|
+
type="button"
|
|
169
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
170
|
+
onClick={() => {
|
|
171
|
+
onChange(option);
|
|
172
|
+
closeMenu();
|
|
173
|
+
}}
|
|
174
|
+
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-gray-50"
|
|
175
|
+
>
|
|
176
|
+
<Check className={cn('h-4 w-4', option === value.trim() ? 'text-primary' : 'text-transparent')} />
|
|
177
|
+
<span className="truncate text-gray-700">{option}</span>
|
|
178
|
+
</button>
|
|
179
|
+
))}
|
|
164
180
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
</div>
|
|
181
|
+
{orderedOptions.length === 0 && value.trim().length === 0 && (
|
|
182
|
+
<div className="px-3 py-2 text-sm text-gray-500">{emptyText}</div>
|
|
183
|
+
)}
|
|
169
184
|
</div>
|
|
170
|
-
|
|
171
|
-
</
|
|
185
|
+
</PopoverContent>
|
|
186
|
+
</Popover>
|
|
172
187
|
);
|
|
173
188
|
}
|
|
@@ -3,6 +3,14 @@ import * as PopoverPrimitive from '@radix-ui/react-popover';
|
|
|
3
3
|
|
|
4
4
|
import { cn } from '@/shared/lib/utils';
|
|
5
5
|
|
|
6
|
+
const FLOATING_CONTENT_AVAILABLE_HEIGHT_GAP = '2rem';
|
|
7
|
+
|
|
8
|
+
function createPopoverAvailableHeightLimit(limit: string): string {
|
|
9
|
+
return `min(${limit}, max(0px, calc(var(--radix-popover-content-available-height, 100vh) - ${FLOATING_CONTENT_AVAILABLE_HEIGHT_GAP})))`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const POPOVER_CONTENT_MAX_HEIGHT = createPopoverAvailableHeightLimit('24rem');
|
|
13
|
+
|
|
6
14
|
const Popover = PopoverPrimitive.Root;
|
|
7
15
|
|
|
8
16
|
const PopoverTrigger = PopoverPrimitive.Trigger;
|
|
@@ -12,20 +20,28 @@ const PopoverAnchor = PopoverPrimitive.Anchor;
|
|
|
12
20
|
const PopoverContent = React.forwardRef<
|
|
13
21
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
|
14
22
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
|
15
|
-
>(({ className, sideOffset = 8, align = 'start', ...props }, ref) => (
|
|
23
|
+
>(({ className, sideOffset = 8, align = 'start', collisionPadding = 12, style, ...props }, ref) => (
|
|
16
24
|
<PopoverPrimitive.Portal>
|
|
17
25
|
<PopoverPrimitive.Content
|
|
18
26
|
ref={ref}
|
|
19
27
|
sideOffset={sideOffset}
|
|
20
28
|
align={align}
|
|
29
|
+
collisionPadding={collisionPadding}
|
|
21
30
|
className={cn(
|
|
22
|
-
'z-[var(--z-popover,50)] w-72 overflow-hidden rounded-2xl border border-gray-200/50 bg-white p-4 shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
31
|
+
'z-[var(--z-popover,50)] w-72 overflow-x-hidden overflow-y-auto rounded-2xl border border-gray-200/50 bg-white p-4 shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
23
32
|
className
|
|
24
33
|
)}
|
|
34
|
+
style={{ maxHeight: POPOVER_CONTENT_MAX_HEIGHT, ...style }}
|
|
25
35
|
{...props}
|
|
26
36
|
/>
|
|
27
37
|
</PopoverPrimitive.Portal>
|
|
28
38
|
));
|
|
29
39
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
|
30
40
|
|
|
31
|
-
export {
|
|
41
|
+
export {
|
|
42
|
+
Popover,
|
|
43
|
+
PopoverTrigger,
|
|
44
|
+
PopoverContent,
|
|
45
|
+
PopoverAnchor,
|
|
46
|
+
createPopoverAvailableHeightLimit,
|
|
47
|
+
};
|
|
@@ -6,6 +6,13 @@ import { cn } from "@/shared/lib/utils"
|
|
|
6
6
|
const Select = SelectPrimitive.Root
|
|
7
7
|
const SelectGroup = SelectPrimitive.Group
|
|
8
8
|
const SelectValue = SelectPrimitive.Value
|
|
9
|
+
const SELECT_CONTENT_AVAILABLE_HEIGHT_GAP = "2rem"
|
|
10
|
+
|
|
11
|
+
function createSelectAvailableHeightLimit(limit: string): string {
|
|
12
|
+
return `min(${limit}, max(0px, calc(var(--radix-select-content-available-height, 100vh) - ${SELECT_CONTENT_AVAILABLE_HEIGHT_GAP})))`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const SELECT_CONTENT_MAX_HEIGHT = createSelectAvailableHeightLimit("24rem")
|
|
9
16
|
|
|
10
17
|
type SelectTriggerProps = React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
|
|
11
18
|
indicator?: React.ReactNode
|
|
@@ -66,24 +73,26 @@ type SelectContentProps = React.ComponentPropsWithoutRef<typeof SelectPrimitive.
|
|
|
66
73
|
const SelectContent = React.forwardRef<
|
|
67
74
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
|
68
75
|
SelectContentProps
|
|
69
|
-
>(({ className, children, position = "popper", viewportClassName, ...props }, ref) => (
|
|
76
|
+
>(({ className, children, collisionPadding = 12, position = "popper", style, viewportClassName, ...props }, ref) => (
|
|
70
77
|
<SelectPrimitive.Portal>
|
|
71
78
|
<SelectPrimitive.Content
|
|
72
79
|
ref={ref}
|
|
73
80
|
className={cn(
|
|
74
|
-
"relative z-[var(--z-popover,50)] max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-white",
|
|
81
|
+
"relative z-[var(--z-popover,50)] flex max-h-96 min-w-[8rem] flex-col overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-white",
|
|
75
82
|
position === "popper" &&
|
|
76
83
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
77
84
|
className
|
|
78
85
|
)}
|
|
86
|
+
collisionPadding={collisionPadding}
|
|
79
87
|
position={position}
|
|
88
|
+
style={{ maxHeight: SELECT_CONTENT_MAX_HEIGHT, ...style }}
|
|
80
89
|
{...props}
|
|
81
90
|
>
|
|
82
91
|
<SelectScrollUpButton />
|
|
83
92
|
<SelectPrimitive.Viewport
|
|
84
93
|
className={cn(
|
|
85
|
-
"p-1",
|
|
86
|
-
position === "popper" && "
|
|
94
|
+
"min-h-0 flex-1 overflow-y-auto overscroll-contain p-1",
|
|
95
|
+
position === "popper" && "w-full min-w-[var(--radix-select-trigger-width)]",
|
|
87
96
|
viewportClassName
|
|
88
97
|
)}
|
|
89
98
|
>
|
|
@@ -144,4 +153,5 @@ export {
|
|
|
144
153
|
SelectSeparator,
|
|
145
154
|
SelectScrollUpButton,
|
|
146
155
|
SelectScrollDownButton,
|
|
156
|
+
createSelectAvailableHeightLimit,
|
|
147
157
|
}
|
|
@@ -9,33 +9,37 @@ type UseInfiniteScrollLoaderParams = {
|
|
|
9
9
|
watchValue?: string | number;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
export function useInfiniteScrollLoader(
|
|
12
|
+
export function useInfiniteScrollLoader({
|
|
13
|
+
disabled,
|
|
14
|
+
onLoadMore,
|
|
15
|
+
thresholdPx = DEFAULT_THRESHOLD_PX,
|
|
16
|
+
watchValue
|
|
17
|
+
}: UseInfiniteScrollLoaderParams) {
|
|
13
18
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
14
19
|
const sentinelRef = useRef<HTMLDivElement | null>(null);
|
|
15
|
-
const onLoadMoreRef = useRef(
|
|
20
|
+
const onLoadMoreRef = useRef(onLoadMore);
|
|
16
21
|
const loadingRef = useRef(false);
|
|
17
22
|
|
|
18
23
|
useEffect(() => {
|
|
19
|
-
onLoadMoreRef.current =
|
|
20
|
-
}, [
|
|
24
|
+
onLoadMoreRef.current = onLoadMore;
|
|
25
|
+
}, [onLoadMore]);
|
|
21
26
|
|
|
22
27
|
useEffect(() => {
|
|
23
|
-
if (
|
|
28
|
+
if (disabled) {
|
|
24
29
|
loadingRef.current = false;
|
|
25
30
|
}
|
|
26
|
-
}, [
|
|
31
|
+
}, [disabled]);
|
|
27
32
|
|
|
28
33
|
useEffect(() => {
|
|
29
34
|
const container = containerRef.current;
|
|
30
35
|
const sentinel = sentinelRef.current;
|
|
31
|
-
const thresholdPx = params.thresholdPx ?? DEFAULT_THRESHOLD_PX;
|
|
32
36
|
|
|
33
|
-
if (
|
|
37
|
+
if (disabled || !container || !sentinel) {
|
|
34
38
|
return;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
const triggerLoadMore = () => {
|
|
38
|
-
if (loadingRef.current ||
|
|
42
|
+
if (loadingRef.current || disabled) {
|
|
39
43
|
return;
|
|
40
44
|
}
|
|
41
45
|
|
|
@@ -79,7 +83,7 @@ export function useInfiniteScrollLoader(params: UseInfiniteScrollLoaderParams) {
|
|
|
79
83
|
return () => {
|
|
80
84
|
container.removeEventListener('scroll', maybeLoadMore);
|
|
81
85
|
};
|
|
82
|
-
}, [
|
|
86
|
+
}, [disabled, thresholdPx, watchValue]);
|
|
83
87
|
|
|
84
88
|
return {
|
|
85
89
|
containerRef,
|
|
@@ -10,6 +10,8 @@ export * from './utils/marketplace.utils';
|
|
|
10
10
|
export * from './utils/mcp-marketplace.utils';
|
|
11
11
|
export * from './utils/ncp-attachments.utils';
|
|
12
12
|
export * from './utils/ncp-session.utils';
|
|
13
|
+
export * from './preferences/preference.utils';
|
|
14
|
+
export * from './preferences/preference.types';
|
|
13
15
|
export * from './ncp-session.types';
|
|
14
16
|
export * from './ncp-session-query-cache.utils';
|
|
15
17
|
export * from './raw-client.utils';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type PreferenceJsonValue =
|
|
2
|
+
| null
|
|
3
|
+
| boolean
|
|
4
|
+
| number
|
|
5
|
+
| string
|
|
6
|
+
| PreferenceJsonValue[]
|
|
7
|
+
| { [key: string]: PreferenceJsonValue };
|
|
8
|
+
|
|
9
|
+
export type PreferenceEntryView = {
|
|
10
|
+
key: string;
|
|
11
|
+
value: PreferenceJsonValue | null;
|
|
12
|
+
updatedAt?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type PreferenceUpdateRequest = {
|
|
16
|
+
value: PreferenceJsonValue;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type PreferenceDeleteResult = {
|
|
20
|
+
key: string;
|
|
21
|
+
deleted: boolean;
|
|
22
|
+
};
|