@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.
Files changed (128) hide show
  1. package/.env +2 -0
  2. package/.turbo/turbo-build.log +18 -0
  3. package/README.md +78 -0
  4. package/app/App.css +38 -0
  5. package/app/App.tsx +61 -0
  6. package/app/index.css +18 -0
  7. package/app/main.tsx +10 -0
  8. package/dist/components/StyleMe.d.ts +15 -0
  9. package/dist/components/StyleMe.d.ts.map +1 -0
  10. package/dist/components/error-boundary.d.ts +17 -0
  11. package/dist/components/error-boundary.d.ts.map +1 -0
  12. package/dist/components/explorer/edit-namespace-dialog.d.ts +14 -0
  13. package/dist/components/explorer/edit-namespace-dialog.d.ts.map +1 -0
  14. package/dist/components/explorer/edit-row-dialog.d.ts +10 -0
  15. package/dist/components/explorer/edit-row-dialog.d.ts.map +1 -0
  16. package/dist/components/explorer/expandable-deleted-attr.d.ts +15 -0
  17. package/dist/components/explorer/expandable-deleted-attr.d.ts.map +1 -0
  18. package/dist/components/explorer/explorer-layout.d.ts +8 -0
  19. package/dist/components/explorer/explorer-layout.d.ts.map +1 -0
  20. package/dist/components/explorer/index.d.ts +44 -0
  21. package/dist/components/explorer/index.d.ts.map +1 -0
  22. package/dist/components/explorer/inner-explorer.d.ts +16 -0
  23. package/dist/components/explorer/inner-explorer.d.ts.map +1 -0
  24. package/dist/components/explorer/new-namespace-dialog.d.ts +10 -0
  25. package/dist/components/explorer/new-namespace-dialog.d.ts.map +1 -0
  26. package/dist/components/explorer/query-inspector.d.ts +11 -0
  27. package/dist/components/explorer/query-inspector.d.ts.map +1 -0
  28. package/dist/components/explorer/recently-deleted.d.ts +36 -0
  29. package/dist/components/explorer/recently-deleted.d.ts.map +1 -0
  30. package/dist/components/explorer/search-input.d.ts +9 -0
  31. package/dist/components/explorer/search-input.d.ts.map +1 -0
  32. package/dist/components/explorer/table-components.d.ts +16 -0
  33. package/dist/components/explorer/table-components.d.ts.map +1 -0
  34. package/dist/components/explorer/view-settings.d.ts +10 -0
  35. package/dist/components/explorer/view-settings.d.ts.map +1 -0
  36. package/dist/components/rosePineDawnTheme.d.ts +13 -0
  37. package/dist/components/rosePineDawnTheme.d.ts.map +1 -0
  38. package/dist/components/select.d.ts +16 -0
  39. package/dist/components/select.d.ts.map +1 -0
  40. package/dist/components/toast.d.ts +4 -0
  41. package/dist/components/toast.d.ts.map +1 -0
  42. package/dist/components/ui.d.ts +336 -0
  43. package/dist/components/ui.d.ts.map +1 -0
  44. package/dist/config.d.ts +14 -0
  45. package/dist/config.d.ts.map +1 -0
  46. package/dist/hooks/explorer.d.ts +29 -0
  47. package/dist/hooks/explorer.d.ts.map +1 -0
  48. package/dist/hooks/useAttrNotes.d.ts +10 -0
  49. package/dist/hooks/useAttrNotes.d.ts.map +1 -0
  50. package/dist/hooks/useClickOutside.d.ts +3 -0
  51. package/dist/hooks/useClickOutside.d.ts.map +1 -0
  52. package/dist/hooks/useColumnVisibility.d.ts +12 -0
  53. package/dist/hooks/useColumnVisibility.d.ts.map +1 -0
  54. package/dist/hooks/useEditBlobConstraints.d.ts +32 -0
  55. package/dist/hooks/useEditBlobConstraints.d.ts.map +1 -0
  56. package/dist/hooks/useExplorerHistory.d.ts +1 -0
  57. package/dist/hooks/useExplorerHistory.d.ts.map +1 -0
  58. package/dist/hooks/useIsOverflow.d.ts +6 -0
  59. package/dist/hooks/useIsOverflow.d.ts.map +1 -0
  60. package/dist/hooks/useLocalStorage.d.ts +2 -0
  61. package/dist/hooks/useLocalStorage.d.ts.map +1 -0
  62. package/dist/hooks/useMonacoJSONSchema.d.ts +3 -0
  63. package/dist/hooks/useMonacoJSONSchema.d.ts.map +1 -0
  64. package/dist/hooks/useStableDB.d.ts +7 -0
  65. package/dist/hooks/useStableDB.d.ts.map +1 -0
  66. package/dist/index.cjs +15 -0
  67. package/dist/index.d.ts +7 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +9270 -0
  70. package/dist/schema.d.ts +5 -0
  71. package/dist/schema.d.ts.map +1 -0
  72. package/dist/style.css +1 -0
  73. package/dist/types.d.ts +241 -0
  74. package/dist/types.d.ts.map +1 -0
  75. package/dist/utils/format.d.ts +2 -0
  76. package/dist/utils/format.d.ts.map +1 -0
  77. package/dist/utils/indexingJobs.d.ts +24 -0
  78. package/dist/utils/indexingJobs.d.ts.map +1 -0
  79. package/dist/utils/parsePermsJSON.d.ts +11 -0
  80. package/dist/utils/parsePermsJSON.d.ts.map +1 -0
  81. package/dist/utils/renames.d.ts +3 -0
  82. package/dist/utils/renames.d.ts.map +1 -0
  83. package/dist/utils/tableWidthSize.d.ts +9 -0
  84. package/dist/utils/tableWidthSize.d.ts.map +1 -0
  85. package/index.html +13 -0
  86. package/package.json +109 -0
  87. package/src/components/StyleMe.tsx +97 -0
  88. package/src/components/error-boundary.tsx +76 -0
  89. package/src/components/explorer/edit-namespace-dialog.tsx +1886 -0
  90. package/src/components/explorer/edit-row-dialog.tsx +1151 -0
  91. package/src/components/explorer/expandable-deleted-attr.tsx +170 -0
  92. package/src/components/explorer/explorer-layout.tsx +156 -0
  93. package/src/components/explorer/index.tsx +217 -0
  94. package/src/components/explorer/inner-explorer.tsx +1341 -0
  95. package/src/components/explorer/new-namespace-dialog.tsx +54 -0
  96. package/src/components/explorer/query-inspector.tsx +394 -0
  97. package/src/components/explorer/recently-deleted.tsx +344 -0
  98. package/src/components/explorer/search-input.tsx +358 -0
  99. package/src/components/explorer/table-components.tsx +341 -0
  100. package/src/components/explorer/view-settings.tsx +75 -0
  101. package/src/components/rosePineDawnTheme.ts +45 -0
  102. package/src/components/select.tsx +198 -0
  103. package/src/components/toast.tsx +18 -0
  104. package/src/components/ui.tsx +1561 -0
  105. package/src/config.ts +61 -0
  106. package/src/hooks/explorer.tsx +125 -0
  107. package/src/hooks/useAttrNotes.ts +27 -0
  108. package/src/hooks/useClickOutside.ts +23 -0
  109. package/src/hooks/useColumnVisibility.ts +39 -0
  110. package/src/hooks/useEditBlobConstraints.ts +185 -0
  111. package/src/hooks/useExplorerHistory.ts +0 -0
  112. package/src/hooks/useIsOverflow.ts +24 -0
  113. package/src/hooks/useLocalStorage.ts +51 -0
  114. package/src/hooks/useMonacoJSONSchema.ts +41 -0
  115. package/src/hooks/useStableDB.ts +30 -0
  116. package/src/index.tsx +8 -0
  117. package/src/schema.ts +285 -0
  118. package/src/style.css +5 -0
  119. package/src/types.ts +359 -0
  120. package/src/utils/format.ts +13 -0
  121. package/src/utils/indexingJobs.ts +126 -0
  122. package/src/utils/parsePermsJSON.ts +35 -0
  123. package/src/utils/renames.ts +42 -0
  124. package/src/utils/tableWidthSize.ts +62 -0
  125. package/tailwind.config.cjs +42 -0
  126. package/tsconfig.json +22 -0
  127. package/vite-env.d.ts +1 -0
  128. 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 };