@checkstack/ui 0.0.2

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 (68) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/bunfig.toml +2 -0
  3. package/package.json +40 -0
  4. package/src/components/Accordion.tsx +55 -0
  5. package/src/components/Alert.tsx +90 -0
  6. package/src/components/AmbientBackground.tsx +105 -0
  7. package/src/components/AnimatedCounter.tsx +54 -0
  8. package/src/components/BackLink.tsx +56 -0
  9. package/src/components/Badge.tsx +38 -0
  10. package/src/components/Button.tsx +55 -0
  11. package/src/components/Card.tsx +56 -0
  12. package/src/components/Checkbox.tsx +46 -0
  13. package/src/components/ColorPicker.tsx +69 -0
  14. package/src/components/CommandPalette.tsx +74 -0
  15. package/src/components/ConfirmationModal.tsx +134 -0
  16. package/src/components/DateRangeFilter.tsx +128 -0
  17. package/src/components/DateTimePicker.tsx +65 -0
  18. package/src/components/Dialog.tsx +134 -0
  19. package/src/components/DropdownMenu.tsx +96 -0
  20. package/src/components/DynamicForm/DynamicForm.tsx +126 -0
  21. package/src/components/DynamicForm/DynamicOptionsField.tsx +220 -0
  22. package/src/components/DynamicForm/FormField.tsx +690 -0
  23. package/src/components/DynamicForm/JsonField.tsx +98 -0
  24. package/src/components/DynamicForm/index.ts +11 -0
  25. package/src/components/DynamicForm/types.ts +95 -0
  26. package/src/components/DynamicForm/utils.ts +39 -0
  27. package/src/components/DynamicIcon.tsx +45 -0
  28. package/src/components/EditableText.tsx +141 -0
  29. package/src/components/EmptyState.tsx +32 -0
  30. package/src/components/HealthBadge.tsx +57 -0
  31. package/src/components/InfoBanner.tsx +97 -0
  32. package/src/components/Input.tsx +20 -0
  33. package/src/components/Label.tsx +17 -0
  34. package/src/components/LoadingSpinner.tsx +29 -0
  35. package/src/components/Markdown.tsx +206 -0
  36. package/src/components/NavItem.tsx +112 -0
  37. package/src/components/Page.tsx +58 -0
  38. package/src/components/PageLayout.tsx +83 -0
  39. package/src/components/PaginatedList.tsx +135 -0
  40. package/src/components/Pagination.tsx +195 -0
  41. package/src/components/PermissionDenied.tsx +31 -0
  42. package/src/components/PermissionGate.tsx +97 -0
  43. package/src/components/PluginConfigForm.tsx +91 -0
  44. package/src/components/SectionHeader.tsx +30 -0
  45. package/src/components/Select.tsx +157 -0
  46. package/src/components/StatusCard.tsx +78 -0
  47. package/src/components/StatusUpdateTimeline.tsx +222 -0
  48. package/src/components/StrategyConfigCard.tsx +333 -0
  49. package/src/components/SubscribeButton.tsx +96 -0
  50. package/src/components/Table.tsx +119 -0
  51. package/src/components/Tabs.tsx +141 -0
  52. package/src/components/TemplateEditor.test.ts +156 -0
  53. package/src/components/TemplateEditor.tsx +435 -0
  54. package/src/components/TerminalFeed.tsx +152 -0
  55. package/src/components/Textarea.tsx +22 -0
  56. package/src/components/ThemeProvider.tsx +76 -0
  57. package/src/components/Toast.tsx +118 -0
  58. package/src/components/ToastProvider.tsx +126 -0
  59. package/src/components/Toggle.tsx +47 -0
  60. package/src/components/Tooltip.tsx +20 -0
  61. package/src/components/UserMenu.tsx +79 -0
  62. package/src/hooks/usePagination.e2e.ts +275 -0
  63. package/src/hooks/usePagination.ts +231 -0
  64. package/src/index.ts +53 -0
  65. package/src/themes.css +204 -0
  66. package/src/utils/strip-markdown.ts +44 -0
  67. package/src/utils.ts +8 -0
  68. package/tsconfig.json +6 -0
@@ -0,0 +1,690 @@
1
+ import React from "react";
2
+ import { Plus, Trash2 } from "lucide-react";
3
+
4
+ import {
5
+ Input,
6
+ Label,
7
+ Select,
8
+ SelectContent,
9
+ SelectItem,
10
+ SelectTrigger,
11
+ SelectValue,
12
+ Button,
13
+ Textarea,
14
+ Toggle,
15
+ ColorPicker,
16
+ TemplateEditor,
17
+ } from "../../index";
18
+
19
+ import type { FormFieldProps, JsonSchemaProperty } from "./types";
20
+ import { getCleanDescription } from "./utils";
21
+ import { DynamicOptionsField } from "./DynamicOptionsField";
22
+ import { JsonField } from "./JsonField";
23
+
24
+ /**
25
+ * Recursive field renderer that handles all supported JSON Schema types.
26
+ */
27
+ export const FormField: React.FC<FormFieldProps> = ({
28
+ id,
29
+ label,
30
+ propSchema,
31
+ value,
32
+ isRequired,
33
+ formValues,
34
+ optionsResolvers,
35
+ templateProperties,
36
+ onChange,
37
+ }) => {
38
+ const description = propSchema.description || "";
39
+
40
+ // Dynamic options via resolver
41
+ const resolverName = propSchema["x-options-resolver"];
42
+ if (resolverName && optionsResolvers) {
43
+ return (
44
+ <DynamicOptionsField
45
+ id={id}
46
+ label={label}
47
+ description={description}
48
+ value={value}
49
+ isRequired={isRequired}
50
+ resolverName={resolverName}
51
+ dependsOn={propSchema["x-depends-on"]}
52
+ searchable={propSchema["x-searchable"] === true}
53
+ formValues={formValues}
54
+ optionsResolvers={optionsResolvers}
55
+ onChange={onChange}
56
+ />
57
+ );
58
+ }
59
+
60
+ // Const field handling - auto-set value and hide (value is fixed)
61
+ if (propSchema.const !== undefined) {
62
+ // Silently ensure the value is set, no UI needed
63
+ React.useEffect(() => {
64
+ if (value !== propSchema.const) {
65
+ onChange(propSchema.const);
66
+ }
67
+ }, [value, propSchema.const, onChange]);
68
+ return <></>;
69
+ }
70
+
71
+ // Enum handling
72
+ if (propSchema.enum) {
73
+ const cleanDesc = getCleanDescription(description);
74
+ return (
75
+ <div className="space-y-2">
76
+ <div>
77
+ <Label htmlFor={id}>
78
+ {label} {isRequired && "*"}
79
+ </Label>
80
+ {cleanDesc && (
81
+ <p className="text-sm text-muted-foreground mt-0.5">{cleanDesc}</p>
82
+ )}
83
+ </div>
84
+ <div className="relative">
85
+ <Select
86
+ value={(value as string) || (propSchema.default as string) || ""}
87
+ onValueChange={(val) => onChange(val)}
88
+ >
89
+ <SelectTrigger id={id}>
90
+ <SelectValue placeholder={`Select ${label}`} />
91
+ </SelectTrigger>
92
+ <SelectContent>
93
+ {propSchema.enum.map((opt: string) => (
94
+ <SelectItem key={opt} value={opt}>
95
+ {opt}
96
+ </SelectItem>
97
+ ))}
98
+ </SelectContent>
99
+ </Select>
100
+ </div>
101
+ </div>
102
+ );
103
+ }
104
+
105
+ // String
106
+ if (propSchema.type === "string") {
107
+ const isTextarea =
108
+ propSchema.format === "textarea" ||
109
+ propSchema.description?.includes("[textarea]");
110
+ const isSecret = (
111
+ propSchema as JsonSchemaProperty & { "x-secret"?: boolean }
112
+ )["x-secret"];
113
+ const cleanDesc = getCleanDescription(description);
114
+
115
+ // Textarea fields - use TemplateEditor if templateProperties available
116
+ if (isTextarea) {
117
+ // If we have template properties, use TemplateEditor
118
+ if (templateProperties && templateProperties.length > 0) {
119
+ return (
120
+ <div className="space-y-2">
121
+ <div>
122
+ <Label htmlFor={id}>
123
+ {label} {isRequired && "*"}
124
+ </Label>
125
+ {cleanDesc && (
126
+ <p className="text-sm text-muted-foreground mt-0.5">
127
+ {cleanDesc}
128
+ </p>
129
+ )}
130
+ </div>
131
+ <TemplateEditor
132
+ value={(value as string) || ""}
133
+ onChange={(val) => onChange(val)}
134
+ availableProperties={templateProperties}
135
+ placeholder={
136
+ propSchema.default
137
+ ? `Default: ${String(propSchema.default)}`
138
+ : "Enter template..."
139
+ }
140
+ rows={5}
141
+ />
142
+ </div>
143
+ );
144
+ }
145
+
146
+ // No template properties, fall back to regular textarea
147
+ return (
148
+ <div className="space-y-2">
149
+ <div>
150
+ <Label htmlFor={id}>
151
+ {label} {isRequired && "*"}
152
+ </Label>
153
+ {cleanDesc && (
154
+ <p className="text-sm text-muted-foreground mt-0.5">
155
+ {cleanDesc}
156
+ </p>
157
+ )}
158
+ </div>
159
+ <Textarea
160
+ id={id}
161
+ value={(value as string) || ""}
162
+ onChange={(e) => onChange(e.target.value)}
163
+ placeholder={
164
+ propSchema.default ? `Default: ${String(propSchema.default)}` : ""
165
+ }
166
+ rows={5}
167
+ />
168
+ </div>
169
+ );
170
+ }
171
+
172
+ // Secret field (password input)
173
+ if (isSecret) {
174
+ return (
175
+ <SecretField
176
+ id={id}
177
+ label={label}
178
+ description={cleanDesc}
179
+ value={value as string}
180
+ isRequired={isRequired}
181
+ onChange={onChange}
182
+ />
183
+ );
184
+ }
185
+
186
+ // Color picker field
187
+ const isColor = (
188
+ propSchema as JsonSchemaProperty & { "x-color"?: boolean }
189
+ )["x-color"];
190
+
191
+ if (isColor) {
192
+ return (
193
+ <div className="space-y-2">
194
+ <div>
195
+ <Label htmlFor={id}>
196
+ {label} {isRequired && "*"}
197
+ </Label>
198
+ {cleanDesc && (
199
+ <p className="text-sm text-muted-foreground mt-0.5">
200
+ {cleanDesc}
201
+ </p>
202
+ )}
203
+ </div>
204
+ <ColorPicker
205
+ id={id}
206
+ value={(value as string) || ""}
207
+ onChange={(val) => onChange(val)}
208
+ placeholder={
209
+ propSchema.default ? String(propSchema.default) : "#000000"
210
+ }
211
+ />
212
+ </div>
213
+ );
214
+ }
215
+
216
+ // Default string input - use TemplateEditor if templateProperties available
217
+ // If we have template properties, use TemplateEditor with smaller rows
218
+ if (templateProperties && templateProperties.length > 0) {
219
+ return (
220
+ <div className="space-y-2">
221
+ <div>
222
+ <Label htmlFor={id}>
223
+ {label} {isRequired && "*"}
224
+ </Label>
225
+ {cleanDesc && (
226
+ <p className="text-sm text-muted-foreground mt-0.5">
227
+ {cleanDesc}
228
+ </p>
229
+ )}
230
+ </div>
231
+ <TemplateEditor
232
+ value={(value as string) || ""}
233
+ onChange={(val) => onChange(val)}
234
+ availableProperties={templateProperties}
235
+ placeholder={
236
+ propSchema.default
237
+ ? `Default: ${String(propSchema.default)}`
238
+ : undefined
239
+ }
240
+ rows={2}
241
+ />
242
+ </div>
243
+ );
244
+ }
245
+
246
+ // No template properties - fallback to regular Input
247
+ return (
248
+ <div className="space-y-2">
249
+ <div>
250
+ <Label htmlFor={id}>
251
+ {label} {isRequired && "*"}
252
+ </Label>
253
+ {cleanDesc && (
254
+ <p className="text-sm text-muted-foreground mt-0.5">{cleanDesc}</p>
255
+ )}
256
+ </div>
257
+ <Input
258
+ id={id}
259
+ value={(value as string) || ""}
260
+ onChange={(e) => onChange(e.target.value)}
261
+ placeholder={
262
+ propSchema.default ? `Default: ${String(propSchema.default)}` : ""
263
+ }
264
+ />
265
+ </div>
266
+ );
267
+ }
268
+
269
+ // Number
270
+ if (propSchema.type === "number" || propSchema.type === "integer") {
271
+ const cleanDesc = getCleanDescription(description);
272
+ return (
273
+ <div className="space-y-2">
274
+ <div>
275
+ <Label htmlFor={id}>
276
+ {label} {isRequired && "*"}
277
+ </Label>
278
+ {cleanDesc && (
279
+ <p className="text-sm text-muted-foreground mt-0.5">{cleanDesc}</p>
280
+ )}
281
+ </div>
282
+ <Input
283
+ id={id}
284
+ type="number"
285
+ value={
286
+ value === undefined
287
+ ? (propSchema.default as number | string) || ""
288
+ : (value as number | string)
289
+ }
290
+ onChange={(e) =>
291
+ onChange(
292
+ propSchema.type === "integer"
293
+ ? Number.parseInt(e.target.value, 10)
294
+ : Number.parseFloat(e.target.value)
295
+ )
296
+ }
297
+ />
298
+ </div>
299
+ );
300
+ }
301
+
302
+ // Boolean
303
+ if (propSchema.type === "boolean") {
304
+ const cleanDesc = getCleanDescription(description);
305
+ return (
306
+ <div className="flex items-center justify-between gap-4">
307
+ <div className="flex-1 space-y-1">
308
+ <Label htmlFor={id} className="cursor-pointer">
309
+ {label} {isRequired && "*"}
310
+ </Label>
311
+ {cleanDesc && (
312
+ <p className="text-sm text-muted-foreground">{cleanDesc}</p>
313
+ )}
314
+ </div>
315
+ <Toggle
316
+ checked={
317
+ value === undefined
318
+ ? (propSchema.default as boolean) || false
319
+ : (value as boolean)
320
+ }
321
+ onCheckedChange={(checked) => onChange(checked)}
322
+ />
323
+ </div>
324
+ );
325
+ }
326
+
327
+ // Dictionary/Record (headers)
328
+ if (propSchema.type === "object" && propSchema.additionalProperties) {
329
+ const cleanDesc = getCleanDescription(description);
330
+ return (
331
+ <div className="space-y-2">
332
+ <div>
333
+ <Label htmlFor={id}>
334
+ {label} (JSON) {isRequired && "*"}
335
+ </Label>
336
+ {cleanDesc && (
337
+ <p className="text-sm text-muted-foreground mt-0.5">{cleanDesc}</p>
338
+ )}
339
+ </div>
340
+ <JsonField
341
+ id={id}
342
+ value={value as Record<string, unknown>}
343
+ propSchema={propSchema}
344
+ onChange={(val) => onChange(val)}
345
+ />
346
+ </div>
347
+ );
348
+ }
349
+
350
+ // Object (Nested Form)
351
+ if (propSchema.type === "object" && propSchema.properties) {
352
+ return (
353
+ <div className="space-y-4 p-4 border rounded-lg bg-muted/30">
354
+ <p className="text-sm font-semibold">{label}</p>
355
+ {Object.entries(propSchema.properties).map(([key, subSchema]) => (
356
+ <FormField
357
+ key={key}
358
+ id={`${id}.${key}`}
359
+ label={key.charAt(0).toUpperCase() + key.slice(1)}
360
+ propSchema={subSchema}
361
+ value={(value as Record<string, unknown>)?.[key]}
362
+ isRequired={propSchema.required?.includes(key)}
363
+ formValues={formValues}
364
+ optionsResolvers={optionsResolvers}
365
+ templateProperties={templateProperties}
366
+ onChange={(val) =>
367
+ onChange({ ...(value as Record<string, unknown>), [key]: val })
368
+ }
369
+ />
370
+ ))}
371
+ </div>
372
+ );
373
+ }
374
+
375
+ // Array support
376
+ if (propSchema.type === "array") {
377
+ const items = (value as unknown[]) || [];
378
+ const itemSchema = propSchema.items;
379
+ const cleanDesc = getCleanDescription(description);
380
+
381
+ if (!itemSchema) return <></>;
382
+
383
+ // Helper to create initial value for new array items
384
+ const createNewItem = (): Record<string, unknown> => {
385
+ // Check if itemSchema is a discriminated union
386
+ const variants = itemSchema.oneOf || itemSchema.anyOf;
387
+ if (variants && variants.length > 0) {
388
+ const firstVariant = variants[0];
389
+ if (firstVariant.properties) {
390
+ const newItem: Record<string, unknown> = {};
391
+ // Find discriminator and set all properties with defaults
392
+ for (const [propKey, propDef] of Object.entries(
393
+ firstVariant.properties
394
+ )) {
395
+ if (propDef.const !== undefined) {
396
+ // This is the discriminator field
397
+ newItem[propKey] = propDef.const;
398
+ } else if (propDef.default !== undefined) {
399
+ newItem[propKey] = propDef.default;
400
+ }
401
+ }
402
+ return newItem;
403
+ }
404
+ }
405
+ // Fallback to empty object for regular object items
406
+ return {};
407
+ };
408
+
409
+ return (
410
+ <div className="space-y-4">
411
+ <div className="flex items-center justify-between">
412
+ <div>
413
+ <Label>
414
+ {label} {isRequired && "*"}
415
+ </Label>
416
+ {cleanDesc && (
417
+ <p className="text-sm text-muted-foreground mt-0.5">
418
+ {cleanDesc}
419
+ </p>
420
+ )}
421
+ </div>
422
+ <Button
423
+ type="button"
424
+ variant="outline"
425
+ size="sm"
426
+ onClick={() =>
427
+ onChange([
428
+ ...(items as Record<string, unknown>[]),
429
+ createNewItem(),
430
+ ])
431
+ }
432
+ className="h-8 gap-1 transition-all hover:bg-accent hover:text-accent-foreground"
433
+ >
434
+ <Plus className="h-4 w-4" />
435
+ Add Item
436
+ </Button>
437
+ </div>
438
+ {items.length === 0 && (
439
+ <p className="text-xs text-muted-foreground italic">
440
+ No items added yet.
441
+ </p>
442
+ )}
443
+ <div className="space-y-4">
444
+ {items.map((item: unknown, index: number) => (
445
+ <div key={index} className="relative group">
446
+ <div className="p-4 border rounded-lg bg-background shadow-sm border-border transition-all hover:border-border/80">
447
+ <Button
448
+ type="button"
449
+ variant="ghost"
450
+ size="icon"
451
+ onClick={() => {
452
+ const next = [...(items as unknown[])];
453
+ next.splice(index, 1);
454
+ onChange(next);
455
+ }}
456
+ className="absolute -top-2 -right-2 h-7 w-7 rounded-full bg-background border shadow-sm text-destructive hover:text-destructive/90 hover:bg-destructive/10"
457
+ >
458
+ <Trash2 className="h-3.5 w-3.5" />
459
+ </Button>
460
+ <FormField
461
+ id={`${id}[${index}]`}
462
+ label={`${label} #${index + 1}`}
463
+ propSchema={itemSchema}
464
+ value={item}
465
+ formValues={formValues}
466
+ optionsResolvers={optionsResolvers}
467
+ templateProperties={templateProperties}
468
+ onChange={(val) => {
469
+ const next = [...(items as unknown[])];
470
+ next[index] = val;
471
+ onChange(next);
472
+ }}
473
+ />
474
+ </div>
475
+ </div>
476
+ ))}
477
+ </div>
478
+ </div>
479
+ );
480
+ }
481
+
482
+ // Discriminated Union (oneOf/anyOf) with object variants
483
+ const unionVariants = propSchema.oneOf || propSchema.anyOf;
484
+ if (unionVariants && unionVariants.length > 0) {
485
+ // Find the discriminator field by looking for a property with "const" in each variant
486
+ const firstVariant = unionVariants[0];
487
+ if (!firstVariant.properties) return <></>;
488
+
489
+ // Find discriminator: the field that has "const" in each variant
490
+ let discriminatorField: string | undefined;
491
+ for (const [fieldName, fieldSchema] of Object.entries(
492
+ firstVariant.properties
493
+ )) {
494
+ if (fieldSchema.const !== undefined) {
495
+ discriminatorField = fieldName;
496
+ break;
497
+ }
498
+ }
499
+
500
+ if (!discriminatorField) return <></>;
501
+
502
+ // Get current discriminator value and find matching variant
503
+ const currentValue = value as Record<string, unknown> | undefined;
504
+ const currentDiscriminatorValue = currentValue?.[discriminatorField];
505
+
506
+ // Extract variant options from all variants
507
+ const variantOptions = unionVariants
508
+ .map((variant) => {
509
+ const discProp = variant.properties?.[discriminatorField];
510
+ const constValue = discProp?.const;
511
+ if (constValue === undefined) return;
512
+ return String(constValue);
513
+ })
514
+ .filter((v): v is string => v !== undefined);
515
+
516
+ // Find the currently selected variant
517
+ const selectedVariant =
518
+ unionVariants.find((variant) => {
519
+ const discProp = variant.properties?.[discriminatorField];
520
+ return discProp?.const === currentDiscriminatorValue;
521
+ }) || unionVariants[0];
522
+
523
+ const displayDiscriminatorField =
524
+ discriminatorField.charAt(0).toUpperCase() + discriminatorField.slice(1);
525
+
526
+ return (
527
+ <div className="space-y-3 p-3 border rounded-lg bg-background">
528
+ {/* Discriminator selector */}
529
+ <div className="space-y-2">
530
+ <div>
531
+ <Label htmlFor={`${id}.${discriminatorField}`}>
532
+ {displayDiscriminatorField}
533
+ </Label>
534
+ </div>
535
+ <Select
536
+ value={String(currentDiscriminatorValue || variantOptions[0] || "")}
537
+ onValueChange={(newValue) => {
538
+ // When discriminator changes, reset to new variant with only discriminator set
539
+ const newVariant = unionVariants.find((v) => {
540
+ const discProp = v.properties?.[discriminatorField];
541
+ return String(discProp?.const) === newValue;
542
+ });
543
+ if (newVariant) {
544
+ // Initialize with defaults for the new variant
545
+ const newObj: Record<string, unknown> = {
546
+ [discriminatorField]: newValue,
547
+ };
548
+ // Set defaults for other properties
549
+ for (const [propKey, propDef] of Object.entries(
550
+ newVariant.properties || {}
551
+ )) {
552
+ if (
553
+ propKey !== discriminatorField &&
554
+ propDef.default !== undefined
555
+ ) {
556
+ newObj[propKey] = propDef.default;
557
+ }
558
+ }
559
+ onChange(newObj);
560
+ }
561
+ }}
562
+ >
563
+ <SelectTrigger id={`${id}.${discriminatorField}`}>
564
+ <SelectValue
565
+ placeholder={`Select ${displayDiscriminatorField}`}
566
+ />
567
+ </SelectTrigger>
568
+ <SelectContent>
569
+ {variantOptions.map((opt) => (
570
+ <SelectItem key={opt} value={opt}>
571
+ {opt}
572
+ </SelectItem>
573
+ ))}
574
+ </SelectContent>
575
+ </Select>
576
+ </div>
577
+
578
+ {/* Render other fields from selected variant */}
579
+ {selectedVariant.properties &&
580
+ Object.entries(selectedVariant.properties)
581
+ .filter(([key]) => key !== discriminatorField)
582
+ .map(([key, subSchema]) => (
583
+ <FormField
584
+ key={`${id}.${key}`}
585
+ id={`${id}.${key}`}
586
+ label={
587
+ key.charAt(0).toUpperCase() +
588
+ key.slice(1).replaceAll(/([A-Z])/g, " $1")
589
+ }
590
+ propSchema={subSchema}
591
+ value={currentValue?.[key]}
592
+ isRequired={selectedVariant.required?.includes(key)}
593
+ formValues={formValues}
594
+ optionsResolvers={optionsResolvers}
595
+ templateProperties={templateProperties}
596
+ onChange={(val) => onChange({ ...currentValue, [key]: val })}
597
+ />
598
+ ))}
599
+ </div>
600
+ );
601
+ }
602
+
603
+ return <></>;
604
+ };
605
+
606
+ /**
607
+ * Secret field component with password visibility toggle.
608
+ * Extracted to keep hooks at component top level.
609
+ */
610
+ const SecretField: React.FC<{
611
+ id: string;
612
+ label: string;
613
+ description?: string;
614
+ value: string;
615
+ isRequired?: boolean;
616
+ onChange: (val: unknown) => void;
617
+ }> = ({ id, label, description, value, isRequired, onChange }) => {
618
+ const [showPassword, setShowPassword] = React.useState(false);
619
+ const currentValue = value || "";
620
+ const hasExistingValue = currentValue.length > 0;
621
+
622
+ return (
623
+ <div className="space-y-2">
624
+ <div>
625
+ <Label htmlFor={id}>
626
+ {label} {isRequired && "*"}
627
+ </Label>
628
+ {description && (
629
+ <p className="text-sm text-muted-foreground mt-0.5">{description}</p>
630
+ )}
631
+ </div>
632
+ <div className="relative">
633
+ <Input
634
+ id={id}
635
+ type={showPassword ? "text" : "password"}
636
+ value={currentValue}
637
+ onChange={(e) => onChange(e.target.value)}
638
+ placeholder={hasExistingValue ? "••••••••" : "Enter secret value"}
639
+ className="pr-10"
640
+ />
641
+ <button
642
+ type="button"
643
+ onClick={() => setShowPassword(!showPassword)}
644
+ className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
645
+ >
646
+ {showPassword ? (
647
+ <svg
648
+ className="h-5 w-5"
649
+ fill="none"
650
+ stroke="currentColor"
651
+ viewBox="0 0 24 24"
652
+ >
653
+ <path
654
+ strokeLinecap="round"
655
+ strokeLinejoin="round"
656
+ strokeWidth={2}
657
+ d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
658
+ />
659
+ </svg>
660
+ ) : (
661
+ <svg
662
+ className="h-5 w-5"
663
+ fill="none"
664
+ stroke="currentColor"
665
+ viewBox="0 0 24 24"
666
+ >
667
+ <path
668
+ strokeLinecap="round"
669
+ strokeLinejoin="round"
670
+ strokeWidth={2}
671
+ d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
672
+ />
673
+ <path
674
+ strokeLinecap="round"
675
+ strokeLinejoin="round"
676
+ strokeWidth={2}
677
+ d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
678
+ />
679
+ </svg>
680
+ )}
681
+ </button>
682
+ </div>
683
+ {hasExistingValue && currentValue === "" && (
684
+ <p className="text-xs text-muted-foreground">
685
+ Leave empty to keep existing value
686
+ </p>
687
+ )}
688
+ </div>
689
+ );
690
+ };