myoperator-ui 0.0.67 → 0.0.69
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/dist/index.js +945 -12
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1159,8 +1159,9 @@ export {
|
|
|
1159
1159
|
},
|
|
1160
1160
|
"input": {
|
|
1161
1161
|
name: "input",
|
|
1162
|
-
description: "input component",
|
|
1162
|
+
description: "A text input component with error and disabled states",
|
|
1163
1163
|
dependencies: [
|
|
1164
|
+
"class-variance-authority",
|
|
1164
1165
|
"clsx",
|
|
1165
1166
|
"tailwind-merge"
|
|
1166
1167
|
],
|
|
@@ -1225,10 +1226,12 @@ export { Input, inputVariants }
|
|
|
1225
1226
|
},
|
|
1226
1227
|
"multi-select": {
|
|
1227
1228
|
name: "multi-select",
|
|
1228
|
-
description: "multi-select component",
|
|
1229
|
+
description: "A multi-select dropdown component with search, badges, and async loading",
|
|
1229
1230
|
dependencies: [
|
|
1231
|
+
"class-variance-authority",
|
|
1230
1232
|
"clsx",
|
|
1231
|
-
"tailwind-merge"
|
|
1233
|
+
"tailwind-merge",
|
|
1234
|
+
"lucide-react"
|
|
1232
1235
|
],
|
|
1233
1236
|
files: [
|
|
1234
1237
|
{
|
|
@@ -1648,10 +1651,12 @@ export { MultiSelect, multiSelectTriggerVariants }
|
|
|
1648
1651
|
},
|
|
1649
1652
|
"select-field": {
|
|
1650
1653
|
name: "select-field",
|
|
1651
|
-
description: "select
|
|
1654
|
+
description: "A select field with label, helper text, and validation states",
|
|
1652
1655
|
dependencies: [
|
|
1656
|
+
"@radix-ui/react-select",
|
|
1653
1657
|
"clsx",
|
|
1654
|
-
"tailwind-merge"
|
|
1658
|
+
"tailwind-merge",
|
|
1659
|
+
"lucide-react"
|
|
1655
1660
|
],
|
|
1656
1661
|
files: [
|
|
1657
1662
|
{
|
|
@@ -1938,10 +1943,13 @@ export { SelectField }
|
|
|
1938
1943
|
},
|
|
1939
1944
|
"select": {
|
|
1940
1945
|
name: "select",
|
|
1941
|
-
description: "select component",
|
|
1946
|
+
description: "A select dropdown component built on Radix UI Select",
|
|
1942
1947
|
dependencies: [
|
|
1948
|
+
"@radix-ui/react-select",
|
|
1949
|
+
"class-variance-authority",
|
|
1943
1950
|
"clsx",
|
|
1944
|
-
"tailwind-merge"
|
|
1951
|
+
"tailwind-merge",
|
|
1952
|
+
"lucide-react"
|
|
1945
1953
|
],
|
|
1946
1954
|
files: [
|
|
1947
1955
|
{
|
|
@@ -2599,10 +2607,12 @@ export { Tag, TagGroup, tagVariants }
|
|
|
2599
2607
|
},
|
|
2600
2608
|
"text-field": {
|
|
2601
2609
|
name: "text-field",
|
|
2602
|
-
description: "text
|
|
2610
|
+
description: "A text field with label, helper text, icons, and validation states",
|
|
2603
2611
|
dependencies: [
|
|
2612
|
+
"class-variance-authority",
|
|
2604
2613
|
"clsx",
|
|
2605
|
-
"tailwind-merge"
|
|
2614
|
+
"tailwind-merge",
|
|
2615
|
+
"lucide-react"
|
|
2606
2616
|
],
|
|
2607
2617
|
files: [
|
|
2608
2618
|
{
|
|
@@ -3014,6 +3024,908 @@ const Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(
|
|
|
3014
3024
|
Toggle.displayName = "Toggle"
|
|
3015
3025
|
|
|
3016
3026
|
export { Toggle, toggleVariants }
|
|
3027
|
+
`, prefix)
|
|
3028
|
+
}
|
|
3029
|
+
]
|
|
3030
|
+
},
|
|
3031
|
+
"event-selector": {
|
|
3032
|
+
name: "event-selector",
|
|
3033
|
+
description: "A component for selecting webhook events with groups, categories, and tri-state checkboxes",
|
|
3034
|
+
dependencies: [
|
|
3035
|
+
"clsx",
|
|
3036
|
+
"tailwind-merge"
|
|
3037
|
+
],
|
|
3038
|
+
internalDependencies: [
|
|
3039
|
+
"checkbox",
|
|
3040
|
+
"collapsible"
|
|
3041
|
+
],
|
|
3042
|
+
isMultiFile: true,
|
|
3043
|
+
directory: "event-selector",
|
|
3044
|
+
mainFile: "event-selector.tsx",
|
|
3045
|
+
files: [
|
|
3046
|
+
{
|
|
3047
|
+
name: "event-selector.tsx",
|
|
3048
|
+
content: prefixTailwindClasses(`import * as React from "react"
|
|
3049
|
+
import { cn } from "../../../lib/utils"
|
|
3050
|
+
import { EventGroupComponent } from "./event-group"
|
|
3051
|
+
import type { EventSelectorProps, EventCategory, EventGroup } from "./types"
|
|
3052
|
+
|
|
3053
|
+
/**
|
|
3054
|
+
* EventSelector - A component for selecting webhook events
|
|
3055
|
+
*
|
|
3056
|
+
* Install via CLI:
|
|
3057
|
+
* \`\`\`bash
|
|
3058
|
+
* npx myoperator-ui add event-selector
|
|
3059
|
+
* \`\`\`
|
|
3060
|
+
*
|
|
3061
|
+
* Or import directly from npm:
|
|
3062
|
+
* \`\`\`tsx
|
|
3063
|
+
* import { EventSelector } from "@myoperator/ui"
|
|
3064
|
+
* \`\`\`
|
|
3065
|
+
*
|
|
3066
|
+
* @example
|
|
3067
|
+
* \`\`\`tsx
|
|
3068
|
+
* <EventSelector
|
|
3069
|
+
* events={events}
|
|
3070
|
+
* groups={groups}
|
|
3071
|
+
* selectedEvents={selected}
|
|
3072
|
+
* onSelectionChange={setSelected}
|
|
3073
|
+
* />
|
|
3074
|
+
* \`\`\`
|
|
3075
|
+
*/
|
|
3076
|
+
export const EventSelector = React.forwardRef<HTMLDivElement, EventSelectorProps>(
|
|
3077
|
+
(
|
|
3078
|
+
{
|
|
3079
|
+
events,
|
|
3080
|
+
groups,
|
|
3081
|
+
categories,
|
|
3082
|
+
selectedEvents: controlledSelected,
|
|
3083
|
+
onSelectionChange,
|
|
3084
|
+
defaultSelectedEvents = [],
|
|
3085
|
+
title = "Events",
|
|
3086
|
+
description = "Select which events should trigger this webhook",
|
|
3087
|
+
emptyGroupMessage,
|
|
3088
|
+
renderEmptyGroup,
|
|
3089
|
+
className,
|
|
3090
|
+
...props
|
|
3091
|
+
},
|
|
3092
|
+
ref
|
|
3093
|
+
) => {
|
|
3094
|
+
// Controlled vs uncontrolled state
|
|
3095
|
+
const [internalSelected, setInternalSelected] = React.useState<string[]>(
|
|
3096
|
+
defaultSelectedEvents
|
|
3097
|
+
)
|
|
3098
|
+
|
|
3099
|
+
const isControlled = controlledSelected !== undefined
|
|
3100
|
+
const selectedEvents = isControlled ? controlledSelected : internalSelected
|
|
3101
|
+
|
|
3102
|
+
const handleSelectionChange = React.useCallback(
|
|
3103
|
+
(newSelection: string[]) => {
|
|
3104
|
+
if (!isControlled) {
|
|
3105
|
+
setInternalSelected(newSelection)
|
|
3106
|
+
}
|
|
3107
|
+
onSelectionChange?.(newSelection)
|
|
3108
|
+
},
|
|
3109
|
+
[isControlled, onSelectionChange]
|
|
3110
|
+
)
|
|
3111
|
+
|
|
3112
|
+
// Get events for a specific group
|
|
3113
|
+
const getEventsForGroup = (groupId: string) => {
|
|
3114
|
+
return events.filter((event) => event.group === groupId)
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
// Get groups for a specific category
|
|
3118
|
+
const getGroupsForCategory = (category: EventCategory): EventGroup[] => {
|
|
3119
|
+
return category.groups
|
|
3120
|
+
.map((groupId) => groups.find((g) => g.id === groupId))
|
|
3121
|
+
.filter((g): g is EventGroup => g !== undefined)
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
// Calculate total selected count
|
|
3125
|
+
const totalSelected = selectedEvents.length
|
|
3126
|
+
|
|
3127
|
+
// Render groups without categories
|
|
3128
|
+
const renderGroups = (groupsToRender: EventGroup[]) => {
|
|
3129
|
+
return groupsToRender.map((group) => (
|
|
3130
|
+
<EventGroupComponent
|
|
3131
|
+
key={group.id}
|
|
3132
|
+
group={group}
|
|
3133
|
+
events={getEventsForGroup(group.id)}
|
|
3134
|
+
selectedEvents={selectedEvents}
|
|
3135
|
+
onSelectionChange={handleSelectionChange}
|
|
3136
|
+
emptyGroupMessage={emptyGroupMessage}
|
|
3137
|
+
renderEmptyGroup={renderEmptyGroup}
|
|
3138
|
+
/>
|
|
3139
|
+
))
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
// Render categories with nested groups
|
|
3143
|
+
const renderCategories = () => {
|
|
3144
|
+
// Ensure categories is an array before using array methods
|
|
3145
|
+
if (!categories || !Array.isArray(categories) || categories.length === 0) {
|
|
3146
|
+
return renderGroups(groups)
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
// Get groups that belong to categories
|
|
3150
|
+
const groupsInCategories = new Set(categories.flatMap((c) => c.groups))
|
|
3151
|
+
|
|
3152
|
+
// Get orphan groups (not in any category)
|
|
3153
|
+
const orphanGroups = groups.filter((g) => !groupsInCategories.has(g.id))
|
|
3154
|
+
|
|
3155
|
+
return (
|
|
3156
|
+
<>
|
|
3157
|
+
{categories.map((category) => {
|
|
3158
|
+
const categoryGroups = getGroupsForCategory(category)
|
|
3159
|
+
const categoryEventIds = categoryGroups.flatMap((g) =>
|
|
3160
|
+
getEventsForGroup(g.id).map((e) => e.id)
|
|
3161
|
+
)
|
|
3162
|
+
const selectedInCategory = categoryEventIds.filter((id) =>
|
|
3163
|
+
selectedEvents.includes(id)
|
|
3164
|
+
)
|
|
3165
|
+
|
|
3166
|
+
return (
|
|
3167
|
+
<div
|
|
3168
|
+
key={category.id}
|
|
3169
|
+
className="border border-[#E5E7EB] rounded-lg overflow-hidden"
|
|
3170
|
+
>
|
|
3171
|
+
{/* Category Header - no checkbox, just label */}
|
|
3172
|
+
<div className="flex items-center justify-between p-4 bg-white border-b border-[#E5E7EB]">
|
|
3173
|
+
<div className="flex items-center gap-3">
|
|
3174
|
+
{category.icon && (
|
|
3175
|
+
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-[#F3F4F6]">
|
|
3176
|
+
{category.icon}
|
|
3177
|
+
</div>
|
|
3178
|
+
)}
|
|
3179
|
+
<span className="font-medium text-[#333333]">
|
|
3180
|
+
{category.name}
|
|
3181
|
+
</span>
|
|
3182
|
+
</div>
|
|
3183
|
+
{selectedInCategory.length > 0 && (
|
|
3184
|
+
<span className="text-sm text-[#6B7280]">
|
|
3185
|
+
{selectedInCategory.length} Selected
|
|
3186
|
+
</span>
|
|
3187
|
+
)}
|
|
3188
|
+
</div>
|
|
3189
|
+
{/* Category Groups */}
|
|
3190
|
+
<div className="p-4 space-y-3 bg-[#F9FAFB]">
|
|
3191
|
+
{renderGroups(categoryGroups)}
|
|
3192
|
+
</div>
|
|
3193
|
+
</div>
|
|
3194
|
+
)
|
|
3195
|
+
})}
|
|
3196
|
+
{/* Render orphan groups outside categories */}
|
|
3197
|
+
{orphanGroups.length > 0 && (
|
|
3198
|
+
<div className="space-y-3">{renderGroups(orphanGroups)}</div>
|
|
3199
|
+
)}
|
|
3200
|
+
</>
|
|
3201
|
+
)
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
return (
|
|
3205
|
+
<div
|
|
3206
|
+
ref={ref}
|
|
3207
|
+
className={cn("w-full", className)}
|
|
3208
|
+
{...props}
|
|
3209
|
+
>
|
|
3210
|
+
{/* Header */}
|
|
3211
|
+
<div className="flex items-start justify-between mb-4">
|
|
3212
|
+
<div>
|
|
3213
|
+
<h3 className="text-base font-semibold text-[#333333]">{title}</h3>
|
|
3214
|
+
{description && (
|
|
3215
|
+
<p className="text-sm text-[#6B7280] mt-1">{description}</p>
|
|
3216
|
+
)}
|
|
3217
|
+
</div>
|
|
3218
|
+
<span className="text-sm font-medium text-[#333333]">
|
|
3219
|
+
{totalSelected} Selected
|
|
3220
|
+
</span>
|
|
3221
|
+
</div>
|
|
3222
|
+
|
|
3223
|
+
{/* Groups */}
|
|
3224
|
+
<div className="space-y-3">{renderCategories()}</div>
|
|
3225
|
+
</div>
|
|
3226
|
+
)
|
|
3227
|
+
}
|
|
3228
|
+
)
|
|
3229
|
+
EventSelector.displayName = "EventSelector"
|
|
3230
|
+
`, prefix)
|
|
3231
|
+
},
|
|
3232
|
+
{
|
|
3233
|
+
name: "event-group.tsx",
|
|
3234
|
+
content: prefixTailwindClasses(`import * as React from "react"
|
|
3235
|
+
import { cn } from "../../../lib/utils"
|
|
3236
|
+
import { Checkbox, type CheckedState } from "../checkbox"
|
|
3237
|
+
import {
|
|
3238
|
+
Collapsible,
|
|
3239
|
+
CollapsibleItem,
|
|
3240
|
+
CollapsibleTrigger,
|
|
3241
|
+
CollapsibleContent,
|
|
3242
|
+
} from "../collapsible"
|
|
3243
|
+
import { EventItemComponent } from "./event-item"
|
|
3244
|
+
import type { EventGroupComponentProps } from "./types"
|
|
3245
|
+
|
|
3246
|
+
/**
|
|
3247
|
+
* Event group with collapsible section and group-level checkbox
|
|
3248
|
+
*/
|
|
3249
|
+
export const EventGroupComponent = React.forwardRef<
|
|
3250
|
+
HTMLDivElement,
|
|
3251
|
+
EventGroupComponentProps & React.HTMLAttributes<HTMLDivElement>
|
|
3252
|
+
>(
|
|
3253
|
+
(
|
|
3254
|
+
{
|
|
3255
|
+
group,
|
|
3256
|
+
events,
|
|
3257
|
+
selectedEvents,
|
|
3258
|
+
onSelectionChange,
|
|
3259
|
+
emptyGroupMessage = "No events available",
|
|
3260
|
+
renderEmptyGroup,
|
|
3261
|
+
className,
|
|
3262
|
+
...props
|
|
3263
|
+
},
|
|
3264
|
+
ref
|
|
3265
|
+
) => {
|
|
3266
|
+
// Calculate selection state for this group
|
|
3267
|
+
const groupEventIds = events.map((e) => e.id)
|
|
3268
|
+
const selectedInGroup = groupEventIds.filter((id) =>
|
|
3269
|
+
selectedEvents.includes(id)
|
|
3270
|
+
)
|
|
3271
|
+
const allSelected = groupEventIds.length > 0 && selectedInGroup.length === groupEventIds.length
|
|
3272
|
+
const someSelected = selectedInGroup.length > 0 && selectedInGroup.length < groupEventIds.length
|
|
3273
|
+
|
|
3274
|
+
const checkboxState: CheckedState = allSelected
|
|
3275
|
+
? true
|
|
3276
|
+
: someSelected
|
|
3277
|
+
? "indeterminate"
|
|
3278
|
+
: false
|
|
3279
|
+
|
|
3280
|
+
const selectedCount = selectedInGroup.length
|
|
3281
|
+
|
|
3282
|
+
// Handle group checkbox click
|
|
3283
|
+
const handleGroupCheckbox = () => {
|
|
3284
|
+
if (allSelected) {
|
|
3285
|
+
// Deselect all events in this group
|
|
3286
|
+
onSelectionChange(selectedEvents.filter((id) => !groupEventIds.includes(id)))
|
|
3287
|
+
} else {
|
|
3288
|
+
// Select all events in this group
|
|
3289
|
+
const newSelection = [...selectedEvents]
|
|
3290
|
+
groupEventIds.forEach((id) => {
|
|
3291
|
+
if (!newSelection.includes(id)) {
|
|
3292
|
+
newSelection.push(id)
|
|
3293
|
+
}
|
|
3294
|
+
})
|
|
3295
|
+
onSelectionChange(newSelection)
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
// Handle individual event selection
|
|
3300
|
+
const handleEventSelection = (eventId: string, selected: boolean) => {
|
|
3301
|
+
if (selected) {
|
|
3302
|
+
onSelectionChange([...selectedEvents, eventId])
|
|
3303
|
+
} else {
|
|
3304
|
+
onSelectionChange(selectedEvents.filter((id) => id !== eventId))
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
return (
|
|
3309
|
+
<div
|
|
3310
|
+
ref={ref}
|
|
3311
|
+
className={cn("bg-[#F9FAFB] rounded-lg", className)}
|
|
3312
|
+
{...props}
|
|
3313
|
+
>
|
|
3314
|
+
<Collapsible type="multiple">
|
|
3315
|
+
<CollapsibleItem value={group.id}>
|
|
3316
|
+
<CollapsibleTrigger
|
|
3317
|
+
showChevron={true}
|
|
3318
|
+
className="w-full p-4 hover:bg-[#F3F4F6] rounded-lg"
|
|
3319
|
+
>
|
|
3320
|
+
<div className="flex items-center gap-3 flex-1">
|
|
3321
|
+
<Checkbox
|
|
3322
|
+
checked={checkboxState}
|
|
3323
|
+
onCheckedChange={handleGroupCheckbox}
|
|
3324
|
+
onClick={(e) => e.stopPropagation()}
|
|
3325
|
+
aria-label={\`Select all \${group.name}\`}
|
|
3326
|
+
/>
|
|
3327
|
+
<div className="flex flex-col items-start text-left flex-1 min-w-0">
|
|
3328
|
+
<div className="flex items-center gap-2">
|
|
3329
|
+
{group.icon && (
|
|
3330
|
+
<span className="text-[#6B7280]">{group.icon}</span>
|
|
3331
|
+
)}
|
|
3332
|
+
<span className="font-medium text-[#333333]">
|
|
3333
|
+
{group.name}
|
|
3334
|
+
</span>
|
|
3335
|
+
</div>
|
|
3336
|
+
<span className="text-sm text-[#6B7280] mt-0.5">
|
|
3337
|
+
{group.description}
|
|
3338
|
+
</span>
|
|
3339
|
+
</div>
|
|
3340
|
+
{selectedCount > 0 && (
|
|
3341
|
+
<span className="text-sm text-[#6B7280] whitespace-nowrap">
|
|
3342
|
+
{selectedCount} Selected
|
|
3343
|
+
</span>
|
|
3344
|
+
)}
|
|
3345
|
+
</div>
|
|
3346
|
+
</CollapsibleTrigger>
|
|
3347
|
+
<CollapsibleContent>
|
|
3348
|
+
<div className="border-t border-[#E5E7EB]">
|
|
3349
|
+
{events.length > 0 ? (
|
|
3350
|
+
events.map((event) => (
|
|
3351
|
+
<EventItemComponent
|
|
3352
|
+
key={event.id}
|
|
3353
|
+
event={event}
|
|
3354
|
+
isSelected={selectedEvents.includes(event.id)}
|
|
3355
|
+
onSelectionChange={(selected) =>
|
|
3356
|
+
handleEventSelection(event.id, selected)
|
|
3357
|
+
}
|
|
3358
|
+
/>
|
|
3359
|
+
))
|
|
3360
|
+
) : renderEmptyGroup ? (
|
|
3361
|
+
<div className="py-4 px-8">
|
|
3362
|
+
{renderEmptyGroup(group)}
|
|
3363
|
+
</div>
|
|
3364
|
+
) : (
|
|
3365
|
+
<div className="py-4 px-8 text-sm text-[#6B7280] italic">
|
|
3366
|
+
{emptyGroupMessage}
|
|
3367
|
+
</div>
|
|
3368
|
+
)}
|
|
3369
|
+
</div>
|
|
3370
|
+
</CollapsibleContent>
|
|
3371
|
+
</CollapsibleItem>
|
|
3372
|
+
</Collapsible>
|
|
3373
|
+
</div>
|
|
3374
|
+
)
|
|
3375
|
+
}
|
|
3376
|
+
)
|
|
3377
|
+
EventGroupComponent.displayName = "EventGroupComponent"
|
|
3378
|
+
`, prefix)
|
|
3379
|
+
},
|
|
3380
|
+
{
|
|
3381
|
+
name: "event-item.tsx",
|
|
3382
|
+
content: prefixTailwindClasses(`import * as React from "react"
|
|
3383
|
+
import { cn } from "../../../lib/utils"
|
|
3384
|
+
import { Checkbox } from "../checkbox"
|
|
3385
|
+
import type { EventItemComponentProps } from "./types"
|
|
3386
|
+
|
|
3387
|
+
/**
|
|
3388
|
+
* Individual event item with checkbox
|
|
3389
|
+
*/
|
|
3390
|
+
export const EventItemComponent = React.forwardRef<
|
|
3391
|
+
HTMLDivElement,
|
|
3392
|
+
EventItemComponentProps & React.HTMLAttributes<HTMLDivElement>
|
|
3393
|
+
>(({ event, isSelected, onSelectionChange, className, ...props }, ref) => {
|
|
3394
|
+
return (
|
|
3395
|
+
<div
|
|
3396
|
+
ref={ref}
|
|
3397
|
+
className={cn(
|
|
3398
|
+
"flex items-start gap-3 py-2 pl-8 pr-4",
|
|
3399
|
+
className
|
|
3400
|
+
)}
|
|
3401
|
+
{...props}
|
|
3402
|
+
>
|
|
3403
|
+
<Checkbox
|
|
3404
|
+
checked={isSelected}
|
|
3405
|
+
onCheckedChange={(checked) => onSelectionChange(checked === true)}
|
|
3406
|
+
aria-label={\`Select \${event.name}\`}
|
|
3407
|
+
/>
|
|
3408
|
+
<div className="flex-1 min-w-0">
|
|
3409
|
+
<div className="text-sm font-medium text-[#333333]">{event.name}</div>
|
|
3410
|
+
<div className="text-sm text-[#6B7280] mt-0.5 leading-relaxed">
|
|
3411
|
+
{event.description}
|
|
3412
|
+
</div>
|
|
3413
|
+
</div>
|
|
3414
|
+
</div>
|
|
3415
|
+
)
|
|
3416
|
+
})
|
|
3417
|
+
EventItemComponent.displayName = "EventItemComponent"
|
|
3418
|
+
`, prefix)
|
|
3419
|
+
},
|
|
3420
|
+
{
|
|
3421
|
+
name: "types.ts",
|
|
3422
|
+
content: prefixTailwindClasses(`import * as React from "react"
|
|
3423
|
+
|
|
3424
|
+
/**
|
|
3425
|
+
* Represents an individual event item
|
|
3426
|
+
*/
|
|
3427
|
+
export interface EventItem {
|
|
3428
|
+
/** Unique identifier for the event */
|
|
3429
|
+
id: string
|
|
3430
|
+
/** Display name of the event (e.g., "Call.Initiated") */
|
|
3431
|
+
name: string
|
|
3432
|
+
/** Description of when this event is triggered */
|
|
3433
|
+
description: string
|
|
3434
|
+
/** Group ID this event belongs to */
|
|
3435
|
+
group: string
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
/**
|
|
3439
|
+
* Represents a group of events
|
|
3440
|
+
*/
|
|
3441
|
+
export interface EventGroup {
|
|
3442
|
+
/** Unique identifier for the group */
|
|
3443
|
+
id: string
|
|
3444
|
+
/** Display name of the group (e.g., "In-Call Events") */
|
|
3445
|
+
name: string
|
|
3446
|
+
/** Description of the group */
|
|
3447
|
+
description: string
|
|
3448
|
+
/** Optional icon to display next to the group name */
|
|
3449
|
+
icon?: React.ReactNode
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
/**
|
|
3453
|
+
* Optional top-level category that can contain multiple groups
|
|
3454
|
+
*/
|
|
3455
|
+
export interface EventCategory {
|
|
3456
|
+
/** Unique identifier for the category */
|
|
3457
|
+
id: string
|
|
3458
|
+
/** Display name of the category (e.g., "Call Events (Voice)") */
|
|
3459
|
+
name: string
|
|
3460
|
+
/** Optional icon to display next to the category name */
|
|
3461
|
+
icon?: React.ReactNode
|
|
3462
|
+
/** Array of group IDs that belong to this category */
|
|
3463
|
+
groups: string[]
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
/**
|
|
3467
|
+
* Props for the EventSelector component
|
|
3468
|
+
*/
|
|
3469
|
+
export interface EventSelectorProps {
|
|
3470
|
+
// Data
|
|
3471
|
+
/** Array of event items */
|
|
3472
|
+
events: EventItem[]
|
|
3473
|
+
/** Array of event groups */
|
|
3474
|
+
groups: EventGroup[]
|
|
3475
|
+
/** Optional array of categories for top-level grouping */
|
|
3476
|
+
categories?: EventCategory[]
|
|
3477
|
+
|
|
3478
|
+
// State (controlled mode)
|
|
3479
|
+
/** Array of selected event IDs (controlled) */
|
|
3480
|
+
selectedEvents?: string[]
|
|
3481
|
+
/** Callback when selection changes */
|
|
3482
|
+
onSelectionChange?: (selectedIds: string[]) => void
|
|
3483
|
+
|
|
3484
|
+
// State (uncontrolled mode)
|
|
3485
|
+
/** Default selected events for uncontrolled usage */
|
|
3486
|
+
defaultSelectedEvents?: string[]
|
|
3487
|
+
|
|
3488
|
+
// Customization
|
|
3489
|
+
/** Title displayed at the top (default: "Events") */
|
|
3490
|
+
title?: string
|
|
3491
|
+
/** Description displayed below the title */
|
|
3492
|
+
description?: string
|
|
3493
|
+
/** Message shown when a group has no events */
|
|
3494
|
+
emptyGroupMessage?: string
|
|
3495
|
+
/** Custom render function for empty group state (overrides emptyGroupMessage) */
|
|
3496
|
+
renderEmptyGroup?: (group: EventGroup) => React.ReactNode
|
|
3497
|
+
|
|
3498
|
+
// Styling
|
|
3499
|
+
/** Additional CSS classes for the root element */
|
|
3500
|
+
className?: string
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
/**
|
|
3504
|
+
* Internal props for EventGroup component
|
|
3505
|
+
*/
|
|
3506
|
+
export interface EventGroupComponentProps {
|
|
3507
|
+
/** The group data */
|
|
3508
|
+
group: EventGroup
|
|
3509
|
+
/** Events that belong to this group */
|
|
3510
|
+
events: EventItem[]
|
|
3511
|
+
/** Currently selected event IDs */
|
|
3512
|
+
selectedEvents: string[]
|
|
3513
|
+
/** Callback to update selected events */
|
|
3514
|
+
onSelectionChange: (selectedIds: string[]) => void
|
|
3515
|
+
/** Message shown when group has no events */
|
|
3516
|
+
emptyGroupMessage?: string
|
|
3517
|
+
/** Custom render function for empty group state */
|
|
3518
|
+
renderEmptyGroup?: (group: EventGroup) => React.ReactNode
|
|
3519
|
+
}
|
|
3520
|
+
|
|
3521
|
+
/**
|
|
3522
|
+
* Internal props for EventItem component
|
|
3523
|
+
*/
|
|
3524
|
+
export interface EventItemComponentProps {
|
|
3525
|
+
/** The event data */
|
|
3526
|
+
event: EventItem
|
|
3527
|
+
/** Whether this event is selected */
|
|
3528
|
+
isSelected: boolean
|
|
3529
|
+
/** Callback when selection changes */
|
|
3530
|
+
onSelectionChange: (selected: boolean) => void
|
|
3531
|
+
}
|
|
3532
|
+
`, prefix)
|
|
3533
|
+
}
|
|
3534
|
+
]
|
|
3535
|
+
},
|
|
3536
|
+
"key-value-input": {
|
|
3537
|
+
name: "key-value-input",
|
|
3538
|
+
description: "A component for managing key-value pairs with validation and duplicate detection",
|
|
3539
|
+
dependencies: [
|
|
3540
|
+
"clsx",
|
|
3541
|
+
"tailwind-merge",
|
|
3542
|
+
"lucide-react"
|
|
3543
|
+
],
|
|
3544
|
+
internalDependencies: [
|
|
3545
|
+
"button",
|
|
3546
|
+
"input"
|
|
3547
|
+
],
|
|
3548
|
+
isMultiFile: true,
|
|
3549
|
+
directory: "key-value-input",
|
|
3550
|
+
mainFile: "key-value-input.tsx",
|
|
3551
|
+
files: [
|
|
3552
|
+
{
|
|
3553
|
+
name: "key-value-input.tsx",
|
|
3554
|
+
content: prefixTailwindClasses(`import * as React from "react"
|
|
3555
|
+
import { Plus } from "lucide-react"
|
|
3556
|
+
import { cn } from "../../../lib/utils"
|
|
3557
|
+
import { Button } from "../button"
|
|
3558
|
+
import { KeyValueRow } from "./key-value-row"
|
|
3559
|
+
import type { KeyValueInputProps, KeyValuePair } from "./types"
|
|
3560
|
+
|
|
3561
|
+
// Helper to generate unique IDs
|
|
3562
|
+
const generateId = () =>
|
|
3563
|
+
\`kv-\${Date.now()}-\${Math.random().toString(36).substr(2, 9)}\`
|
|
3564
|
+
|
|
3565
|
+
/**
|
|
3566
|
+
* KeyValueInput - A component for managing key-value pairs
|
|
3567
|
+
*
|
|
3568
|
+
* Install via CLI:
|
|
3569
|
+
* \`\`\`bash
|
|
3570
|
+
* npx myoperator-ui add key-value-input
|
|
3571
|
+
* \`\`\`
|
|
3572
|
+
*
|
|
3573
|
+
* Or import directly from npm:
|
|
3574
|
+
* \`\`\`tsx
|
|
3575
|
+
* import { KeyValueInput } from "@myoperator/ui"
|
|
3576
|
+
* \`\`\`
|
|
3577
|
+
*
|
|
3578
|
+
* @example
|
|
3579
|
+
* \`\`\`tsx
|
|
3580
|
+
* <KeyValueInput
|
|
3581
|
+
* title="HTTP Headers"
|
|
3582
|
+
* description="Add custom headers for the webhook request"
|
|
3583
|
+
* value={headers}
|
|
3584
|
+
* onChange={setHeaders}
|
|
3585
|
+
* />
|
|
3586
|
+
* \`\`\`
|
|
3587
|
+
*/
|
|
3588
|
+
export const KeyValueInput = React.forwardRef<
|
|
3589
|
+
HTMLDivElement,
|
|
3590
|
+
KeyValueInputProps
|
|
3591
|
+
>(
|
|
3592
|
+
(
|
|
3593
|
+
{
|
|
3594
|
+
title,
|
|
3595
|
+
description,
|
|
3596
|
+
addButtonText = "Add Header",
|
|
3597
|
+
maxItems = 10,
|
|
3598
|
+
keyPlaceholder = "Key",
|
|
3599
|
+
valuePlaceholder = "Value",
|
|
3600
|
+
keyLabel = "Key",
|
|
3601
|
+
valueLabel = "Value",
|
|
3602
|
+
value: controlledValue,
|
|
3603
|
+
onChange,
|
|
3604
|
+
defaultValue = [],
|
|
3605
|
+
className,
|
|
3606
|
+
...props
|
|
3607
|
+
},
|
|
3608
|
+
ref
|
|
3609
|
+
) => {
|
|
3610
|
+
// Controlled vs uncontrolled state
|
|
3611
|
+
const [internalPairs, setInternalPairs] =
|
|
3612
|
+
React.useState<KeyValuePair[]>(defaultValue)
|
|
3613
|
+
|
|
3614
|
+
const isControlled = controlledValue !== undefined
|
|
3615
|
+
const pairs = isControlled ? controlledValue : internalPairs
|
|
3616
|
+
|
|
3617
|
+
// Track which keys have been touched for validation
|
|
3618
|
+
const [touchedKeys, setTouchedKeys] = React.useState<Set<string>>(new Set())
|
|
3619
|
+
|
|
3620
|
+
const handlePairsChange = React.useCallback(
|
|
3621
|
+
(newPairs: KeyValuePair[]) => {
|
|
3622
|
+
if (!isControlled) {
|
|
3623
|
+
setInternalPairs(newPairs)
|
|
3624
|
+
}
|
|
3625
|
+
onChange?.(newPairs)
|
|
3626
|
+
},
|
|
3627
|
+
[isControlled, onChange]
|
|
3628
|
+
)
|
|
3629
|
+
|
|
3630
|
+
// Check for duplicate keys (case-insensitive)
|
|
3631
|
+
const getDuplicateKeys = React.useCallback((): Set<string> => {
|
|
3632
|
+
const keyCount = new Map<string, number>()
|
|
3633
|
+
pairs.forEach((pair) => {
|
|
3634
|
+
if (pair.key.trim()) {
|
|
3635
|
+
const key = pair.key.toLowerCase()
|
|
3636
|
+
keyCount.set(key, (keyCount.get(key) || 0) + 1)
|
|
3637
|
+
}
|
|
3638
|
+
})
|
|
3639
|
+
const duplicates = new Set<string>()
|
|
3640
|
+
keyCount.forEach((count, key) => {
|
|
3641
|
+
if (count > 1) duplicates.add(key)
|
|
3642
|
+
})
|
|
3643
|
+
return duplicates
|
|
3644
|
+
}, [pairs])
|
|
3645
|
+
|
|
3646
|
+
const duplicateKeys = getDuplicateKeys()
|
|
3647
|
+
|
|
3648
|
+
// Add new row
|
|
3649
|
+
const handleAdd = () => {
|
|
3650
|
+
if (pairs.length >= maxItems) return
|
|
3651
|
+
const newPair: KeyValuePair = {
|
|
3652
|
+
id: generateId(),
|
|
3653
|
+
key: "",
|
|
3654
|
+
value: "",
|
|
3655
|
+
}
|
|
3656
|
+
handlePairsChange([...pairs, newPair])
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
// Update key
|
|
3660
|
+
const handleKeyChange = (id: string, key: string) => {
|
|
3661
|
+
handlePairsChange(
|
|
3662
|
+
pairs.map((pair) => (pair.id === id ? { ...pair, key } : pair))
|
|
3663
|
+
)
|
|
3664
|
+
setTouchedKeys((prev) => new Set(prev).add(id))
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
// Update value
|
|
3668
|
+
const handleValueChange = (id: string, value: string) => {
|
|
3669
|
+
handlePairsChange(
|
|
3670
|
+
pairs.map((pair) => (pair.id === id ? { ...pair, value } : pair))
|
|
3671
|
+
)
|
|
3672
|
+
}
|
|
3673
|
+
|
|
3674
|
+
// Delete row
|
|
3675
|
+
const handleDelete = (id: string) => {
|
|
3676
|
+
handlePairsChange(pairs.filter((pair) => pair.id !== id))
|
|
3677
|
+
setTouchedKeys((prev) => {
|
|
3678
|
+
const next = new Set(prev)
|
|
3679
|
+
next.delete(id)
|
|
3680
|
+
return next
|
|
3681
|
+
})
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
const isAtLimit = pairs.length >= maxItems
|
|
3685
|
+
const addButtonTitle = isAtLimit
|
|
3686
|
+
? \`Maximum of \${maxItems} items allowed\`
|
|
3687
|
+
: undefined
|
|
3688
|
+
|
|
3689
|
+
return (
|
|
3690
|
+
<div ref={ref} className={cn("w-full", className)} {...props}>
|
|
3691
|
+
{/* Header */}
|
|
3692
|
+
<div className="mb-3">
|
|
3693
|
+
<h3 className="text-base font-semibold text-[#333333]">{title}</h3>
|
|
3694
|
+
{description && (
|
|
3695
|
+
<p className="text-sm text-[#6B7280] mt-1">{description}</p>
|
|
3696
|
+
)}
|
|
3697
|
+
</div>
|
|
3698
|
+
|
|
3699
|
+
{/* Content Container with Background - only show when there are items */}
|
|
3700
|
+
{pairs.length > 0 && (
|
|
3701
|
+
<div className="bg-[#F9FAFB] rounded-lg p-4 mb-4">
|
|
3702
|
+
{/* Column Headers */}
|
|
3703
|
+
<div className="flex items-center gap-3 mb-3">
|
|
3704
|
+
<div className="flex-1">
|
|
3705
|
+
<span className="text-sm font-medium text-[#333333]">
|
|
3706
|
+
{keyLabel}
|
|
3707
|
+
<span className="text-[#FF3B3B] ml-0.5">*</span>
|
|
3708
|
+
</span>
|
|
3709
|
+
</div>
|
|
3710
|
+
<div className="flex-1">
|
|
3711
|
+
<span className="text-sm font-medium text-[#333333]">
|
|
3712
|
+
{valueLabel}
|
|
3713
|
+
</span>
|
|
3714
|
+
</div>
|
|
3715
|
+
{/* Spacer for delete button column */}
|
|
3716
|
+
<div className="w-8 flex-shrink-0" />
|
|
3717
|
+
</div>
|
|
3718
|
+
|
|
3719
|
+
{/* Rows */}
|
|
3720
|
+
<div className="space-y-3">
|
|
3721
|
+
{pairs.map((pair) => (
|
|
3722
|
+
<KeyValueRow
|
|
3723
|
+
key={pair.id}
|
|
3724
|
+
pair={pair}
|
|
3725
|
+
isDuplicateKey={duplicateKeys.has(pair.key.toLowerCase())}
|
|
3726
|
+
isKeyEmpty={touchedKeys.has(pair.id) && !pair.key.trim()}
|
|
3727
|
+
keyPlaceholder={keyPlaceholder}
|
|
3728
|
+
valuePlaceholder={valuePlaceholder}
|
|
3729
|
+
onKeyChange={handleKeyChange}
|
|
3730
|
+
onValueChange={handleValueChange}
|
|
3731
|
+
onDelete={handleDelete}
|
|
3732
|
+
/>
|
|
3733
|
+
))}
|
|
3734
|
+
</div>
|
|
3735
|
+
</div>
|
|
3736
|
+
)}
|
|
3737
|
+
|
|
3738
|
+
{/* Add Button using dashed variant - outside the gray container */}
|
|
3739
|
+
<Button
|
|
3740
|
+
type="button"
|
|
3741
|
+
variant="dashed"
|
|
3742
|
+
onClick={handleAdd}
|
|
3743
|
+
disabled={isAtLimit}
|
|
3744
|
+
title={addButtonTitle}
|
|
3745
|
+
className="w-full justify-center"
|
|
3746
|
+
>
|
|
3747
|
+
<Plus className="h-4 w-4" />
|
|
3748
|
+
{addButtonText}
|
|
3749
|
+
</Button>
|
|
3750
|
+
|
|
3751
|
+
{/* Limit indicator */}
|
|
3752
|
+
{isAtLimit && (
|
|
3753
|
+
<p className="text-xs text-[#6B7280] mt-2 text-center">
|
|
3754
|
+
Maximum of {maxItems} items reached
|
|
3755
|
+
</p>
|
|
3756
|
+
)}
|
|
3757
|
+
</div>
|
|
3758
|
+
)
|
|
3759
|
+
}
|
|
3760
|
+
)
|
|
3761
|
+
KeyValueInput.displayName = "KeyValueInput"
|
|
3762
|
+
`, prefix)
|
|
3763
|
+
},
|
|
3764
|
+
{
|
|
3765
|
+
name: "key-value-row.tsx",
|
|
3766
|
+
content: prefixTailwindClasses(`import * as React from "react"
|
|
3767
|
+
import { Trash2 } from "lucide-react"
|
|
3768
|
+
import { cn } from "../../../lib/utils"
|
|
3769
|
+
import { Input } from "../input"
|
|
3770
|
+
import { Button } from "../button"
|
|
3771
|
+
import type { KeyValueRowProps } from "./types"
|
|
3772
|
+
|
|
3773
|
+
/**
|
|
3774
|
+
* Individual key-value pair row with inputs and delete button
|
|
3775
|
+
*/
|
|
3776
|
+
export const KeyValueRow = React.forwardRef<
|
|
3777
|
+
HTMLDivElement,
|
|
3778
|
+
KeyValueRowProps & React.HTMLAttributes<HTMLDivElement>
|
|
3779
|
+
>(
|
|
3780
|
+
(
|
|
3781
|
+
{
|
|
3782
|
+
pair,
|
|
3783
|
+
isDuplicateKey,
|
|
3784
|
+
isKeyEmpty,
|
|
3785
|
+
keyPlaceholder = "Key",
|
|
3786
|
+
valuePlaceholder = "Value",
|
|
3787
|
+
onKeyChange,
|
|
3788
|
+
onValueChange,
|
|
3789
|
+
onDelete,
|
|
3790
|
+
className,
|
|
3791
|
+
...props
|
|
3792
|
+
},
|
|
3793
|
+
ref
|
|
3794
|
+
) => {
|
|
3795
|
+
// Determine if key input should show error state
|
|
3796
|
+
const keyHasError = isDuplicateKey || isKeyEmpty
|
|
3797
|
+
|
|
3798
|
+
// Determine error message
|
|
3799
|
+
const errorMessage = isDuplicateKey
|
|
3800
|
+
? "Duplicate key"
|
|
3801
|
+
: isKeyEmpty
|
|
3802
|
+
? "Key is required"
|
|
3803
|
+
: null
|
|
3804
|
+
|
|
3805
|
+
return (
|
|
3806
|
+
<div
|
|
3807
|
+
ref={ref}
|
|
3808
|
+
className={cn("flex items-start gap-3", className)}
|
|
3809
|
+
{...props}
|
|
3810
|
+
>
|
|
3811
|
+
{/* Key Input */}
|
|
3812
|
+
<div className="flex-1">
|
|
3813
|
+
<Input
|
|
3814
|
+
value={pair.key}
|
|
3815
|
+
onChange={(e) => onKeyChange(pair.id, e.target.value)}
|
|
3816
|
+
placeholder={keyPlaceholder}
|
|
3817
|
+
state={keyHasError ? "error" : "default"}
|
|
3818
|
+
aria-label="Key"
|
|
3819
|
+
/>
|
|
3820
|
+
{errorMessage && (
|
|
3821
|
+
<span className="text-xs text-[#FF3B3B] mt-1 block">
|
|
3822
|
+
{errorMessage}
|
|
3823
|
+
</span>
|
|
3824
|
+
)}
|
|
3825
|
+
</div>
|
|
3826
|
+
|
|
3827
|
+
{/* Value Input */}
|
|
3828
|
+
<div className="flex-1">
|
|
3829
|
+
<Input
|
|
3830
|
+
value={pair.value}
|
|
3831
|
+
onChange={(e) => onValueChange(pair.id, e.target.value)}
|
|
3832
|
+
placeholder={valuePlaceholder}
|
|
3833
|
+
aria-label="Value"
|
|
3834
|
+
/>
|
|
3835
|
+
</div>
|
|
3836
|
+
|
|
3837
|
+
{/* Delete Button */}
|
|
3838
|
+
<Button
|
|
3839
|
+
type="button"
|
|
3840
|
+
variant="ghost"
|
|
3841
|
+
size="icon"
|
|
3842
|
+
onClick={() => onDelete(pair.id)}
|
|
3843
|
+
className="text-gray-400 hover:text-[#EF4444] hover:bg-[#FEF2F2] flex-shrink-0"
|
|
3844
|
+
aria-label="Delete row"
|
|
3845
|
+
>
|
|
3846
|
+
<Trash2 className="h-4 w-4" />
|
|
3847
|
+
</Button>
|
|
3848
|
+
</div>
|
|
3849
|
+
)
|
|
3850
|
+
}
|
|
3851
|
+
)
|
|
3852
|
+
KeyValueRow.displayName = "KeyValueRow"
|
|
3853
|
+
`, prefix)
|
|
3854
|
+
},
|
|
3855
|
+
{
|
|
3856
|
+
name: "types.ts",
|
|
3857
|
+
content: prefixTailwindClasses(`import * as React from "react"
|
|
3858
|
+
|
|
3859
|
+
/**
|
|
3860
|
+
* Represents a single key-value pair
|
|
3861
|
+
*/
|
|
3862
|
+
export interface KeyValuePair {
|
|
3863
|
+
/** Unique identifier for the pair */
|
|
3864
|
+
id: string
|
|
3865
|
+
/** The key (e.g., header name) */
|
|
3866
|
+
key: string
|
|
3867
|
+
/** The value (e.g., header value) */
|
|
3868
|
+
value: string
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
/**
|
|
3872
|
+
* Props for the KeyValueInput component
|
|
3873
|
+
*/
|
|
3874
|
+
export interface KeyValueInputProps {
|
|
3875
|
+
// Customization
|
|
3876
|
+
/** Title displayed at the top (e.g., "HTTP Headers") */
|
|
3877
|
+
title: string
|
|
3878
|
+
/** Description displayed below the title */
|
|
3879
|
+
description?: string
|
|
3880
|
+
/** Text for the add button (default: "Add Header") */
|
|
3881
|
+
addButtonText?: string
|
|
3882
|
+
/** Maximum number of items allowed (default: 10) */
|
|
3883
|
+
maxItems?: number
|
|
3884
|
+
/** Placeholder for key input */
|
|
3885
|
+
keyPlaceholder?: string
|
|
3886
|
+
/** Placeholder for value input */
|
|
3887
|
+
valuePlaceholder?: string
|
|
3888
|
+
/** Label for key column header (default: "Key") */
|
|
3889
|
+
keyLabel?: string
|
|
3890
|
+
/** Label for value column header (default: "Value") */
|
|
3891
|
+
valueLabel?: string
|
|
3892
|
+
|
|
3893
|
+
// State (controlled mode)
|
|
3894
|
+
/** Array of key-value pairs (controlled) */
|
|
3895
|
+
value?: KeyValuePair[]
|
|
3896
|
+
/** Callback when pairs change */
|
|
3897
|
+
onChange?: (pairs: KeyValuePair[]) => void
|
|
3898
|
+
|
|
3899
|
+
// State (uncontrolled mode)
|
|
3900
|
+
/** Default key-value pairs for uncontrolled usage */
|
|
3901
|
+
defaultValue?: KeyValuePair[]
|
|
3902
|
+
|
|
3903
|
+
// Styling
|
|
3904
|
+
/** Additional CSS classes for the root element */
|
|
3905
|
+
className?: string
|
|
3906
|
+
}
|
|
3907
|
+
|
|
3908
|
+
/**
|
|
3909
|
+
* Internal props for KeyValueRow component
|
|
3910
|
+
*/
|
|
3911
|
+
export interface KeyValueRowProps {
|
|
3912
|
+
/** The key-value pair data */
|
|
3913
|
+
pair: KeyValuePair
|
|
3914
|
+
/** Whether the key is a duplicate */
|
|
3915
|
+
isDuplicateKey: boolean
|
|
3916
|
+
/** Whether key is empty (for validation) */
|
|
3917
|
+
isKeyEmpty: boolean
|
|
3918
|
+
/** Placeholder for key input */
|
|
3919
|
+
keyPlaceholder?: string
|
|
3920
|
+
/** Placeholder for value input */
|
|
3921
|
+
valuePlaceholder?: string
|
|
3922
|
+
/** Callback when key changes */
|
|
3923
|
+
onKeyChange: (id: string, key: string) => void
|
|
3924
|
+
/** Callback when value changes */
|
|
3925
|
+
onValueChange: (id: string, value: string) => void
|
|
3926
|
+
/** Callback when row is deleted */
|
|
3927
|
+
onDelete: (id: string) => void
|
|
3928
|
+
}
|
|
3017
3929
|
`, prefix)
|
|
3018
3930
|
}
|
|
3019
3931
|
]
|
|
@@ -3152,10 +4064,26 @@ async function add(components, options) {
|
|
|
3152
4064
|
try {
|
|
3153
4065
|
const installed = [];
|
|
3154
4066
|
const dependencies = /* @__PURE__ */ new Set();
|
|
3155
|
-
|
|
4067
|
+
const installedComponents = /* @__PURE__ */ new Set();
|
|
4068
|
+
const installComponent = async (componentName) => {
|
|
4069
|
+
if (installedComponents.has(componentName)) {
|
|
4070
|
+
return;
|
|
4071
|
+
}
|
|
3156
4072
|
const component = registry[componentName];
|
|
4073
|
+
if (!component) {
|
|
4074
|
+
spinner.warn(`Component ${componentName} not found in registry`);
|
|
4075
|
+
return;
|
|
4076
|
+
}
|
|
4077
|
+
if (component.internalDependencies && component.internalDependencies.length > 0) {
|
|
4078
|
+
spinner.text = `Installing dependencies for ${componentName}...`;
|
|
4079
|
+
for (const depName of component.internalDependencies) {
|
|
4080
|
+
await installComponent(depName);
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
4083
|
+
spinner.text = `Installing ${componentName}...`;
|
|
4084
|
+
const targetDir = component.isMultiFile ? path2.join(componentsDir, component.directory) : componentsDir;
|
|
3157
4085
|
for (const file of component.files) {
|
|
3158
|
-
const filePath = path2.join(
|
|
4086
|
+
const filePath = path2.join(targetDir, file.name);
|
|
3159
4087
|
if (await fs2.pathExists(filePath)) {
|
|
3160
4088
|
if (!options.overwrite) {
|
|
3161
4089
|
spinner.warn(`${file.name} already exists. Use --overwrite to replace.`);
|
|
@@ -3164,11 +4092,16 @@ async function add(components, options) {
|
|
|
3164
4092
|
}
|
|
3165
4093
|
await fs2.ensureDir(path2.dirname(filePath));
|
|
3166
4094
|
await fs2.writeFile(filePath, file.content);
|
|
3167
|
-
|
|
4095
|
+
const relativePath = component.isMultiFile ? `${component.directory}/${file.name}` : file.name;
|
|
4096
|
+
installed.push(relativePath);
|
|
3168
4097
|
}
|
|
3169
4098
|
if (component.dependencies) {
|
|
3170
4099
|
component.dependencies.forEach((dep) => dependencies.add(dep));
|
|
3171
4100
|
}
|
|
4101
|
+
installedComponents.add(componentName);
|
|
4102
|
+
};
|
|
4103
|
+
for (const componentName of components) {
|
|
4104
|
+
await installComponent(componentName);
|
|
3172
4105
|
}
|
|
3173
4106
|
spinner.succeed("Components installed successfully!");
|
|
3174
4107
|
if (installed.length > 0) {
|