@greatapps/greatagents-ui 0.1.0 → 0.2.0

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.
@@ -0,0 +1,577 @@
1
+ import {
2
+ type Announcements,
3
+ closestCenter,
4
+ closestCorners,
5
+ DndContext,
6
+ type DndContextProps,
7
+ type DragEndEvent,
8
+ type DraggableAttributes,
9
+ type DraggableSyntheticListeners,
10
+ DragOverlay,
11
+ type DragStartEvent,
12
+ type DropAnimation,
13
+ defaultDropAnimationSideEffects,
14
+ KeyboardSensor,
15
+ MouseSensor,
16
+ type ScreenReaderInstructions,
17
+ TouchSensor,
18
+ type UniqueIdentifier,
19
+ useSensor,
20
+ useSensors,
21
+ } from "@dnd-kit/core";
22
+ import {
23
+ restrictToHorizontalAxis,
24
+ restrictToParentElement,
25
+ restrictToVerticalAxis,
26
+ } from "@dnd-kit/modifiers";
27
+ import {
28
+ arrayMove,
29
+ horizontalListSortingStrategy,
30
+ SortableContext,
31
+ type SortableContextProps,
32
+ sortableKeyboardCoordinates,
33
+ useSortable,
34
+ verticalListSortingStrategy,
35
+ } from "@dnd-kit/sortable";
36
+ import { CSS } from "@dnd-kit/utilities";
37
+ import { Slot as SlotPrimitive } from "radix-ui";
38
+ import * as React from "react";
39
+ import * as ReactDOM from "react-dom";
40
+ import { useComposedRefs } from "../../lib/compose-refs";
41
+ import { cn } from "../../lib";
42
+
43
+ const orientationConfig = {
44
+ vertical: {
45
+ modifiers: [restrictToVerticalAxis, restrictToParentElement],
46
+ strategy: verticalListSortingStrategy,
47
+ collisionDetection: closestCenter,
48
+ },
49
+ horizontal: {
50
+ modifiers: [restrictToHorizontalAxis, restrictToParentElement],
51
+ strategy: horizontalListSortingStrategy,
52
+ collisionDetection: closestCenter,
53
+ },
54
+ mixed: {
55
+ modifiers: [restrictToParentElement],
56
+ strategy: undefined,
57
+ collisionDetection: closestCorners,
58
+ },
59
+ };
60
+
61
+ const ROOT_NAME = "Sortable";
62
+ const CONTENT_NAME = "SortableContent";
63
+ const ITEM_NAME = "SortableItem";
64
+ const ITEM_HANDLE_NAME = "SortableItemHandle";
65
+ const OVERLAY_NAME = "SortableOverlay";
66
+
67
+ interface SortableRootContextValue<T> {
68
+ id: string;
69
+ items: UniqueIdentifier[];
70
+ modifiers: DndContextProps["modifiers"];
71
+ strategy: SortableContextProps["strategy"];
72
+ activeId: UniqueIdentifier | null;
73
+ setActiveId: (id: UniqueIdentifier | null) => void;
74
+ getItemValue: (item: T) => UniqueIdentifier;
75
+ flatCursor: boolean;
76
+ }
77
+
78
+ const SortableRootContext =
79
+ React.createContext<SortableRootContextValue<unknown> | null>(null);
80
+
81
+ function useSortableContext(consumerName: string) {
82
+ const context = React.useContext(SortableRootContext);
83
+ if (!context) {
84
+ throw new Error(`\`${consumerName}\` must be used within \`${ROOT_NAME}\``);
85
+ }
86
+ return context;
87
+ }
88
+
89
+ interface GetItemValue<T> {
90
+ /**
91
+ * Callback that returns a unique identifier for each sortable item. Required for array of objects.
92
+ * @example getItemValue={(item) => item.id}
93
+ */
94
+ getItemValue: (item: T) => UniqueIdentifier;
95
+ }
96
+
97
+ type SortableProps<T> = DndContextProps &
98
+ (T extends object ? GetItemValue<T> : Partial<GetItemValue<T>>) & {
99
+ value: T[];
100
+ onValueChange?: (items: T[]) => void;
101
+ onMove?: (
102
+ event: DragEndEvent & { activeIndex: number; overIndex: number },
103
+ ) => void;
104
+ strategy?: SortableContextProps["strategy"];
105
+ orientation?: "vertical" | "horizontal" | "mixed";
106
+ flatCursor?: boolean;
107
+ };
108
+
109
+ function Sortable<T>(props: SortableProps<T>) {
110
+ const {
111
+ value,
112
+ onValueChange,
113
+ collisionDetection,
114
+ modifiers,
115
+ strategy,
116
+ onMove,
117
+ orientation = "vertical",
118
+ flatCursor = false,
119
+ getItemValue: getItemValueProp,
120
+ accessibility,
121
+ ...sortableProps
122
+ } = props;
123
+
124
+ const id = React.useId();
125
+ const [activeId, setActiveId] = React.useState<UniqueIdentifier | null>(null);
126
+
127
+ const sensors = useSensors(
128
+ useSensor(MouseSensor),
129
+ useSensor(TouchSensor),
130
+ useSensor(KeyboardSensor, {
131
+ coordinateGetter: sortableKeyboardCoordinates,
132
+ }),
133
+ );
134
+ const config = React.useMemo(
135
+ () => orientationConfig[orientation],
136
+ [orientation],
137
+ );
138
+
139
+ const getItemValue = React.useCallback(
140
+ (item: T): UniqueIdentifier => {
141
+ if (typeof item === "object" && !getItemValueProp) {
142
+ throw new Error(
143
+ "`getItemValue` is required when using array of objects",
144
+ );
145
+ }
146
+ return getItemValueProp
147
+ ? getItemValueProp(item)
148
+ : (item as UniqueIdentifier);
149
+ },
150
+ [getItemValueProp],
151
+ );
152
+
153
+ const items = React.useMemo(() => {
154
+ return value.map((item) => getItemValue(item));
155
+ }, [value, getItemValue]);
156
+
157
+ /* eslint-disable react-hooks/preserve-manual-memoization, react-hooks/exhaustive-deps -- shadcn sortable: intentional dependency on specific props */
158
+ const onDragStart = React.useCallback(
159
+ (event: DragStartEvent) => {
160
+ sortableProps.onDragStart?.(event);
161
+
162
+ if (event.activatorEvent.defaultPrevented) return;
163
+
164
+ setActiveId(event.active.id);
165
+ },
166
+ [sortableProps.onDragStart],
167
+ );
168
+
169
+ const onDragEnd = React.useCallback(
170
+ (event: DragEndEvent) => {
171
+ sortableProps.onDragEnd?.(event);
172
+
173
+ if (event.activatorEvent.defaultPrevented) return;
174
+
175
+ const { active, over } = event;
176
+ if (over && active.id !== over?.id) {
177
+ const activeIndex = value.findIndex(
178
+ (item) => getItemValue(item) === active.id,
179
+ );
180
+ const overIndex = value.findIndex(
181
+ (item) => getItemValue(item) === over.id,
182
+ );
183
+
184
+ if (onMove) {
185
+ onMove({ ...event, activeIndex, overIndex });
186
+ } else {
187
+ onValueChange?.(arrayMove(value, activeIndex, overIndex));
188
+ }
189
+ }
190
+ setActiveId(null);
191
+ },
192
+ [value, onValueChange, onMove, getItemValue, sortableProps.onDragEnd],
193
+ );
194
+
195
+ const onDragCancel = React.useCallback(
196
+ (event: DragEndEvent) => {
197
+ sortableProps.onDragCancel?.(event);
198
+
199
+ if (event.activatorEvent.defaultPrevented) return;
200
+
201
+ setActiveId(null);
202
+ },
203
+ [sortableProps.onDragCancel],
204
+ );
205
+ /* eslint-enable react-hooks/preserve-manual-memoization, react-hooks/exhaustive-deps */
206
+
207
+ const announcements: Announcements = React.useMemo(
208
+ () => ({
209
+ onDragStart({ active }) {
210
+ const activeValue = active.id.toString();
211
+ return `Grabbed sortable item "${activeValue}". Current position is ${active.data.current?.sortable.index + 1} of ${value.length}. Use arrow keys to move, space to drop.`;
212
+ },
213
+ onDragOver({ active, over }) {
214
+ if (over) {
215
+ const overIndex = over.data.current?.sortable.index ?? 0;
216
+ const activeIndex = active.data.current?.sortable.index ?? 0;
217
+ const moveDirection = overIndex > activeIndex ? "down" : "up";
218
+ const activeValue = active.id.toString();
219
+ return `Sortable item "${activeValue}" moved ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;
220
+ }
221
+ return "Sortable item is no longer over a droppable area. Press escape to cancel.";
222
+ },
223
+ onDragEnd({ active, over }) {
224
+ const activeValue = active.id.toString();
225
+ if (over) {
226
+ const overIndex = over.data.current?.sortable.index ?? 0;
227
+ return `Sortable item "${activeValue}" dropped at position ${overIndex + 1} of ${value.length}.`;
228
+ }
229
+ return `Sortable item "${activeValue}" dropped. No changes were made.`;
230
+ },
231
+ onDragCancel({ active }) {
232
+ const activeIndex = active.data.current?.sortable.index ?? 0;
233
+ const activeValue = active.id.toString();
234
+ return `Sorting cancelled. Sortable item "${activeValue}" returned to position ${activeIndex + 1} of ${value.length}.`;
235
+ },
236
+ onDragMove({ active, over }) {
237
+ if (over) {
238
+ const overIndex = over.data.current?.sortable.index ?? 0;
239
+ const activeIndex = active.data.current?.sortable.index ?? 0;
240
+ const moveDirection = overIndex > activeIndex ? "down" : "up";
241
+ const activeValue = active.id.toString();
242
+ return `Sortable item "${activeValue}" is moving ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;
243
+ }
244
+ return "Sortable item is no longer over a droppable area. Press escape to cancel.";
245
+ },
246
+ }),
247
+ [value],
248
+ );
249
+
250
+ const screenReaderInstructions: ScreenReaderInstructions = React.useMemo(
251
+ () => ({
252
+ draggable: `
253
+ To pick up a sortable item, press space or enter.
254
+ While dragging, use the ${orientation === "vertical" ? "up and down" : orientation === "horizontal" ? "left and right" : "arrow"} keys to move the item.
255
+ Press space or enter again to drop the item in its new position, or press escape to cancel.
256
+ `,
257
+ }),
258
+ [orientation],
259
+ );
260
+
261
+ const contextValue = React.useMemo(
262
+ () => ({
263
+ id,
264
+ items,
265
+ modifiers: modifiers ?? config.modifiers,
266
+ strategy: strategy ?? config.strategy,
267
+ activeId,
268
+ setActiveId,
269
+ getItemValue,
270
+ flatCursor,
271
+ }),
272
+ [
273
+ id,
274
+ items,
275
+ modifiers,
276
+ strategy,
277
+ config.modifiers,
278
+ config.strategy,
279
+ activeId,
280
+ getItemValue,
281
+ flatCursor,
282
+ ],
283
+ );
284
+
285
+ return (
286
+ <SortableRootContext.Provider
287
+ value={contextValue as SortableRootContextValue<unknown>}
288
+ >
289
+ <DndContext
290
+ collisionDetection={collisionDetection ?? config.collisionDetection}
291
+ modifiers={modifiers ?? config.modifiers}
292
+ sensors={sensors}
293
+ {...sortableProps}
294
+ id={id}
295
+ onDragStart={onDragStart}
296
+ onDragEnd={onDragEnd}
297
+ onDragCancel={onDragCancel}
298
+ accessibility={{
299
+ announcements,
300
+ screenReaderInstructions,
301
+ ...accessibility,
302
+ }}
303
+ />
304
+ </SortableRootContext.Provider>
305
+ );
306
+ }
307
+
308
+ const SortableContentContext = React.createContext<boolean>(false);
309
+
310
+ interface SortableContentProps extends React.ComponentProps<"div"> {
311
+ strategy?: SortableContextProps["strategy"];
312
+ children: React.ReactNode;
313
+ asChild?: boolean;
314
+ withoutSlot?: boolean;
315
+ }
316
+
317
+ function SortableContent(props: SortableContentProps) {
318
+ const {
319
+ strategy: strategyProp,
320
+ asChild,
321
+ withoutSlot,
322
+ children,
323
+ ref,
324
+ ...contentProps
325
+ } = props;
326
+
327
+ const context = useSortableContext(CONTENT_NAME);
328
+
329
+ const ContentPrimitive = asChild ? SlotPrimitive.Slot : "div";
330
+
331
+ return (
332
+ <SortableContentContext.Provider value={true}>
333
+ <SortableContext
334
+ items={context.items}
335
+ strategy={strategyProp ?? context.strategy}
336
+ >
337
+ {withoutSlot ? (
338
+ children
339
+ ) : (
340
+ <ContentPrimitive
341
+ data-slot="sortable-content"
342
+ {...contentProps}
343
+ ref={ref}
344
+ >
345
+ {children}
346
+ </ContentPrimitive>
347
+ )}
348
+ </SortableContext>
349
+ </SortableContentContext.Provider>
350
+ );
351
+ }
352
+
353
+ interface SortableItemContextValue {
354
+ id: string;
355
+ attributes: DraggableAttributes;
356
+ listeners: DraggableSyntheticListeners | undefined;
357
+ setActivatorNodeRef: (node: HTMLElement | null) => void;
358
+ isDragging?: boolean;
359
+ disabled?: boolean;
360
+ }
361
+
362
+ const SortableItemContext =
363
+ React.createContext<SortableItemContextValue | null>(null);
364
+
365
+ function useSortableItemContext(consumerName: string) {
366
+ const context = React.useContext(SortableItemContext);
367
+ if (!context) {
368
+ throw new Error(`\`${consumerName}\` must be used within \`${ITEM_NAME}\``);
369
+ }
370
+ return context;
371
+ }
372
+
373
+ interface SortableItemProps extends React.ComponentProps<"div"> {
374
+ value: UniqueIdentifier;
375
+ asHandle?: boolean;
376
+ asChild?: boolean;
377
+ disabled?: boolean;
378
+ }
379
+
380
+ function SortableItem(props: SortableItemProps) {
381
+ const {
382
+ value,
383
+ style,
384
+ asHandle,
385
+ asChild,
386
+ disabled,
387
+ className,
388
+ ref,
389
+ ...itemProps
390
+ } = props;
391
+
392
+ const inSortableContent = React.useContext(SortableContentContext);
393
+ const inSortableOverlay = React.useContext(SortableOverlayContext);
394
+
395
+ if (!inSortableContent && !inSortableOverlay) {
396
+ throw new Error(
397
+ `\`${ITEM_NAME}\` must be used within \`${CONTENT_NAME}\` or \`${OVERLAY_NAME}\``,
398
+ );
399
+ }
400
+
401
+ if (value === "") {
402
+ throw new Error(`\`${ITEM_NAME}\` value cannot be an empty string`);
403
+ }
404
+
405
+ const context = useSortableContext(ITEM_NAME);
406
+ const id = React.useId();
407
+ const {
408
+ attributes,
409
+ listeners,
410
+ setNodeRef,
411
+ setActivatorNodeRef,
412
+ transform,
413
+ transition,
414
+ isDragging,
415
+ } = useSortable({ id: value, disabled });
416
+
417
+ const composedRef = useComposedRefs(ref, (node) => {
418
+ if (disabled) return;
419
+ setNodeRef(node);
420
+ if (asHandle) setActivatorNodeRef(node);
421
+ });
422
+
423
+ const composedStyle = React.useMemo<React.CSSProperties>(() => {
424
+ return {
425
+ transform: CSS.Translate.toString(transform),
426
+ transition,
427
+ ...style,
428
+ };
429
+ }, [transform, transition, style]);
430
+
431
+ const itemContext = React.useMemo<SortableItemContextValue>(
432
+ () => ({
433
+ id,
434
+ attributes,
435
+ listeners,
436
+ setActivatorNodeRef,
437
+ isDragging,
438
+ disabled,
439
+ }),
440
+ [id, attributes, listeners, setActivatorNodeRef, isDragging, disabled],
441
+ );
442
+
443
+ const ItemPrimitive = asChild ? SlotPrimitive.Slot : "div";
444
+
445
+ return (
446
+ <SortableItemContext.Provider value={itemContext}>
447
+ <ItemPrimitive
448
+ id={id}
449
+ data-disabled={disabled}
450
+ data-dragging={isDragging ? "" : undefined}
451
+ data-slot="sortable-item"
452
+ {...itemProps}
453
+ {...(asHandle && !disabled ? attributes : {})}
454
+ {...(asHandle && !disabled ? listeners : {})}
455
+ ref={composedRef}
456
+ style={composedStyle}
457
+ className={cn(
458
+ "focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1",
459
+ {
460
+ "touch-none select-none": asHandle,
461
+ "cursor-default": context.flatCursor,
462
+ "data-dragging:cursor-grabbing": !context.flatCursor,
463
+ "cursor-grab": !isDragging && asHandle && !context.flatCursor,
464
+ "opacity-50": isDragging,
465
+ "pointer-events-none opacity-50": disabled,
466
+ },
467
+ className,
468
+ )}
469
+ />
470
+ </SortableItemContext.Provider>
471
+ );
472
+ }
473
+
474
+ interface SortableItemHandleProps extends React.ComponentProps<"button"> {
475
+ asChild?: boolean;
476
+ }
477
+
478
+ function SortableItemHandle(props: SortableItemHandleProps) {
479
+ const { asChild, disabled, className, ref, ...itemHandleProps } = props;
480
+
481
+ const context = useSortableContext(ITEM_HANDLE_NAME);
482
+ const itemContext = useSortableItemContext(ITEM_HANDLE_NAME);
483
+
484
+ const isDisabled = disabled ?? itemContext.disabled;
485
+
486
+ const composedRef = useComposedRefs(ref, (node) => {
487
+ if (!isDisabled) return;
488
+ itemContext.setActivatorNodeRef(node);
489
+ });
490
+
491
+ const HandlePrimitive = asChild ? SlotPrimitive.Slot : "button";
492
+
493
+ return (
494
+ <HandlePrimitive
495
+ type="button"
496
+ aria-controls={itemContext.id}
497
+ data-disabled={isDisabled}
498
+ data-dragging={itemContext.isDragging ? "" : undefined}
499
+ data-slot="sortable-item-handle"
500
+ {...itemHandleProps}
501
+ {...(isDisabled ? {} : itemContext.attributes)}
502
+ {...(isDisabled ? {} : itemContext.listeners)}
503
+ ref={composedRef}
504
+ className={cn(
505
+ "select-none disabled:pointer-events-none disabled:opacity-50",
506
+ context.flatCursor
507
+ ? "cursor-default"
508
+ : "cursor-grab data-dragging:cursor-grabbing",
509
+ className,
510
+ )}
511
+ disabled={isDisabled}
512
+ />
513
+ );
514
+ }
515
+
516
+ const SortableOverlayContext = React.createContext(false);
517
+
518
+ const dropAnimation: DropAnimation = {
519
+ sideEffects: defaultDropAnimationSideEffects({
520
+ styles: {
521
+ active: {
522
+ opacity: "0.4",
523
+ },
524
+ },
525
+ }),
526
+ };
527
+
528
+ interface SortableOverlayProps
529
+ extends Omit<React.ComponentProps<typeof DragOverlay>, "children"> {
530
+ container?: Element | DocumentFragment | null;
531
+ children?:
532
+ | ((params: { value: UniqueIdentifier }) => React.ReactNode)
533
+ | React.ReactNode;
534
+ }
535
+
536
+ function SortableOverlay(props: SortableOverlayProps) {
537
+ const { container: containerProp, children, ...overlayProps } = props;
538
+
539
+ const context = useSortableContext(OVERLAY_NAME);
540
+
541
+ const [mounted, setMounted] = React.useState(false);
542
+
543
+ React.useLayoutEffect(() => setMounted(true), []);
544
+
545
+ const container =
546
+ containerProp ?? (mounted ? globalThis.document?.body : null);
547
+
548
+ if (!container) return null;
549
+
550
+ return ReactDOM.createPortal(
551
+ <DragOverlay
552
+ dropAnimation={dropAnimation}
553
+ modifiers={context.modifiers}
554
+ className={cn(!context.flatCursor && "cursor-grabbing")}
555
+ {...overlayProps}
556
+ >
557
+ <SortableOverlayContext.Provider value={true}>
558
+ {context.activeId
559
+ ? typeof children === "function"
560
+ ? children({ value: context.activeId })
561
+ : children
562
+ : null}
563
+ </SortableOverlayContext.Provider>
564
+ </DragOverlay>,
565
+ container,
566
+ );
567
+ }
568
+
569
+ export {
570
+ Sortable,
571
+ SortableContent,
572
+ SortableItem,
573
+ SortableItemHandle,
574
+ SortableOverlay,
575
+ //
576
+ type SortableProps,
577
+ };
package/src/index.ts CHANGED
@@ -24,3 +24,19 @@ export { cn } from "./lib";
24
24
 
25
25
  // Hooks
26
26
  export * from "./hooks";
27
+
28
+ // Components
29
+ export { AgentsTable } from "./components/agents/agents-table";
30
+ export { AgentFormDialog } from "./components/agents/agent-form-dialog";
31
+ export { AgentEditForm } from "./components/agents/agent-edit-form";
32
+ export { AgentTabs } from "./components/agents/agent-tabs";
33
+ export { AgentPromptEditor } from "./components/agents/agent-prompt-editor";
34
+ export { AgentObjectivesList } from "./components/agents/agent-objectives-list";
35
+ export { AgentToolsList } from "./components/agents/agent-tools-list";
36
+ export { ToolsTable } from "./components/tools/tools-table";
37
+ export { ToolFormDialog } from "./components/tools/tool-form-dialog";
38
+ export { ToolCredentialsForm } from "./components/tools/tool-credentials-form";
39
+ export { Sortable, SortableContent, SortableItem, SortableItemHandle, SortableOverlay } from "./components/ui/sortable";
40
+
41
+ // Page Compositions
42
+ export * from "./pages";
@@ -0,0 +1,44 @@
1
+ import * as React from "react";
2
+
3
+ type PossibleRef<T> = React.Ref<T> | undefined;
4
+
5
+ function setRef<T>(ref: PossibleRef<T>, value: T) {
6
+ if (typeof ref === "function") {
7
+ return ref(value);
8
+ }
9
+ if (ref !== null && ref !== undefined) {
10
+ ref.current = value;
11
+ }
12
+ }
13
+
14
+ function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
15
+ return (node) => {
16
+ let hasCleanup = false;
17
+ const cleanups = refs.map((ref) => {
18
+ const cleanup = setRef(ref, node);
19
+ if (!hasCleanup && typeof cleanup === "function") {
20
+ hasCleanup = true;
21
+ }
22
+ return cleanup;
23
+ });
24
+ if (hasCleanup) {
25
+ return () => {
26
+ for (let i = 0; i < cleanups.length; i++) {
27
+ const cleanup = cleanups[i];
28
+ if (typeof cleanup === "function") {
29
+ cleanup();
30
+ } else {
31
+ setRef(refs[i], null);
32
+ }
33
+ }
34
+ };
35
+ }
36
+ };
37
+ }
38
+
39
+ function useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
40
+ // eslint-disable-next-line react-hooks/exhaustive-deps
41
+ return React.useCallback(composeRefs(...refs), refs);
42
+ }
43
+
44
+ export { composeRefs, useComposedRefs };