@instantdb/components 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +2 -0
- package/.turbo/turbo-build.log +18 -0
- package/README.md +78 -0
- package/app/App.css +38 -0
- package/app/App.tsx +61 -0
- package/app/index.css +18 -0
- package/app/main.tsx +10 -0
- package/dist/components/StyleMe.d.ts +15 -0
- package/dist/components/StyleMe.d.ts.map +1 -0
- package/dist/components/error-boundary.d.ts +17 -0
- package/dist/components/error-boundary.d.ts.map +1 -0
- package/dist/components/explorer/edit-namespace-dialog.d.ts +14 -0
- package/dist/components/explorer/edit-namespace-dialog.d.ts.map +1 -0
- package/dist/components/explorer/edit-row-dialog.d.ts +10 -0
- package/dist/components/explorer/edit-row-dialog.d.ts.map +1 -0
- package/dist/components/explorer/expandable-deleted-attr.d.ts +15 -0
- package/dist/components/explorer/expandable-deleted-attr.d.ts.map +1 -0
- package/dist/components/explorer/explorer-layout.d.ts +8 -0
- package/dist/components/explorer/explorer-layout.d.ts.map +1 -0
- package/dist/components/explorer/index.d.ts +44 -0
- package/dist/components/explorer/index.d.ts.map +1 -0
- package/dist/components/explorer/inner-explorer.d.ts +16 -0
- package/dist/components/explorer/inner-explorer.d.ts.map +1 -0
- package/dist/components/explorer/new-namespace-dialog.d.ts +10 -0
- package/dist/components/explorer/new-namespace-dialog.d.ts.map +1 -0
- package/dist/components/explorer/query-inspector.d.ts +11 -0
- package/dist/components/explorer/query-inspector.d.ts.map +1 -0
- package/dist/components/explorer/recently-deleted.d.ts +36 -0
- package/dist/components/explorer/recently-deleted.d.ts.map +1 -0
- package/dist/components/explorer/search-input.d.ts +9 -0
- package/dist/components/explorer/search-input.d.ts.map +1 -0
- package/dist/components/explorer/table-components.d.ts +16 -0
- package/dist/components/explorer/table-components.d.ts.map +1 -0
- package/dist/components/explorer/view-settings.d.ts +10 -0
- package/dist/components/explorer/view-settings.d.ts.map +1 -0
- package/dist/components/rosePineDawnTheme.d.ts +13 -0
- package/dist/components/rosePineDawnTheme.d.ts.map +1 -0
- package/dist/components/select.d.ts +16 -0
- package/dist/components/select.d.ts.map +1 -0
- package/dist/components/toast.d.ts +4 -0
- package/dist/components/toast.d.ts.map +1 -0
- package/dist/components/ui.d.ts +336 -0
- package/dist/components/ui.d.ts.map +1 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/hooks/explorer.d.ts +29 -0
- package/dist/hooks/explorer.d.ts.map +1 -0
- package/dist/hooks/useAttrNotes.d.ts +10 -0
- package/dist/hooks/useAttrNotes.d.ts.map +1 -0
- package/dist/hooks/useClickOutside.d.ts +3 -0
- package/dist/hooks/useClickOutside.d.ts.map +1 -0
- package/dist/hooks/useColumnVisibility.d.ts +12 -0
- package/dist/hooks/useColumnVisibility.d.ts.map +1 -0
- package/dist/hooks/useEditBlobConstraints.d.ts +32 -0
- package/dist/hooks/useEditBlobConstraints.d.ts.map +1 -0
- package/dist/hooks/useExplorerHistory.d.ts +1 -0
- package/dist/hooks/useExplorerHistory.d.ts.map +1 -0
- package/dist/hooks/useIsOverflow.d.ts +6 -0
- package/dist/hooks/useIsOverflow.d.ts.map +1 -0
- package/dist/hooks/useLocalStorage.d.ts +2 -0
- package/dist/hooks/useLocalStorage.d.ts.map +1 -0
- package/dist/hooks/useMonacoJSONSchema.d.ts +3 -0
- package/dist/hooks/useMonacoJSONSchema.d.ts.map +1 -0
- package/dist/hooks/useStableDB.d.ts +7 -0
- package/dist/hooks/useStableDB.d.ts.map +1 -0
- package/dist/index.cjs +15 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9270 -0
- package/dist/schema.d.ts +5 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/style.css +1 -0
- package/dist/types.d.ts +241 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/format.d.ts +2 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/indexingJobs.d.ts +24 -0
- package/dist/utils/indexingJobs.d.ts.map +1 -0
- package/dist/utils/parsePermsJSON.d.ts +11 -0
- package/dist/utils/parsePermsJSON.d.ts.map +1 -0
- package/dist/utils/renames.d.ts +3 -0
- package/dist/utils/renames.d.ts.map +1 -0
- package/dist/utils/tableWidthSize.d.ts +9 -0
- package/dist/utils/tableWidthSize.d.ts.map +1 -0
- package/index.html +13 -0
- package/package.json +109 -0
- package/src/components/StyleMe.tsx +97 -0
- package/src/components/error-boundary.tsx +76 -0
- package/src/components/explorer/edit-namespace-dialog.tsx +1886 -0
- package/src/components/explorer/edit-row-dialog.tsx +1151 -0
- package/src/components/explorer/expandable-deleted-attr.tsx +170 -0
- package/src/components/explorer/explorer-layout.tsx +156 -0
- package/src/components/explorer/index.tsx +217 -0
- package/src/components/explorer/inner-explorer.tsx +1341 -0
- package/src/components/explorer/new-namespace-dialog.tsx +54 -0
- package/src/components/explorer/query-inspector.tsx +394 -0
- package/src/components/explorer/recently-deleted.tsx +344 -0
- package/src/components/explorer/search-input.tsx +358 -0
- package/src/components/explorer/table-components.tsx +341 -0
- package/src/components/explorer/view-settings.tsx +75 -0
- package/src/components/rosePineDawnTheme.ts +45 -0
- package/src/components/select.tsx +198 -0
- package/src/components/toast.tsx +18 -0
- package/src/components/ui.tsx +1561 -0
- package/src/config.ts +61 -0
- package/src/hooks/explorer.tsx +125 -0
- package/src/hooks/useAttrNotes.ts +27 -0
- package/src/hooks/useClickOutside.ts +23 -0
- package/src/hooks/useColumnVisibility.ts +39 -0
- package/src/hooks/useEditBlobConstraints.ts +185 -0
- package/src/hooks/useExplorerHistory.ts +0 -0
- package/src/hooks/useIsOverflow.ts +24 -0
- package/src/hooks/useLocalStorage.ts +51 -0
- package/src/hooks/useMonacoJSONSchema.ts +41 -0
- package/src/hooks/useStableDB.ts +30 -0
- package/src/index.tsx +8 -0
- package/src/schema.ts +285 -0
- package/src/style.css +5 -0
- package/src/types.ts +359 -0
- package/src/utils/format.ts +13 -0
- package/src/utils/indexingJobs.ts +126 -0
- package/src/utils/parsePermsJSON.ts +35 -0
- package/src/utils/renames.ts +42 -0
- package/src/utils/tableWidthSize.ts +62 -0
- package/tailwind.config.cjs +42 -0
- package/tsconfig.json +22 -0
- package/vite-env.d.ts +1 -0
- package/vite.config.ts +49 -0
|
@@ -0,0 +1,1561 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Toaster, toast } from 'sonner';
|
|
3
|
+
import { Editor, Monaco, OnMount } from '@monaco-editor/react';
|
|
4
|
+
import type { ClassValue } from 'clsx';
|
|
5
|
+
import clsx from 'clsx';
|
|
6
|
+
import copy from 'copy-to-clipboard';
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { twMerge } from 'tailwind-merge';
|
|
9
|
+
import {
|
|
10
|
+
Select as BaseSelect,
|
|
11
|
+
SelectContent,
|
|
12
|
+
SelectItem,
|
|
13
|
+
SelectTrigger,
|
|
14
|
+
SelectValue,
|
|
15
|
+
} from './select';
|
|
16
|
+
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
|
17
|
+
import { XIcon } from 'lucide-react';
|
|
18
|
+
|
|
19
|
+
import Highlight, { defaultProps } from 'prism-react-renderer';
|
|
20
|
+
|
|
21
|
+
import { parsePermsJSON } from '@lib/utils/parsePermsJSON';
|
|
22
|
+
|
|
23
|
+
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react';
|
|
24
|
+
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
|
25
|
+
import * as HeadlessToggleGroup from '@radix-ui/react-toggle-group';
|
|
26
|
+
import {
|
|
27
|
+
ComponentProps,
|
|
28
|
+
createElement,
|
|
29
|
+
CSSProperties,
|
|
30
|
+
MouseEventHandler,
|
|
31
|
+
PropsWithChildren,
|
|
32
|
+
ReactNode,
|
|
33
|
+
useEffect,
|
|
34
|
+
useRef,
|
|
35
|
+
useState,
|
|
36
|
+
} from 'react';
|
|
37
|
+
|
|
38
|
+
import { InformationCircleIcon } from '@heroicons/react/24/outline';
|
|
39
|
+
import {
|
|
40
|
+
CheckCircleIcon,
|
|
41
|
+
ClipboardDocumentIcon,
|
|
42
|
+
EyeIcon,
|
|
43
|
+
EyeSlashIcon,
|
|
44
|
+
XMarkIcon,
|
|
45
|
+
} from '@heroicons/react/24/solid';
|
|
46
|
+
import CopyToClipboard from 'react-copy-to-clipboard';
|
|
47
|
+
import { errorToast, successToast } from './toast';
|
|
48
|
+
|
|
49
|
+
import { useMonacoJSONSchema } from '@lib/hooks/useMonacoJSONSchema';
|
|
50
|
+
|
|
51
|
+
// content
|
|
52
|
+
|
|
53
|
+
export const Stack = twel('div', 'flex flex-col gap-2');
|
|
54
|
+
export const Group = twel('div', 'flex flex-col gap-2 md:flex-row');
|
|
55
|
+
|
|
56
|
+
export const Content = twel('div', 'prose dark:text-neutral-400');
|
|
57
|
+
export const ScreenHeading = twel('div', 'text-2xl font-bold');
|
|
58
|
+
export const SectionHeading = twel('div', 'text-xl font-bold');
|
|
59
|
+
export const SubsectionHeading = twel('div', 'text-lg');
|
|
60
|
+
export const BlockHeading = twel('div', 'text-md font-bold');
|
|
61
|
+
|
|
62
|
+
export const Hint = twel('div', 'text-sm text-gray-400');
|
|
63
|
+
export const Label = twel(
|
|
64
|
+
'div',
|
|
65
|
+
'text-sm font-bold dark:text-neutral-400 text-gray-700',
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
export const LogoIcon = ({ size = 'mini' }: { size?: 'mini' | 'normal' }) => {
|
|
69
|
+
const sizeToClass = {
|
|
70
|
+
mini: 'h-4 w-4',
|
|
71
|
+
normal: 'h-6 w-6',
|
|
72
|
+
};
|
|
73
|
+
return <img src="/img/icon/logo-512.svg" className={sizeToClass[size]} />;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// controls
|
|
77
|
+
|
|
78
|
+
export type TabItem = {
|
|
79
|
+
id: string;
|
|
80
|
+
label: ReactNode;
|
|
81
|
+
icon?: ReactNode;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type TabButton = Omit<TabItem, 'link'>;
|
|
85
|
+
|
|
86
|
+
export function ToggleCollection({
|
|
87
|
+
className,
|
|
88
|
+
buttonClassName,
|
|
89
|
+
items,
|
|
90
|
+
onChange,
|
|
91
|
+
selectedId,
|
|
92
|
+
disabled,
|
|
93
|
+
}: {
|
|
94
|
+
className?: string;
|
|
95
|
+
buttonClassName?: string;
|
|
96
|
+
items: TabItem[];
|
|
97
|
+
selectedId?: string;
|
|
98
|
+
disabled?: boolean;
|
|
99
|
+
onChange: (tab: TabButton) => void;
|
|
100
|
+
}) {
|
|
101
|
+
return (
|
|
102
|
+
<div className={cn('flex w-full flex-col gap-0.5', className)}>
|
|
103
|
+
{items.map((a) => (
|
|
104
|
+
<button
|
|
105
|
+
key={a.id}
|
|
106
|
+
disabled={disabled}
|
|
107
|
+
onClick={() => {
|
|
108
|
+
onChange(a);
|
|
109
|
+
}}
|
|
110
|
+
className={cn(
|
|
111
|
+
'block cursor-pointer truncate rounded bg-none px-3 py-1 text-left whitespace-nowrap hover:bg-gray-100 disabled:text-gray-400 dark:hover:bg-neutral-700/80',
|
|
112
|
+
{
|
|
113
|
+
'bg-gray-200 dark:bg-neutral-600/50': selectedId === a.id,
|
|
114
|
+
},
|
|
115
|
+
buttonClassName,
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
{a.label}
|
|
119
|
+
</button>
|
|
120
|
+
))}
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function ToggleGroup({
|
|
126
|
+
items,
|
|
127
|
+
onChange,
|
|
128
|
+
selectedId,
|
|
129
|
+
ariaLabel,
|
|
130
|
+
}: {
|
|
131
|
+
items: { id: string; label: string }[];
|
|
132
|
+
selectedId?: string;
|
|
133
|
+
ariaLabel?: string;
|
|
134
|
+
onChange: (tab: { id: string; label: string }) => void;
|
|
135
|
+
}) {
|
|
136
|
+
return (
|
|
137
|
+
<HeadlessToggleGroup.Root
|
|
138
|
+
value={selectedId}
|
|
139
|
+
onValueChange={(id) => {
|
|
140
|
+
if (!id) return;
|
|
141
|
+
|
|
142
|
+
const item = items.find((item) => item.id === id);
|
|
143
|
+
if (!item) return;
|
|
144
|
+
|
|
145
|
+
onChange(item);
|
|
146
|
+
}}
|
|
147
|
+
className="flex gap-1 rounded-sm border border-gray-300 bg-gray-200 p-0.5 text-sm dark:border-neutral-700 dark:bg-neutral-800"
|
|
148
|
+
type="single"
|
|
149
|
+
defaultValue="center"
|
|
150
|
+
aria-label={ariaLabel}
|
|
151
|
+
>
|
|
152
|
+
{items.map((item) => (
|
|
153
|
+
<HeadlessToggleGroup.Item
|
|
154
|
+
key={item.id}
|
|
155
|
+
className={cn(
|
|
156
|
+
'flex-1 rounded-sm p-0.5',
|
|
157
|
+
selectedId === item.id
|
|
158
|
+
? 'bg-white dark:bg-neutral-600/50'
|
|
159
|
+
: 'bg-gray-200 dark:bg-transparent',
|
|
160
|
+
)}
|
|
161
|
+
value={item.id}
|
|
162
|
+
aria-label={item.label}
|
|
163
|
+
>
|
|
164
|
+
{item.label}
|
|
165
|
+
</HeadlessToggleGroup.Item>
|
|
166
|
+
))}
|
|
167
|
+
</HeadlessToggleGroup.Root>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function TextInput({
|
|
172
|
+
value,
|
|
173
|
+
type,
|
|
174
|
+
autoFocus,
|
|
175
|
+
className,
|
|
176
|
+
onChange,
|
|
177
|
+
onKeyDown,
|
|
178
|
+
label,
|
|
179
|
+
error,
|
|
180
|
+
placeholder,
|
|
181
|
+
inputMode,
|
|
182
|
+
tabIndex,
|
|
183
|
+
disabled,
|
|
184
|
+
title,
|
|
185
|
+
required,
|
|
186
|
+
onBlur,
|
|
187
|
+
}: {
|
|
188
|
+
value: string;
|
|
189
|
+
type?: 'text' | 'email' | 'sensitive' | 'password';
|
|
190
|
+
className?: string;
|
|
191
|
+
error?: ReactNode;
|
|
192
|
+
onChange: (value: string) => void;
|
|
193
|
+
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
|
194
|
+
label?: ReactNode;
|
|
195
|
+
placeholder?: string;
|
|
196
|
+
autoFocus?: boolean;
|
|
197
|
+
inputMode?: 'numeric' | 'text';
|
|
198
|
+
tabIndex?: number;
|
|
199
|
+
disabled?: boolean | undefined;
|
|
200
|
+
title?: string | undefined;
|
|
201
|
+
required?: boolean | undefined;
|
|
202
|
+
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
|
|
203
|
+
}) {
|
|
204
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
205
|
+
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
if (autoFocus) {
|
|
208
|
+
inputRef.current?.focus();
|
|
209
|
+
}
|
|
210
|
+
}, []);
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<label className="flex flex-col gap-1">
|
|
214
|
+
{label ? <Label>{label}</Label> : null}
|
|
215
|
+
<input
|
|
216
|
+
disabled={disabled}
|
|
217
|
+
title={title}
|
|
218
|
+
type={type === 'sensitive' ? 'password' : (type ?? 'text')}
|
|
219
|
+
// Try to prevent password managers from trying to save
|
|
220
|
+
// sensitive input
|
|
221
|
+
autoComplete={type === 'sensitive' ? 'off' : undefined}
|
|
222
|
+
data-lpignore={type === 'sensitive' ? 'true' : undefined}
|
|
223
|
+
ref={inputRef}
|
|
224
|
+
inputMode={inputMode}
|
|
225
|
+
placeholder={placeholder}
|
|
226
|
+
value={value ?? ''}
|
|
227
|
+
className={cn(
|
|
228
|
+
'flex w-full flex-1 rounded-sm border-gray-200 bg-white px-3 py-1 placeholder:text-gray-400 disabled:text-gray-400 dark:border-neutral-700 dark:bg-neutral-800 dark:placeholder:text-neutral-500 dark:disabled:text-neutral-700',
|
|
229
|
+
className,
|
|
230
|
+
{
|
|
231
|
+
'border-red-500': error,
|
|
232
|
+
},
|
|
233
|
+
)}
|
|
234
|
+
onChange={(e) => {
|
|
235
|
+
onChange(e.target.value);
|
|
236
|
+
}}
|
|
237
|
+
onKeyDown={onKeyDown}
|
|
238
|
+
onBlur={onBlur}
|
|
239
|
+
tabIndex={tabIndex}
|
|
240
|
+
required={required}
|
|
241
|
+
/>
|
|
242
|
+
{error ? <div className="text-sm text-red-600">{error}</div> : null}
|
|
243
|
+
</label>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function TextArea({
|
|
248
|
+
value,
|
|
249
|
+
autoFocus,
|
|
250
|
+
className,
|
|
251
|
+
onChange,
|
|
252
|
+
onKeyDown,
|
|
253
|
+
label,
|
|
254
|
+
error,
|
|
255
|
+
placeholder,
|
|
256
|
+
inputMode,
|
|
257
|
+
tabIndex,
|
|
258
|
+
disabled,
|
|
259
|
+
title,
|
|
260
|
+
cols,
|
|
261
|
+
rows,
|
|
262
|
+
}: {
|
|
263
|
+
value: string;
|
|
264
|
+
className?: string;
|
|
265
|
+
error?: ReactNode;
|
|
266
|
+
onChange: (value: string) => void;
|
|
267
|
+
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
|
268
|
+
label?: React.ReactNode;
|
|
269
|
+
placeholder?: string;
|
|
270
|
+
autoFocus?: boolean;
|
|
271
|
+
inputMode?: 'numeric' | 'text';
|
|
272
|
+
tabIndex?: number;
|
|
273
|
+
disabled?: boolean | undefined;
|
|
274
|
+
title?: string | undefined;
|
|
275
|
+
cols?: number | undefined;
|
|
276
|
+
rows?: number | undefined;
|
|
277
|
+
}) {
|
|
278
|
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
279
|
+
|
|
280
|
+
useEffect(() => {
|
|
281
|
+
if (autoFocus) {
|
|
282
|
+
inputRef.current?.focus();
|
|
283
|
+
}
|
|
284
|
+
}, []);
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<label className="flex flex-col gap-2">
|
|
288
|
+
{label ? <Label>{label}</Label> : null}
|
|
289
|
+
<textarea
|
|
290
|
+
disabled={disabled}
|
|
291
|
+
title={title}
|
|
292
|
+
ref={inputRef}
|
|
293
|
+
inputMode={inputMode}
|
|
294
|
+
placeholder={placeholder}
|
|
295
|
+
value={value ?? ''}
|
|
296
|
+
className={cn(
|
|
297
|
+
'flex w-full flex-1 rounded-sm border-gray-200 bg-white px-3 py-1 placeholder:text-gray-400 disabled:text-gray-400 dark:border-neutral-700 dark:bg-neutral-800',
|
|
298
|
+
className,
|
|
299
|
+
{
|
|
300
|
+
'border-red-500': error,
|
|
301
|
+
},
|
|
302
|
+
)}
|
|
303
|
+
onChange={(e) => {
|
|
304
|
+
onChange(e.target.value);
|
|
305
|
+
}}
|
|
306
|
+
onKeyDown={onKeyDown}
|
|
307
|
+
tabIndex={tabIndex}
|
|
308
|
+
cols={cols}
|
|
309
|
+
rows={rows}
|
|
310
|
+
/>
|
|
311
|
+
{error ? <div className="text-sm text-red-600">{error}</div> : null}
|
|
312
|
+
</label>
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function Checkbox({
|
|
317
|
+
label,
|
|
318
|
+
error,
|
|
319
|
+
checked,
|
|
320
|
+
onChange,
|
|
321
|
+
className,
|
|
322
|
+
labelClassName,
|
|
323
|
+
required,
|
|
324
|
+
disabled,
|
|
325
|
+
title,
|
|
326
|
+
style,
|
|
327
|
+
}: {
|
|
328
|
+
label?: ReactNode;
|
|
329
|
+
error?: ReactNode;
|
|
330
|
+
checked: boolean;
|
|
331
|
+
className?: string;
|
|
332
|
+
labelClassName?: string;
|
|
333
|
+
onChange: (
|
|
334
|
+
checked: boolean,
|
|
335
|
+
event: React.ChangeEvent<HTMLInputElement>,
|
|
336
|
+
) => void;
|
|
337
|
+
required?: boolean;
|
|
338
|
+
disabled?: boolean | undefined;
|
|
339
|
+
title?: string | undefined;
|
|
340
|
+
style?: CSSProperties;
|
|
341
|
+
}) {
|
|
342
|
+
return (
|
|
343
|
+
<label
|
|
344
|
+
className={cn(
|
|
345
|
+
'items-top flex cursor-pointer gap-2 dark:disabled:opacity-40',
|
|
346
|
+
disabled ? 'cursor-default text-gray-400 opacity-60' : '',
|
|
347
|
+
labelClassName,
|
|
348
|
+
)}
|
|
349
|
+
title={title}
|
|
350
|
+
>
|
|
351
|
+
<input
|
|
352
|
+
style={style}
|
|
353
|
+
disabled={disabled}
|
|
354
|
+
title={title}
|
|
355
|
+
required={required}
|
|
356
|
+
className={cn(
|
|
357
|
+
'mt-0.5 align-middle font-medium text-gray-900 disabled:border-gray-300 disabled:bg-gray-200 dark:border-neutral-500 dark:bg-neutral-600/40 dark:ring-neutral-500 dark:disabled:border-neutral-400 dark:disabled:opacity-50',
|
|
358
|
+
className,
|
|
359
|
+
)}
|
|
360
|
+
type="checkbox"
|
|
361
|
+
checked={checked}
|
|
362
|
+
onChange={(e) => onChange(e.target.checked, e)}
|
|
363
|
+
/>{' '}
|
|
364
|
+
{label}
|
|
365
|
+
{error ? <div className="text-sm text-red-600">{error}</div> : null}
|
|
366
|
+
</label>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function Select<Value extends string | boolean>({
|
|
371
|
+
value,
|
|
372
|
+
options,
|
|
373
|
+
className,
|
|
374
|
+
onChange,
|
|
375
|
+
disabled,
|
|
376
|
+
emptyLabel,
|
|
377
|
+
tabIndex,
|
|
378
|
+
title,
|
|
379
|
+
noOptionsLabel,
|
|
380
|
+
contentClassName,
|
|
381
|
+
visibleValue,
|
|
382
|
+
}: {
|
|
383
|
+
value?: Value;
|
|
384
|
+
options: { label: string | ReactNode; value: Value }[];
|
|
385
|
+
className?: string;
|
|
386
|
+
onChange: (option?: { label: string | ReactNode; value: Value }) => void;
|
|
387
|
+
disabled?: boolean;
|
|
388
|
+
emptyLabel?: string | ReactNode;
|
|
389
|
+
noOptionsLabel?: string | ReactNode;
|
|
390
|
+
tabIndex?: number;
|
|
391
|
+
title?: string | undefined;
|
|
392
|
+
contentClassName?: string;
|
|
393
|
+
visibleValue?: ReactNode;
|
|
394
|
+
}) {
|
|
395
|
+
return (
|
|
396
|
+
<BaseSelect
|
|
397
|
+
disabled={disabled}
|
|
398
|
+
onValueChange={(value) => {
|
|
399
|
+
const o = options.find((o) => o.value === value);
|
|
400
|
+
onChange(o);
|
|
401
|
+
}}
|
|
402
|
+
value={value?.toString()}
|
|
403
|
+
>
|
|
404
|
+
<SelectTrigger className={className} title={title} tabIndex={tabIndex}>
|
|
405
|
+
<SelectValue placeholder={emptyLabel}>{visibleValue}</SelectValue>
|
|
406
|
+
</SelectTrigger>
|
|
407
|
+
<SelectContent className={contentClassName}>
|
|
408
|
+
{options.map((option) => (
|
|
409
|
+
<SelectItem
|
|
410
|
+
key={option.value?.toString()}
|
|
411
|
+
value={option.value?.toString()}
|
|
412
|
+
>
|
|
413
|
+
{option.label}
|
|
414
|
+
</SelectItem>
|
|
415
|
+
))}
|
|
416
|
+
{options.length === 0 && noOptionsLabel}
|
|
417
|
+
</SelectContent>
|
|
418
|
+
</BaseSelect>
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export function Button({
|
|
423
|
+
variant = 'primary',
|
|
424
|
+
size = 'normal',
|
|
425
|
+
type = 'button',
|
|
426
|
+
onClick,
|
|
427
|
+
href,
|
|
428
|
+
className,
|
|
429
|
+
children,
|
|
430
|
+
disabled,
|
|
431
|
+
loading,
|
|
432
|
+
autoFocus,
|
|
433
|
+
tabIndex,
|
|
434
|
+
title,
|
|
435
|
+
}: PropsWithChildren<{
|
|
436
|
+
variant?: 'primary' | 'secondary' | 'subtle' | 'destructive' | 'cta';
|
|
437
|
+
size?: 'mini' | 'normal' | 'large' | 'xl' | 'nano';
|
|
438
|
+
type?: 'link' | 'link-new' | 'button' | 'submit';
|
|
439
|
+
onClick?: MouseEventHandler;
|
|
440
|
+
href?: string;
|
|
441
|
+
className?: string;
|
|
442
|
+
disabled?: boolean;
|
|
443
|
+
loading?: boolean;
|
|
444
|
+
autoFocus?: boolean;
|
|
445
|
+
tabIndex?: number;
|
|
446
|
+
title?: string | undefined;
|
|
447
|
+
}>) {
|
|
448
|
+
const buttonRef = useRef<any>(null);
|
|
449
|
+
const isATag = type === 'link' || (type === 'link-new' && href);
|
|
450
|
+
|
|
451
|
+
useEffect(() => {
|
|
452
|
+
if (autoFocus) {
|
|
453
|
+
buttonRef.current?.focus();
|
|
454
|
+
}
|
|
455
|
+
}, []);
|
|
456
|
+
|
|
457
|
+
const cls = cn(
|
|
458
|
+
`inline-flex justify-center items-center gap-1 whitespace-nowrap px-8 py-1 font-bold rounded-sm cursor-pointer transition-all disabled:cursor-default`,
|
|
459
|
+
{
|
|
460
|
+
// primary
|
|
461
|
+
'bg-[#606AF4] text-white dark:bg-[#606AF4] dark:text-white':
|
|
462
|
+
variant === 'primary',
|
|
463
|
+
'hover:text-slate-100 hover:bg-[#4543e9] dark:hover:text-neutral-100 dark:hover:bg-[#4543e9]':
|
|
464
|
+
variant === 'primary' && isATag,
|
|
465
|
+
'hover:enabled:text-slate-100 hover:enabled:bg-[#4543e9] disabled:bg-[#9197f3] dark:hover:enabled:text-neutral-100 dark:hover:enabled:bg-[#4543e9] dark:disabled:bg-[#9197f3]':
|
|
466
|
+
variant === 'primary' && !isATag,
|
|
467
|
+
// cta
|
|
468
|
+
'bg-orange-600 text-white dark:bg-orange-600 dark:text-white':
|
|
469
|
+
variant === 'cta',
|
|
470
|
+
'hover:text-slate-100 hover:bg-orange-500 dark:hover:text-neutral-100 dark:hover:bg-orange-500':
|
|
471
|
+
variant === 'cta' && isATag,
|
|
472
|
+
'hover:enabled:text-slate-100 hover:enabled:bg-orange-500 dark:hover:enabled:text-neutral-100 dark:hover:enabled:bg-orange-500':
|
|
473
|
+
variant === 'cta' && !isATag,
|
|
474
|
+
// secondary
|
|
475
|
+
'border border-gray-200 text-gray-500 bg-gray-50 shadow-sm dark:border-neutral-600 dark:text-neutral-400 dark:bg-neutral-800':
|
|
476
|
+
variant === 'secondary',
|
|
477
|
+
'hover:text-gray-600 hover:bg-gray-50/30 dark:hover:text-neutral-300 dark:hover:bg-neutral-700/30':
|
|
478
|
+
variant === 'secondary' && isATag,
|
|
479
|
+
'hover:enabled:text-gray-600 hover:enabled:bg-gray-50/30 disabled:text-gray-400 dark:hover:enabled:text-neutral-300 dark:hover:enabled:bg-neutral-700/30 dark:disabled:text-neutral-600':
|
|
480
|
+
variant === 'secondary' && !isATag,
|
|
481
|
+
// subtle
|
|
482
|
+
'text-gray-500 bg-white font-normal dark:text-neutral-400 dark:bg-transparent':
|
|
483
|
+
variant === 'subtle',
|
|
484
|
+
'hover:text-gray-600 hover:bg-gray-200/30 dark:hover:text-neutral-300 dark:hover:bg-neutral-700/30':
|
|
485
|
+
variant === 'subtle' && isATag,
|
|
486
|
+
'hover:enabled:text-gray-600 hover:enabled:bg-gray-200/30 dark:hover:enabled:text-neutral-300 dark:hover:enabled:bg-neutral-700/30':
|
|
487
|
+
variant === 'subtle' && !isATag,
|
|
488
|
+
// destructive
|
|
489
|
+
'text-red-500 dark:bg-red-500/10 bg-white border border-red-200 dark:border-red-900/60':
|
|
490
|
+
variant === 'destructive',
|
|
491
|
+
'hover:text-red-600 hover:text-red-600 hover:border-red-300 dark:hover:border-red-800':
|
|
492
|
+
variant === 'destructive' && isATag,
|
|
493
|
+
'hover:enabled:text-red-600 hover:enabled:text-red-600 hover:enabled:border-red-300 disabled:border-red-50 disabled:text-red-300 dark:hover:enabled:text-red-500 dark:hover:enabled:border-red-800 dark:disabled:border-red-950 dark:disabled:text-red-800':
|
|
494
|
+
variant === 'destructive' && !isATag,
|
|
495
|
+
'text-lg': size === 'large',
|
|
496
|
+
'text-xl': size === 'xl',
|
|
497
|
+
'text-sm px-2 py-0.5': size === 'mini',
|
|
498
|
+
'text-xs px-2 py-0': size === 'nano',
|
|
499
|
+
'cursor-not-allowed': disabled,
|
|
500
|
+
'cursor-wait opacity-75': loading, // Apply wait cursor and lower opacity when loading,
|
|
501
|
+
'bg-gray-200 text-gray-400 dark:bg-neutral-700 dark:text-neutral-500':
|
|
502
|
+
variant == 'cta' && disabled,
|
|
503
|
+
},
|
|
504
|
+
className,
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
if (isATag) {
|
|
508
|
+
return (
|
|
509
|
+
<a
|
|
510
|
+
title={title}
|
|
511
|
+
tabIndex={tabIndex}
|
|
512
|
+
ref={buttonRef}
|
|
513
|
+
className={cls}
|
|
514
|
+
{...(type === 'link-new'
|
|
515
|
+
? { target: '_blank', rel: 'noopener noreferrer' }
|
|
516
|
+
: {})}
|
|
517
|
+
{...(loading || disabled
|
|
518
|
+
? { 'aria-disabled': true }
|
|
519
|
+
: { href, onClick })}
|
|
520
|
+
>
|
|
521
|
+
{children}
|
|
522
|
+
</a>
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return (
|
|
527
|
+
<button
|
|
528
|
+
title={title}
|
|
529
|
+
tabIndex={tabIndex}
|
|
530
|
+
ref={buttonRef}
|
|
531
|
+
disabled={loading || disabled}
|
|
532
|
+
type={type === 'submit' ? 'submit' : 'button'}
|
|
533
|
+
className={cls}
|
|
534
|
+
onClick={onClick}
|
|
535
|
+
>
|
|
536
|
+
{children}
|
|
537
|
+
</button>
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
interface IconButtonProps {
|
|
542
|
+
icon: ReactNode;
|
|
543
|
+
label: string;
|
|
544
|
+
onClick: () => void;
|
|
545
|
+
disabled?: boolean;
|
|
546
|
+
labelDirection?: ComponentProps<typeof TooltipContent>['side'];
|
|
547
|
+
variant?: 'primary' | 'secondary' | 'subtle';
|
|
548
|
+
className?: string;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export const IconButton = ({
|
|
552
|
+
icon,
|
|
553
|
+
label,
|
|
554
|
+
onClick,
|
|
555
|
+
disabled,
|
|
556
|
+
labelDirection,
|
|
557
|
+
variant,
|
|
558
|
+
className,
|
|
559
|
+
}: IconButtonProps) => {
|
|
560
|
+
return (
|
|
561
|
+
<Tooltip>
|
|
562
|
+
<TooltipTrigger>
|
|
563
|
+
<button
|
|
564
|
+
title={label}
|
|
565
|
+
disabled={disabled}
|
|
566
|
+
onClick={onClick}
|
|
567
|
+
className={cn(
|
|
568
|
+
'flex h-9 w-9 items-center justify-center rounded-sm p-2',
|
|
569
|
+
variant === 'primary' &&
|
|
570
|
+
'bg-[#616AF4] text-white hover:bg-[#4543E9]',
|
|
571
|
+
variant === 'secondary' &&
|
|
572
|
+
'border border-gray-300 bg-white text-gray-800 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-200 dark:hover:bg-neutral-700/50',
|
|
573
|
+
variant === 'subtle' &&
|
|
574
|
+
'text-gray-800 hover:bg-gray-200/30 dark:bg-neutral-800 dark:text-neutral-200 dark:hover:bg-neutral-700/50',
|
|
575
|
+
disabled && 'cursor-not-allowed opacity-40',
|
|
576
|
+
className,
|
|
577
|
+
)}
|
|
578
|
+
>
|
|
579
|
+
{icon}
|
|
580
|
+
</button>
|
|
581
|
+
</TooltipTrigger>
|
|
582
|
+
<TooltipContent side={labelDirection}>{label}</TooltipContent>
|
|
583
|
+
</Tooltip>
|
|
584
|
+
);
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// interactions
|
|
588
|
+
|
|
589
|
+
export function useDialog() {
|
|
590
|
+
const [open, setOpen] = useState(false);
|
|
591
|
+
|
|
592
|
+
return {
|
|
593
|
+
open,
|
|
594
|
+
onOpen() {
|
|
595
|
+
setOpen(true);
|
|
596
|
+
},
|
|
597
|
+
toggleOpen() {
|
|
598
|
+
setOpen((_open) => !_open);
|
|
599
|
+
},
|
|
600
|
+
onClose() {
|
|
601
|
+
setOpen(false);
|
|
602
|
+
},
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function MainDialog({
|
|
607
|
+
...props
|
|
608
|
+
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
|
609
|
+
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function DialogPortal({
|
|
613
|
+
...props
|
|
614
|
+
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
|
615
|
+
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function DialogOverlay({
|
|
619
|
+
className,
|
|
620
|
+
...props
|
|
621
|
+
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
|
622
|
+
return (
|
|
623
|
+
<DialogPrimitive.Overlay
|
|
624
|
+
data-slot="dialog-overlay"
|
|
625
|
+
className={cn(
|
|
626
|
+
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
|
|
627
|
+
className,
|
|
628
|
+
)}
|
|
629
|
+
{...props}
|
|
630
|
+
/>
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function DialogContent({
|
|
635
|
+
className,
|
|
636
|
+
children,
|
|
637
|
+
showCloseButton = false,
|
|
638
|
+
...props
|
|
639
|
+
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
|
640
|
+
showCloseButton?: boolean;
|
|
641
|
+
}) {
|
|
642
|
+
const shadowRoot = useShadowRoot();
|
|
643
|
+
const darkMode = useShadowDarkMode();
|
|
644
|
+
|
|
645
|
+
return (
|
|
646
|
+
<DialogPortal container={shadowRoot} data-slot="dialog-portal">
|
|
647
|
+
<DialogOverlay className={cn(darkMode ? 'dark' : '', 'overflow-y-auto')}>
|
|
648
|
+
<DialogPrimitive.Content
|
|
649
|
+
data-slot="dialog-content"
|
|
650
|
+
className={cn(
|
|
651
|
+
'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 relative top-[50%] left-[50%] z-50 grid max-h-[calc(100%-2rem)] w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 overflow-y-auto rounded-lg border border-gray-200 bg-white p-6 shadow-lg duration-200 sm:max-w-lg dark:border-neutral-700 dark:bg-neutral-800 dark:text-white',
|
|
652
|
+
darkMode ? 'dark' : '',
|
|
653
|
+
className,
|
|
654
|
+
)}
|
|
655
|
+
{...props}
|
|
656
|
+
>
|
|
657
|
+
{children}
|
|
658
|
+
{showCloseButton && (
|
|
659
|
+
<DialogPrimitive.Close
|
|
660
|
+
data-slot="dialog-close"
|
|
661
|
+
className="absolute top-4 right-4 rounded-xs opacity-70 transition-opacity duration-100 hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
|
662
|
+
>
|
|
663
|
+
<XIcon />
|
|
664
|
+
<span className="sr-only">Close</span>
|
|
665
|
+
</DialogPrimitive.Close>
|
|
666
|
+
)}
|
|
667
|
+
</DialogPrimitive.Content>
|
|
668
|
+
</DialogOverlay>
|
|
669
|
+
</DialogPortal>
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export { DialogContent, DialogOverlay, DialogPortal };
|
|
674
|
+
|
|
675
|
+
export function Dialog({
|
|
676
|
+
open,
|
|
677
|
+
children,
|
|
678
|
+
onClose,
|
|
679
|
+
className,
|
|
680
|
+
stopFocusPropagation = false,
|
|
681
|
+
hideCloseButton = false,
|
|
682
|
+
}: {
|
|
683
|
+
open: boolean;
|
|
684
|
+
children: React.ReactNode;
|
|
685
|
+
onClose: () => void;
|
|
686
|
+
className?: string;
|
|
687
|
+
stopFocusPropagation?: boolean;
|
|
688
|
+
hideCloseButton?: boolean;
|
|
689
|
+
}) {
|
|
690
|
+
return (
|
|
691
|
+
<MainDialog
|
|
692
|
+
onOpenChange={(s) => {
|
|
693
|
+
if (!s) {
|
|
694
|
+
onClose();
|
|
695
|
+
}
|
|
696
|
+
}}
|
|
697
|
+
open={open}
|
|
698
|
+
>
|
|
699
|
+
<DialogContent
|
|
700
|
+
onFocusCapture={(e) => {
|
|
701
|
+
if (stopFocusPropagation) {
|
|
702
|
+
e.stopPropagation();
|
|
703
|
+
}
|
|
704
|
+
}}
|
|
705
|
+
autoFocus={false}
|
|
706
|
+
tabIndex={undefined}
|
|
707
|
+
className={`w-full max-w-xl overflow-y-auto rounded border-solid bg-white p-3 text-sm shadow dark:bg-neutral-800 dark:text-white ${className}`}
|
|
708
|
+
>
|
|
709
|
+
{!hideCloseButton && (
|
|
710
|
+
<XMarkIcon
|
|
711
|
+
className="absolute top-[18px] right-3 h-4 w-4 cursor-pointer"
|
|
712
|
+
onClick={onClose}
|
|
713
|
+
/>
|
|
714
|
+
)}
|
|
715
|
+
{children}
|
|
716
|
+
</DialogContent>
|
|
717
|
+
{/*</div>*/}
|
|
718
|
+
</MainDialog>
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// abstractions
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* @deprecated Use `useForm` with a regular `<form>` and` <Button type="submit">` instead
|
|
726
|
+
*/
|
|
727
|
+
export function ActionForm({
|
|
728
|
+
className,
|
|
729
|
+
children,
|
|
730
|
+
}: {
|
|
731
|
+
className?: string;
|
|
732
|
+
children: React.ReactNode;
|
|
733
|
+
}) {
|
|
734
|
+
return (
|
|
735
|
+
<form onSubmit={(e) => e.preventDefault()} className={className}>
|
|
736
|
+
{children}
|
|
737
|
+
</form>
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function createErrorMesageFromEx(errorMessage: string, error: any): string {
|
|
742
|
+
let base = errorMessage;
|
|
743
|
+
|
|
744
|
+
const topLevelMessage = error?.message as string | undefined;
|
|
745
|
+
const hintMessage = error?.hint?.errors?.[0]?.message as string | undefined;
|
|
746
|
+
|
|
747
|
+
const hasTopLevelMessage = topLevelMessage?.length;
|
|
748
|
+
if (hasTopLevelMessage) {
|
|
749
|
+
base += `\n${topLevelMessage}`;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const hasHint = hintMessage?.length;
|
|
753
|
+
// Sometimes, the `hint` is directly embedded in the top-level message,
|
|
754
|
+
// so we avoid repeating it here.
|
|
755
|
+
const hintIsDistinct =
|
|
756
|
+
hasHint &&
|
|
757
|
+
(!hasTopLevelMessage || topLevelMessage.indexOf(hintMessage) === -1);
|
|
758
|
+
|
|
759
|
+
if (hintIsDistinct) {
|
|
760
|
+
base += `\n${hintMessage}`;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return base;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
export function ActionButton({
|
|
767
|
+
type,
|
|
768
|
+
variant,
|
|
769
|
+
disabled,
|
|
770
|
+
className,
|
|
771
|
+
label,
|
|
772
|
+
submitLabel,
|
|
773
|
+
errorMessage,
|
|
774
|
+
successMessage,
|
|
775
|
+
onClick,
|
|
776
|
+
tabIndex,
|
|
777
|
+
title,
|
|
778
|
+
}: {
|
|
779
|
+
type?: 'button' | 'submit';
|
|
780
|
+
variant?: 'primary' | 'secondary' | 'destructive';
|
|
781
|
+
disabled?: boolean;
|
|
782
|
+
className?: string;
|
|
783
|
+
label: ReactNode;
|
|
784
|
+
submitLabel: string;
|
|
785
|
+
errorMessage: string;
|
|
786
|
+
successMessage?: string;
|
|
787
|
+
onClick: () => any;
|
|
788
|
+
tabIndex?: number;
|
|
789
|
+
title?: string | undefined;
|
|
790
|
+
}) {
|
|
791
|
+
const [submitting, setSubmitting] = useState(false);
|
|
792
|
+
|
|
793
|
+
async function _onClick() {
|
|
794
|
+
if (submitting) return;
|
|
795
|
+
|
|
796
|
+
setSubmitting(true);
|
|
797
|
+
try {
|
|
798
|
+
await onClick();
|
|
799
|
+
if (successMessage) {
|
|
800
|
+
successToast(successMessage);
|
|
801
|
+
}
|
|
802
|
+
} catch (error) {
|
|
803
|
+
errorToast(createErrorMesageFromEx(errorMessage, error));
|
|
804
|
+
} finally {
|
|
805
|
+
setSubmitting(false);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
return (
|
|
810
|
+
<Button
|
|
811
|
+
tabIndex={tabIndex}
|
|
812
|
+
variant={variant ?? 'secondary'}
|
|
813
|
+
type={type}
|
|
814
|
+
disabled={disabled || submitting}
|
|
815
|
+
className={className}
|
|
816
|
+
onClick={_onClick}
|
|
817
|
+
title={title}
|
|
818
|
+
>
|
|
819
|
+
{submitting ? submitLabel : label}
|
|
820
|
+
</Button>
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
// other
|
|
824
|
+
|
|
825
|
+
export function redactedValue(v: string): string {
|
|
826
|
+
if (v.length === 36 && v.indexOf('-') === 8) {
|
|
827
|
+
// Probably a uuid, so preserve the dashes
|
|
828
|
+
return v.replaceAll(/[^-]/g, '*');
|
|
829
|
+
}
|
|
830
|
+
return v.replaceAll(/./g, '*');
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
export function SmallCopyable({
|
|
834
|
+
value,
|
|
835
|
+
label,
|
|
836
|
+
size = 'normal',
|
|
837
|
+
defaultHidden,
|
|
838
|
+
hideValue,
|
|
839
|
+
onChangeHideValue,
|
|
840
|
+
multiline = false,
|
|
841
|
+
}: {
|
|
842
|
+
value: string;
|
|
843
|
+
label?: string;
|
|
844
|
+
size?: 'normal' | 'large';
|
|
845
|
+
defaultHidden?: boolean;
|
|
846
|
+
hideValue?: boolean;
|
|
847
|
+
onChangeHideValue?: () => void;
|
|
848
|
+
multiline?: boolean;
|
|
849
|
+
}) {
|
|
850
|
+
const [hidden, setHidden] = useState(defaultHidden);
|
|
851
|
+
const handleChangeHideValue =
|
|
852
|
+
onChangeHideValue || (defaultHidden ? () => setHidden(!hidden) : null);
|
|
853
|
+
const [tooltipOpen, setTooltipOpen] = useState(false);
|
|
854
|
+
|
|
855
|
+
return (
|
|
856
|
+
<div
|
|
857
|
+
className={cn(
|
|
858
|
+
'flex items-center rounded font-mono text-xs opacity-70',
|
|
859
|
+
{},
|
|
860
|
+
)}
|
|
861
|
+
>
|
|
862
|
+
{label ? (
|
|
863
|
+
<div
|
|
864
|
+
className="py-1.5 opacity-50"
|
|
865
|
+
style={{
|
|
866
|
+
borderTopLeftRadius: 'calc(0.25rem - 1px)',
|
|
867
|
+
borderBottomLeftRadius: 'calc(0.25rem - 1px)',
|
|
868
|
+
}}
|
|
869
|
+
>
|
|
870
|
+
{label}:
|
|
871
|
+
</div>
|
|
872
|
+
) : null}
|
|
873
|
+
<Tooltip open={tooltipOpen}>
|
|
874
|
+
<TooltipTrigger asChild>
|
|
875
|
+
<pre
|
|
876
|
+
className={clsx('flex-1 cursor-pointer px-2 py-1.5 select-text', {
|
|
877
|
+
truncate: !multiline,
|
|
878
|
+
'break-all whitespace-pre-wrap': multiline,
|
|
879
|
+
})}
|
|
880
|
+
title={hideValue || hidden ? 'Copy to clipboard' : value}
|
|
881
|
+
onClick={(e) => {
|
|
882
|
+
// Only copy if no text is selected
|
|
883
|
+
const selection = window.getSelection();
|
|
884
|
+
if (!selection || selection.toString().length === 0) {
|
|
885
|
+
window.navigator.clipboard.writeText(value);
|
|
886
|
+
setTooltipOpen(true);
|
|
887
|
+
setTimeout(() => setTooltipOpen(false), 1000);
|
|
888
|
+
}
|
|
889
|
+
}}
|
|
890
|
+
>
|
|
891
|
+
{hideValue || hidden ? redactedValue(value) : value}
|
|
892
|
+
</pre>
|
|
893
|
+
</TooltipTrigger>
|
|
894
|
+
<TooltipContent side="bottom">Copied!</TooltipContent>
|
|
895
|
+
</Tooltip>
|
|
896
|
+
|
|
897
|
+
<div className="">
|
|
898
|
+
{!!handleChangeHideValue && (
|
|
899
|
+
<button
|
|
900
|
+
onClick={handleChangeHideValue}
|
|
901
|
+
className={cn(
|
|
902
|
+
'flex items-center gap-x-1 rounded-sm px-2 py-1 opacity-50 transition-colors hover:bg-gray-50 dark:hover:bg-neutral-700',
|
|
903
|
+
{ 'text-xs': size === 'normal', 'text-sm': size === 'large' },
|
|
904
|
+
)}
|
|
905
|
+
>
|
|
906
|
+
{hideValue || hidden ? (
|
|
907
|
+
<EyeSlashIcon className="h-4 w-4" aria-hidden="true" />
|
|
908
|
+
) : (
|
|
909
|
+
<EyeIcon className="h-4 w-4" aria-hidden="true" />
|
|
910
|
+
)}
|
|
911
|
+
</button>
|
|
912
|
+
)}
|
|
913
|
+
</div>
|
|
914
|
+
</div>
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
export function Copyable({
|
|
919
|
+
value,
|
|
920
|
+
label,
|
|
921
|
+
size = 'normal',
|
|
922
|
+
defaultHidden,
|
|
923
|
+
hideValue,
|
|
924
|
+
onChangeHideValue,
|
|
925
|
+
multiline,
|
|
926
|
+
onCopy,
|
|
927
|
+
}: {
|
|
928
|
+
value: string;
|
|
929
|
+
label?: string;
|
|
930
|
+
size?: 'normal' | 'large';
|
|
931
|
+
defaultHidden?: boolean;
|
|
932
|
+
hideValue?: boolean;
|
|
933
|
+
onChangeHideValue?: () => void;
|
|
934
|
+
multiline?: boolean;
|
|
935
|
+
onCopy?: () => void;
|
|
936
|
+
}) {
|
|
937
|
+
const [hidden, setHidden] = useState(defaultHidden);
|
|
938
|
+
const [tooltipOpen, setTooltipOpen] = useState(false);
|
|
939
|
+
const [copyLabel, setCopyLabel] = useState('Copy');
|
|
940
|
+
const handleChangeHideValue =
|
|
941
|
+
onChangeHideValue || (defaultHidden ? () => setHidden(!hidden) : null);
|
|
942
|
+
|
|
943
|
+
return (
|
|
944
|
+
<div
|
|
945
|
+
className={cn(
|
|
946
|
+
'flex items-center rounded border bg-white font-mono dark:border-neutral-700 dark:bg-neutral-800',
|
|
947
|
+
{
|
|
948
|
+
'text-sm': size === 'normal',
|
|
949
|
+
'text-base': size === 'large',
|
|
950
|
+
},
|
|
951
|
+
)}
|
|
952
|
+
>
|
|
953
|
+
{label ? (
|
|
954
|
+
<div
|
|
955
|
+
className="border-r bg-gray-50 px-3 py-1.5 dark:border-r-neutral-700 dark:bg-neutral-700"
|
|
956
|
+
style={{
|
|
957
|
+
borderTopLeftRadius: 'calc(0.25rem - 1px)',
|
|
958
|
+
borderBottomLeftRadius: 'calc(0.25rem - 1px)',
|
|
959
|
+
}}
|
|
960
|
+
>
|
|
961
|
+
{label}
|
|
962
|
+
</div>
|
|
963
|
+
) : null}
|
|
964
|
+
<Tooltip open={tooltipOpen}>
|
|
965
|
+
<TooltipTrigger asChild>
|
|
966
|
+
<pre
|
|
967
|
+
className={clsx('flex-1 cursor-pointer px-2 py-1.5 select-text', {
|
|
968
|
+
truncate: !multiline,
|
|
969
|
+
'break-all whitespace-pre-wrap': multiline,
|
|
970
|
+
})}
|
|
971
|
+
title={hideValue || hidden ? 'Copy to clipboard' : value}
|
|
972
|
+
onClick={(e) => {
|
|
973
|
+
// Only copy if no text is selected
|
|
974
|
+
const selection = window.getSelection();
|
|
975
|
+
if (!selection || selection.toString().length === 0) {
|
|
976
|
+
window.navigator.clipboard.writeText(value);
|
|
977
|
+
setTooltipOpen(true);
|
|
978
|
+
setTimeout(() => setTooltipOpen(false), 1000);
|
|
979
|
+
onCopy?.();
|
|
980
|
+
}
|
|
981
|
+
}}
|
|
982
|
+
>
|
|
983
|
+
{hideValue || hidden ? redactedValue(value) : value}
|
|
984
|
+
</pre>
|
|
985
|
+
</TooltipTrigger>
|
|
986
|
+
<TooltipContent side="bottom">Copied!</TooltipContent>
|
|
987
|
+
</Tooltip>
|
|
988
|
+
<div className="flex gap-1 px-1">
|
|
989
|
+
{!!handleChangeHideValue && (
|
|
990
|
+
<button
|
|
991
|
+
onClick={handleChangeHideValue}
|
|
992
|
+
className={cn(
|
|
993
|
+
'flex items-center gap-x-1 rounded-sm bg-white px-2 py-1 ring-1 ring-gray-300 ring-inset hover:bg-gray-50 dark:bg-neutral-600/20 dark:ring-neutral-600',
|
|
994
|
+
{ 'text-xs': size === 'normal', 'text-sm': size === 'large' },
|
|
995
|
+
)}
|
|
996
|
+
>
|
|
997
|
+
{hideValue || hidden ? (
|
|
998
|
+
<EyeSlashIcon className="h-4 w-4" aria-hidden="true" />
|
|
999
|
+
) : (
|
|
1000
|
+
<EyeIcon className="h-4 w-4" aria-hidden="true" />
|
|
1001
|
+
)}
|
|
1002
|
+
</button>
|
|
1003
|
+
)}
|
|
1004
|
+
<CopyToClipboard text={value}>
|
|
1005
|
+
<button
|
|
1006
|
+
onClick={() => {
|
|
1007
|
+
setCopyLabel('Copied!');
|
|
1008
|
+
setTimeout(() => {
|
|
1009
|
+
setCopyLabel('Copy');
|
|
1010
|
+
}, 2500);
|
|
1011
|
+
}}
|
|
1012
|
+
className={cn(
|
|
1013
|
+
'flex items-center gap-x-1 rounded-sm bg-white px-2 py-1 ring-1 ring-gray-300 ring-inset hover:bg-gray-50 dark:bg-neutral-600/20 dark:ring-neutral-600',
|
|
1014
|
+
{ 'text-xs': size === 'normal', 'text-sm': size === 'large' },
|
|
1015
|
+
)}
|
|
1016
|
+
>
|
|
1017
|
+
<ClipboardDocumentIcon
|
|
1018
|
+
className="-ml-0.5 h-4 w-4"
|
|
1019
|
+
aria-hidden="true"
|
|
1020
|
+
/>
|
|
1021
|
+
{copyLabel}
|
|
1022
|
+
</button>
|
|
1023
|
+
</CopyToClipboard>
|
|
1024
|
+
</div>
|
|
1025
|
+
</div>
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
export function Copytext({ value }: { value: string }) {
|
|
1030
|
+
const [showCopied, setShowCopied] = useState(false);
|
|
1031
|
+
|
|
1032
|
+
return (
|
|
1033
|
+
<span className="inline-flex items-center rounded-sm bg-gray-500 px-2 text-sm text-white">
|
|
1034
|
+
<code
|
|
1035
|
+
className="truncate"
|
|
1036
|
+
onClick={(e) => {
|
|
1037
|
+
const el = e.target as HTMLPreElement;
|
|
1038
|
+
const selection = window.getSelection();
|
|
1039
|
+
if (!selection || !el) return;
|
|
1040
|
+
|
|
1041
|
+
// Set the start and end of the selection to the entire text content of the element.
|
|
1042
|
+
selection.selectAllChildren(el);
|
|
1043
|
+
}}
|
|
1044
|
+
>
|
|
1045
|
+
{value}
|
|
1046
|
+
</code>
|
|
1047
|
+
<CopyToClipboard
|
|
1048
|
+
text={value}
|
|
1049
|
+
onCopy={(text, result) => {
|
|
1050
|
+
if (result) {
|
|
1051
|
+
setShowCopied(true);
|
|
1052
|
+
setTimeout(() => {
|
|
1053
|
+
setShowCopied(false);
|
|
1054
|
+
}, 2500);
|
|
1055
|
+
}
|
|
1056
|
+
}}
|
|
1057
|
+
>
|
|
1058
|
+
{showCopied ? (
|
|
1059
|
+
<CheckCircleIcon className="pl-1" height={'1em'} />
|
|
1060
|
+
) : (
|
|
1061
|
+
<ClipboardDocumentIcon
|
|
1062
|
+
className="cursor-pointer pl-1"
|
|
1063
|
+
height={'1em'}
|
|
1064
|
+
/>
|
|
1065
|
+
)}
|
|
1066
|
+
</CopyToClipboard>
|
|
1067
|
+
</span>
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
export const Divider = ({
|
|
1072
|
+
children,
|
|
1073
|
+
className,
|
|
1074
|
+
}: PropsWithChildren<{ className?: string }>) => (
|
|
1075
|
+
<div className={cn('flex items-center justify-center', className)}>
|
|
1076
|
+
<div
|
|
1077
|
+
aria-hidden="true"
|
|
1078
|
+
className="h-px w-full bg-gray-200 dark:bg-neutral-700"
|
|
1079
|
+
data-orientation="horizontal"
|
|
1080
|
+
role="separator"
|
|
1081
|
+
></div>
|
|
1082
|
+
{children}
|
|
1083
|
+
<div
|
|
1084
|
+
aria-hidden="true"
|
|
1085
|
+
className="h-px w-full bg-gray-200 dark:bg-neutral-700"
|
|
1086
|
+
data-orientation="horizontal"
|
|
1087
|
+
role="separator"
|
|
1088
|
+
></div>
|
|
1089
|
+
</div>
|
|
1090
|
+
);
|
|
1091
|
+
|
|
1092
|
+
export const InfoTip = ({ children }: PropsWithChildren) => {
|
|
1093
|
+
return (
|
|
1094
|
+
<Popover
|
|
1095
|
+
as="span"
|
|
1096
|
+
className="relative inline-flex align-middle"
|
|
1097
|
+
data-open="true"
|
|
1098
|
+
>
|
|
1099
|
+
<PopoverButton className="inline">
|
|
1100
|
+
<InformationCircleIcon
|
|
1101
|
+
height="1em"
|
|
1102
|
+
width="1em"
|
|
1103
|
+
className="cursor-pointer"
|
|
1104
|
+
/>
|
|
1105
|
+
</PopoverButton>
|
|
1106
|
+
|
|
1107
|
+
<PopoverPanel
|
|
1108
|
+
anchor="bottom start"
|
|
1109
|
+
className="z-50 rounded-lg bg-white p-2 shadow-lg dark:bg-neutral-800"
|
|
1110
|
+
>
|
|
1111
|
+
{children}
|
|
1112
|
+
</PopoverPanel>
|
|
1113
|
+
</Popover>
|
|
1114
|
+
);
|
|
1115
|
+
};
|
|
1116
|
+
|
|
1117
|
+
export const Badge = ({
|
|
1118
|
+
children,
|
|
1119
|
+
className,
|
|
1120
|
+
}: PropsWithChildren & { className?: string }) => {
|
|
1121
|
+
return (
|
|
1122
|
+
<span
|
|
1123
|
+
className={cn(
|
|
1124
|
+
'inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-700/30 dark:text-blue-100',
|
|
1125
|
+
className,
|
|
1126
|
+
)}
|
|
1127
|
+
>
|
|
1128
|
+
{children}
|
|
1129
|
+
</span>
|
|
1130
|
+
);
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
export function ProgressButton({
|
|
1134
|
+
percentage = 0,
|
|
1135
|
+
loading,
|
|
1136
|
+
className,
|
|
1137
|
+
children,
|
|
1138
|
+
variant,
|
|
1139
|
+
...props
|
|
1140
|
+
}: PropsWithChildren<{
|
|
1141
|
+
percentage?: number;
|
|
1142
|
+
loading?: boolean;
|
|
1143
|
+
className?: string;
|
|
1144
|
+
variant?: 'primary' | 'secondary' | 'subtle' | 'destructive' | 'cta';
|
|
1145
|
+
}> &
|
|
1146
|
+
Parameters<typeof Button>[0]) {
|
|
1147
|
+
const progressFillStyle = {
|
|
1148
|
+
transform: loading
|
|
1149
|
+
? `scaleX(${Math.max(0, Math.min(100, percentage)) / 100})`
|
|
1150
|
+
: 'scaleX(0)',
|
|
1151
|
+
transition: 'transform 0.3s ease-in-out',
|
|
1152
|
+
transformOrigin: 'left',
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1155
|
+
const progressFillClass = cn('absolute inset-0 transition-all', {
|
|
1156
|
+
'bg-[#4543e9]': variant === 'primary' || !variant,
|
|
1157
|
+
'bg-orange-500': variant === 'cta',
|
|
1158
|
+
'bg-gray-200': variant === 'secondary',
|
|
1159
|
+
'bg-gray-300': variant === 'subtle',
|
|
1160
|
+
'bg-red-200': variant === 'destructive',
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
return (
|
|
1164
|
+
<Button
|
|
1165
|
+
{...props}
|
|
1166
|
+
variant={variant}
|
|
1167
|
+
loading={loading}
|
|
1168
|
+
className={cn('relative overflow-hidden', className)}
|
|
1169
|
+
>
|
|
1170
|
+
{loading && (
|
|
1171
|
+
<div className={progressFillClass} style={progressFillStyle} />
|
|
1172
|
+
)}
|
|
1173
|
+
<span className="relative z-10">{children}</span>
|
|
1174
|
+
</Button>
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
|
1179
|
+
import { Fragment, useId } from 'react';
|
|
1180
|
+
|
|
1181
|
+
function TooltipProvider({
|
|
1182
|
+
delayDuration = 100,
|
|
1183
|
+
...props
|
|
1184
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
|
1185
|
+
return (
|
|
1186
|
+
<TooltipPrimitive.Provider
|
|
1187
|
+
data-slot="tooltip-provider"
|
|
1188
|
+
delayDuration={delayDuration}
|
|
1189
|
+
{...props}
|
|
1190
|
+
/>
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function Tooltip({
|
|
1195
|
+
...props
|
|
1196
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
|
1197
|
+
return (
|
|
1198
|
+
<TooltipProvider>
|
|
1199
|
+
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
|
1200
|
+
</TooltipProvider>
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function TooltipTrigger({
|
|
1205
|
+
...props
|
|
1206
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
|
1207
|
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function TooltipContent({
|
|
1211
|
+
className,
|
|
1212
|
+
sideOffset = 0,
|
|
1213
|
+
children,
|
|
1214
|
+
...props
|
|
1215
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
|
1216
|
+
const shadowRoot = useShadowRoot();
|
|
1217
|
+
const darkMode = useShadowDarkMode();
|
|
1218
|
+
return (
|
|
1219
|
+
<TooltipPrimitive.Portal container={shadowRoot}>
|
|
1220
|
+
<TooltipPrimitive.Content
|
|
1221
|
+
data-slot="tooltip-content"
|
|
1222
|
+
sideOffset={sideOffset}
|
|
1223
|
+
className={cn(
|
|
1224
|
+
'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 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) border border-gray-100 bg-white px-3 py-1.5 text-xs text-balance text-gray-900 dark:border-neutral-600 dark:bg-neutral-800 dark:text-white',
|
|
1225
|
+
darkMode ? 'dark' : '',
|
|
1226
|
+
className,
|
|
1227
|
+
)}
|
|
1228
|
+
{...props}
|
|
1229
|
+
>
|
|
1230
|
+
{children}
|
|
1231
|
+
</TooltipPrimitive.Content>
|
|
1232
|
+
</TooltipPrimitive.Portal>
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
|
|
1237
|
+
|
|
1238
|
+
function DropdownMenu({
|
|
1239
|
+
...props
|
|
1240
|
+
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
|
1241
|
+
return <DropdownMenuPrimitive.Root {...props} />;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function DropdownMenuTrigger({
|
|
1245
|
+
...props
|
|
1246
|
+
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
|
1247
|
+
return <DropdownMenuPrimitive.Trigger {...props} />;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function DropdownMenuContent({
|
|
1251
|
+
className,
|
|
1252
|
+
sideOffset = 4,
|
|
1253
|
+
...props
|
|
1254
|
+
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
|
1255
|
+
const shadowRoot = useShadowRoot();
|
|
1256
|
+
const darkMode = useShadowDarkMode();
|
|
1257
|
+
return (
|
|
1258
|
+
<DropdownMenuPrimitive.Portal container={shadowRoot}>
|
|
1259
|
+
<DropdownMenuPrimitive.Content
|
|
1260
|
+
sideOffset={sideOffset}
|
|
1261
|
+
className={cn(
|
|
1262
|
+
'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 z-50 min-w-48 overflow-visible rounded-md border border-neutral-200 bg-white p-1 text-neutral-950 shadow-lg dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-50',
|
|
1263
|
+
darkMode ? 'dark' : '',
|
|
1264
|
+
className,
|
|
1265
|
+
)}
|
|
1266
|
+
{...props}
|
|
1267
|
+
/>
|
|
1268
|
+
</DropdownMenuPrimitive.Portal>
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function DropdownMenuItem({
|
|
1273
|
+
className,
|
|
1274
|
+
...props
|
|
1275
|
+
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item>) {
|
|
1276
|
+
return (
|
|
1277
|
+
<DropdownMenuPrimitive.Item
|
|
1278
|
+
className={cn(
|
|
1279
|
+
'relative flex cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm transition-colors outline-none select-none focus:bg-neutral-100 focus:text-neutral-900 data-disabled:pointer-events-none data-disabled:opacity-50 dark:focus:bg-neutral-700 dark:focus:text-neutral-50',
|
|
1280
|
+
className,
|
|
1281
|
+
)}
|
|
1282
|
+
{...props}
|
|
1283
|
+
/>
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
function DropdownMenuSeparator({
|
|
1288
|
+
className,
|
|
1289
|
+
...props
|
|
1290
|
+
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
|
1291
|
+
return (
|
|
1292
|
+
<DropdownMenuPrimitive.Separator
|
|
1293
|
+
className={cn(
|
|
1294
|
+
'-mx-1 my-1 h-px bg-neutral-100 dark:bg-neutral-700',
|
|
1295
|
+
className,
|
|
1296
|
+
)}
|
|
1297
|
+
{...props}
|
|
1298
|
+
/>
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
export {
|
|
1303
|
+
DropdownMenu,
|
|
1304
|
+
DropdownMenuContent,
|
|
1305
|
+
DropdownMenuItem,
|
|
1306
|
+
DropdownMenuSeparator,
|
|
1307
|
+
DropdownMenuTrigger,
|
|
1308
|
+
};
|
|
1309
|
+
|
|
1310
|
+
// utils
|
|
1311
|
+
|
|
1312
|
+
export function twel<T = {}>(el: string, cls: ClassValue[] | ClassValue) {
|
|
1313
|
+
return function (props: { className?: string; children: ReactNode } & T) {
|
|
1314
|
+
return createElement(el, {
|
|
1315
|
+
...props,
|
|
1316
|
+
className: cn(cls, props.className),
|
|
1317
|
+
});
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
export function cn(...inputs: ClassValue[]) {
|
|
1322
|
+
return twMerge(clsx(inputs));
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
export function FullscreenLoading() {
|
|
1326
|
+
return (
|
|
1327
|
+
<div className="animate-slow-pulse flex w-full flex-1 flex-col bg-gray-300"></div>
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// code editors
|
|
1332
|
+
export function CodeEditor(props: {
|
|
1333
|
+
value: string;
|
|
1334
|
+
darkMode: boolean;
|
|
1335
|
+
language: string;
|
|
1336
|
+
onChange: (value: string) => void;
|
|
1337
|
+
schema?: object;
|
|
1338
|
+
onMount?: OnMount;
|
|
1339
|
+
path?: string;
|
|
1340
|
+
tabIndex?: number;
|
|
1341
|
+
loading?: boolean;
|
|
1342
|
+
readOnly?: boolean;
|
|
1343
|
+
className?: string;
|
|
1344
|
+
}) {
|
|
1345
|
+
return (
|
|
1346
|
+
<Editor
|
|
1347
|
+
theme={props.darkMode ? 'vs-dark' : 'vs-light'}
|
|
1348
|
+
className={cn(
|
|
1349
|
+
props.loading ? 'animate-pulse' : undefined,
|
|
1350
|
+
props.className,
|
|
1351
|
+
)}
|
|
1352
|
+
height={'100%'}
|
|
1353
|
+
language={props.language}
|
|
1354
|
+
value={props.value ?? ''}
|
|
1355
|
+
defaultPath={props.path}
|
|
1356
|
+
options={{
|
|
1357
|
+
scrollBeyondLastLine: false,
|
|
1358
|
+
overviewRulerLanes: 0,
|
|
1359
|
+
hideCursorInOverviewRuler: true,
|
|
1360
|
+
minimap: { enabled: false },
|
|
1361
|
+
automaticLayout: true,
|
|
1362
|
+
tabIndex: props.tabIndex,
|
|
1363
|
+
readOnly: props.readOnly,
|
|
1364
|
+
}}
|
|
1365
|
+
onChange={(value) => {
|
|
1366
|
+
props.onChange(value || '');
|
|
1367
|
+
}}
|
|
1368
|
+
onMount={props.onMount}
|
|
1369
|
+
beforeMount={(monaco) => {}}
|
|
1370
|
+
loading={<FullscreenLoading />}
|
|
1371
|
+
/>
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
export function JSONEditor(props: {
|
|
1376
|
+
value: string;
|
|
1377
|
+
darkMode: boolean;
|
|
1378
|
+
label: ReactNode;
|
|
1379
|
+
onSave: (value: string) => void;
|
|
1380
|
+
schema?: object;
|
|
1381
|
+
}) {
|
|
1382
|
+
const [draft, setDraft] = useState(props.value);
|
|
1383
|
+
const editorId = useId();
|
|
1384
|
+
const filePath = `json-editor-${editorId}.json`;
|
|
1385
|
+
|
|
1386
|
+
const [monacoInstance, setMonacomonacoInstance] = useState<
|
|
1387
|
+
Monaco | undefined
|
|
1388
|
+
>(undefined);
|
|
1389
|
+
|
|
1390
|
+
useMonacoJSONSchema(filePath, monacoInstance, props.schema);
|
|
1391
|
+
|
|
1392
|
+
useEffect(() => {
|
|
1393
|
+
setDraft(props.value);
|
|
1394
|
+
}, [props.value]);
|
|
1395
|
+
|
|
1396
|
+
return (
|
|
1397
|
+
<div className="flex h-full min-h-0 flex-col bg-gray-50 dark:bg-[#252525]">
|
|
1398
|
+
<div className="flex items-center justify-between gap-4 border-b px-4 py-2 dark:border-b-neutral-700">
|
|
1399
|
+
<div className="font-mono">{props.label}</div>
|
|
1400
|
+
<Button size="mini" onClick={() => props.onSave(draft)}>
|
|
1401
|
+
Save
|
|
1402
|
+
</Button>
|
|
1403
|
+
</div>
|
|
1404
|
+
<div className="min-h-0 grow">
|
|
1405
|
+
<CodeEditor
|
|
1406
|
+
darkMode={props.darkMode}
|
|
1407
|
+
language="json"
|
|
1408
|
+
value={props.value}
|
|
1409
|
+
path={filePath}
|
|
1410
|
+
onChange={(draft) => setDraft(draft)}
|
|
1411
|
+
onMount={function handleEditorDidMount(editor, monaco) {
|
|
1412
|
+
setMonacomonacoInstance(monaco);
|
|
1413
|
+
// cmd+S binding to save
|
|
1414
|
+
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () =>
|
|
1415
|
+
props.onSave(editor.getValue()),
|
|
1416
|
+
);
|
|
1417
|
+
|
|
1418
|
+
// Handle JSON5 paste conversion
|
|
1419
|
+
editor.onDidPaste(async () => {
|
|
1420
|
+
const model = editor.getModel();
|
|
1421
|
+
if (!model) return;
|
|
1422
|
+
|
|
1423
|
+
// Wait 20 ms for paste to complete
|
|
1424
|
+
setTimeout(async () => {
|
|
1425
|
+
const fullContent = model.getValue();
|
|
1426
|
+
if (!fullContent.trim()) return;
|
|
1427
|
+
|
|
1428
|
+
const converted = parsePermsJSON(fullContent);
|
|
1429
|
+
if (converted.status === 'ok') {
|
|
1430
|
+
model.setValue(JSON.stringify(converted.value, null, 2));
|
|
1431
|
+
}
|
|
1432
|
+
}, 20);
|
|
1433
|
+
});
|
|
1434
|
+
}}
|
|
1435
|
+
/>
|
|
1436
|
+
</div>
|
|
1437
|
+
</div>
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
export type FenceLanguage =
|
|
1442
|
+
| 'jsx'
|
|
1443
|
+
| 'tsx'
|
|
1444
|
+
| 'javascript'
|
|
1445
|
+
| 'typescript'
|
|
1446
|
+
| 'bash'
|
|
1447
|
+
| 'json'
|
|
1448
|
+
| 'sql';
|
|
1449
|
+
|
|
1450
|
+
export function Fence({
|
|
1451
|
+
code,
|
|
1452
|
+
language,
|
|
1453
|
+
style: _style,
|
|
1454
|
+
darkMode,
|
|
1455
|
+
className: _className,
|
|
1456
|
+
copyable,
|
|
1457
|
+
}: {
|
|
1458
|
+
code: string;
|
|
1459
|
+
darkMode?: boolean;
|
|
1460
|
+
language: FenceLanguage;
|
|
1461
|
+
className?: string;
|
|
1462
|
+
style?: any;
|
|
1463
|
+
copyable?: boolean;
|
|
1464
|
+
}) {
|
|
1465
|
+
const [copyLabel, setCopyLabel] = useState('Copy');
|
|
1466
|
+
return (
|
|
1467
|
+
<Highlight
|
|
1468
|
+
{...defaultProps}
|
|
1469
|
+
code={code.trimEnd()}
|
|
1470
|
+
language={language}
|
|
1471
|
+
theme={
|
|
1472
|
+
darkMode || false
|
|
1473
|
+
? {
|
|
1474
|
+
plain: {
|
|
1475
|
+
backgroundColor: '#262626',
|
|
1476
|
+
color: 'white',
|
|
1477
|
+
},
|
|
1478
|
+
styles: [],
|
|
1479
|
+
}
|
|
1480
|
+
: rosePineDawnTheme
|
|
1481
|
+
}
|
|
1482
|
+
>
|
|
1483
|
+
{({ className, style, tokens, getTokenProps }) => (
|
|
1484
|
+
<pre
|
|
1485
|
+
className={clsx(className, _className)}
|
|
1486
|
+
style={{
|
|
1487
|
+
...style,
|
|
1488
|
+
..._style,
|
|
1489
|
+
...(copyable ? { position: 'relative' } : {}),
|
|
1490
|
+
}}
|
|
1491
|
+
>
|
|
1492
|
+
{copyable ? (
|
|
1493
|
+
<div className="absolute top-0 right-0 flex items-center px-2">
|
|
1494
|
+
<button
|
|
1495
|
+
onClick={(e) => {
|
|
1496
|
+
copy(code);
|
|
1497
|
+
setCopyLabel('Copied!');
|
|
1498
|
+
setTimeout(() => {
|
|
1499
|
+
setCopyLabel('Copy');
|
|
1500
|
+
}, 2500);
|
|
1501
|
+
e.preventDefault();
|
|
1502
|
+
e.stopPropagation();
|
|
1503
|
+
}}
|
|
1504
|
+
className="flex items-center gap-x-1 rounded-sm bg-white px-2 py-1 text-xs ring-1 ring-gray-300 ring-inset hover:bg-gray-50 dark:bg-neutral-800 dark:ring-neutral-700"
|
|
1505
|
+
>
|
|
1506
|
+
<ClipboardDocumentIcon
|
|
1507
|
+
className="-ml-0.5 h-4 w-4"
|
|
1508
|
+
aria-hidden="true"
|
|
1509
|
+
/>
|
|
1510
|
+
{copyLabel}
|
|
1511
|
+
</button>
|
|
1512
|
+
</div>
|
|
1513
|
+
) : null}
|
|
1514
|
+
<code>
|
|
1515
|
+
{tokens.map((line, lineIndex) => (
|
|
1516
|
+
<Fragment key={lineIndex}>
|
|
1517
|
+
{line
|
|
1518
|
+
.filter((token) => !token.empty)
|
|
1519
|
+
.map((token, tokenIndex) => {
|
|
1520
|
+
const { key, ...props } = getTokenProps({ token });
|
|
1521
|
+
return <span key={key || tokenIndex} {...props} />;
|
|
1522
|
+
})}
|
|
1523
|
+
{'\n'}
|
|
1524
|
+
</Fragment>
|
|
1525
|
+
))}
|
|
1526
|
+
</code>
|
|
1527
|
+
</pre>
|
|
1528
|
+
)}
|
|
1529
|
+
</Highlight>
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
import * as SwitchPrimitive from '@radix-ui/react-switch';
|
|
1534
|
+
import { rosePineDawnTheme } from './rosePineDawnTheme';
|
|
1535
|
+
import { useShadowRoot, useShadowDarkMode } from './StyleMe';
|
|
1536
|
+
function Switch({
|
|
1537
|
+
className,
|
|
1538
|
+
...props
|
|
1539
|
+
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
|
1540
|
+
return (
|
|
1541
|
+
<SwitchPrimitive.Root
|
|
1542
|
+
data-slot="switch"
|
|
1543
|
+
className={cn(
|
|
1544
|
+
'focus-visible:border-ring focus-visible:ring-ring/50 peer inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-2xs outline-hidden transition-all focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-neutral-800 data-[state=unchecked]:bg-neutral-300 dark:border dark:border-neutral-600 dark:data-[state=checked]:border-transparent dark:data-[state=checked]:bg-white dark:data-[state=unchecked]:bg-neutral-700',
|
|
1545
|
+
className,
|
|
1546
|
+
)}
|
|
1547
|
+
{...props}
|
|
1548
|
+
>
|
|
1549
|
+
<SwitchPrimitive.Thumb
|
|
1550
|
+
data-slot="switch-thumb"
|
|
1551
|
+
className={cn(
|
|
1552
|
+
'pointer-events-none block size-3.5 translate-y-0 rounded-full border-transparent bg-white ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=checked]:bg-white data-[state=unchecked]:translate-x-0 dark:bg-neutral-200 dark:data-[state=checked]:bg-neutral-600 dark:data-[state=unchecked]:bg-neutral-200',
|
|
1553
|
+
)}
|
|
1554
|
+
/>
|
|
1555
|
+
</SwitchPrimitive.Root>
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
export { Toaster, toast };
|
|
1560
|
+
|
|
1561
|
+
export { Switch };
|