@dust-tt/sparkle 0.2.448 → 0.2.449-rc-samuel

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 (56) hide show
  1. package/dist/cjs/index.js +1 -1
  2. package/dist/esm/components/AppCommandPalette.d.ts +11 -0
  3. package/dist/esm/components/AppCommandPalette.d.ts.map +1 -0
  4. package/dist/esm/components/AppCommandPalette.js +44 -0
  5. package/dist/esm/components/AppCommandPalette.js.map +1 -0
  6. package/dist/esm/components/CommandPalette.d.ts +3 -0
  7. package/dist/esm/components/CommandPalette.d.ts.map +1 -0
  8. package/dist/esm/components/CommandPalette.js +61 -0
  9. package/dist/esm/components/CommandPalette.js.map +1 -0
  10. package/dist/esm/components/Input.d.ts +1 -0
  11. package/dist/esm/components/Input.d.ts.map +1 -1
  12. package/dist/esm/components/Input.js +2 -2
  13. package/dist/esm/components/Input.js.map +1 -1
  14. package/dist/esm/components/Notification.d.ts +2 -2
  15. package/dist/esm/components/Notification.d.ts.map +1 -1
  16. package/dist/esm/components/Notification.js +25 -3
  17. package/dist/esm/components/Notification.js.map +1 -1
  18. package/dist/esm/components/Resizable.d.ts +1 -1
  19. package/dist/esm/components/SamsCommandPalette.d.ts +81 -0
  20. package/dist/esm/components/SamsCommandPalette.d.ts.map +1 -0
  21. package/dist/esm/components/SamsCommandPalette.js +56 -0
  22. package/dist/esm/components/SamsCommandPalette.js.map +1 -0
  23. package/dist/esm/components/index.d.ts +1 -0
  24. package/dist/esm/components/index.d.ts.map +1 -1
  25. package/dist/esm/components/index.js +1 -0
  26. package/dist/esm/components/index.js.map +1 -1
  27. package/dist/esm/components/markdown/List.d.ts.map +1 -1
  28. package/dist/esm/components/markdown/List.js +3 -7
  29. package/dist/esm/components/markdown/List.js.map +1 -1
  30. package/dist/esm/contexts/CommandPaletteContext.d.ts +28 -0
  31. package/dist/esm/contexts/CommandPaletteContext.d.ts.map +1 -0
  32. package/dist/esm/contexts/CommandPaletteContext.js +83 -0
  33. package/dist/esm/contexts/CommandPaletteContext.js.map +1 -0
  34. package/dist/esm/hooks/useCommandPalette.d.ts +27 -0
  35. package/dist/esm/hooks/useCommandPalette.d.ts.map +1 -0
  36. package/dist/esm/hooks/useCommandPalette.js +62 -0
  37. package/dist/esm/hooks/useCommandPalette.js.map +1 -0
  38. package/dist/esm/stories/Notification.stories.js +8 -1
  39. package/dist/esm/stories/Notification.stories.js.map +1 -1
  40. package/dist/esm/stories/SamsCommandPalette.stories.d.ts +25 -0
  41. package/dist/esm/stories/SamsCommandPalette.stories.d.ts.map +1 -0
  42. package/dist/esm/stories/SamsCommandPalette.stories.js +100 -0
  43. package/dist/esm/stories/SamsCommandPalette.stories.js.map +1 -0
  44. package/dist/sparkle.css +136 -0
  45. package/package.json +2 -1
  46. package/src/components/AppCommandPalette.tsx +58 -0
  47. package/src/components/CommandPalette.tsx +112 -0
  48. package/src/components/Input.tsx +8 -1
  49. package/src/components/Notification.tsx +38 -19
  50. package/src/components/SamsCommandPalette.tsx +166 -0
  51. package/src/components/index.ts +1 -0
  52. package/src/components/markdown/List.tsx +3 -7
  53. package/src/contexts/CommandPaletteContext.tsx +135 -0
  54. package/src/hooks/useCommandPalette.tsx +87 -0
  55. package/src/stories/Notification.stories.tsx +10 -0
  56. package/src/stories/SamsCommandPalette.stories.tsx +133 -0
@@ -0,0 +1,112 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useRef } from "react";
4
+
5
+ import { cn } from "@sparkle/lib/utils";
6
+
7
+ import { useCommandPalette } from "../hooks/useCommandPalette";
8
+ import {
9
+ SamsCommand,
10
+ SamsCommandDialog,
11
+ SamsCommandEmpty,
12
+ SamsCommandGroup,
13
+ SamsCommandInput,
14
+ SamsCommandItem,
15
+ SamsCommandList,
16
+ } from "./SamsCommandPalette";
17
+
18
+ export const CommandPalette: React.FC = () => {
19
+ const { commands, isOpen, setOpen, closeCommandPalette } =
20
+ useCommandPalette();
21
+ const inputRef = useRef<HTMLInputElement>(null);
22
+
23
+ // Focus the input when the palette opens
24
+ useEffect(() => {
25
+ if (isOpen) {
26
+ // Use a small timeout to ensure the dialog is fully rendered before focusing
27
+ const focusTimeout = setTimeout(() => {
28
+ inputRef.current?.focus();
29
+ }, 50);
30
+
31
+ return () => clearTimeout(focusTimeout);
32
+ }
33
+ }, [isOpen]);
34
+
35
+ // Group commands by category
36
+ const groupedCommands = commands.reduce<Record<string, typeof commands>>(
37
+ (acc, command) => {
38
+ const category = command.category || "General";
39
+ if (!acc[category]) {
40
+ acc[category] = [];
41
+ }
42
+ acc[category].push(command);
43
+ return acc;
44
+ },
45
+ {}
46
+ );
47
+
48
+ // Sort categories and commands
49
+ const sortedCategories = Object.keys(groupedCommands).sort();
50
+
51
+ // Sort commands by priority (if specified)
52
+ Object.keys(groupedCommands).forEach((category) => {
53
+ groupedCommands[category].sort((a, b) => {
54
+ const priorityA = a.priority ?? 0;
55
+ const priorityB = b.priority ?? 0;
56
+ return priorityA - priorityB; // Lower priority first
57
+ });
58
+ });
59
+
60
+ return (
61
+ <SamsCommandDialog open={isOpen} onOpenChange={setOpen}>
62
+ <SamsCommand>
63
+ <SamsCommandInput
64
+ ref={inputRef}
65
+ placeholder="Type a command or search..."
66
+ autoFocus
67
+ />
68
+ <SamsCommandList>
69
+ <SamsCommandEmpty>No results found.</SamsCommandEmpty>
70
+ {sortedCategories.map((category, categoryIndex) => (
71
+ <SamsCommandGroup key={category} heading={category}>
72
+ {groupedCommands[category].map((command, commandIndex) => (
73
+ <SamsCommandItem
74
+ key={command.id}
75
+ onSelect={() => {
76
+ command.action();
77
+ // Optionally close the palette after executing a command
78
+ closeCommandPalette();
79
+ }}
80
+ className={cn(
81
+ "s-relative s-flex s-items-center s-gap-2 s-rounded-md s-px-2 s-py-1.5 s-text-sm s-font-medium",
82
+ command.id.includes("warning")
83
+ ? "s-text-warning-500 data-[selected=true]:s-bg-warning-50 data-[selected=true]:s-text-warning-500"
84
+ : "s-text-foreground data-[selected=true]:s-bg-muted-background data-[selected=true]:s-text-foreground",
85
+
86
+ // if it's the last one, rounded-b-md"
87
+ categoryIndex === sortedCategories.length - 1 &&
88
+ commandIndex === groupedCommands[category].length - 1
89
+ ? "s-rounded-b-xl"
90
+ : ""
91
+ )}
92
+ >
93
+ {command.icon && (
94
+ <span className="s-mr-2 s-flex s-h-5 s-w-5 s-items-center s-justify-center">
95
+ {command.icon}
96
+ </span>
97
+ )}
98
+ <span>{command.label}</span>
99
+ {command.shortcut && (
100
+ <span className="s-ml-auto s-text-xs s-tracking-widest s-text-muted-foreground">
101
+ {command.shortcut}
102
+ </span>
103
+ )}
104
+ </SamsCommandItem>
105
+ ))}
106
+ </SamsCommandGroup>
107
+ ))}
108
+ </SamsCommandList>
109
+ </SamsCommand>
110
+ </SamsCommandDialog>
111
+ );
112
+ };
@@ -19,6 +19,7 @@ export interface InputProps
19
19
  isError?: boolean;
20
20
  className?: string;
21
21
  label?: string;
22
+ isInCommandPalette?: boolean;
22
23
  }
23
24
 
24
25
  const INPUT_STATES = ["error", "disabled", "default"];
@@ -91,6 +92,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
91
92
  label,
92
93
  isError,
93
94
  disabled,
95
+ isInCommandPalette,
94
96
  ...props
95
97
  },
96
98
  ref
@@ -102,7 +104,12 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
102
104
  ? "disabled"
103
105
  : "default";
104
106
  return (
105
- <div className="s-flex s-flex-col s-gap-1">
107
+ <div
108
+ className={cn(
109
+ "s-flex s-flex-col s-gap-1",
110
+ isInCommandPalette && "s-w-full"
111
+ )}
112
+ >
106
113
  {label && (
107
114
  <Label htmlFor={props.name} className="s-mb-1">
108
115
  {label}
@@ -1,8 +1,13 @@
1
+ import { cva } from "class-variance-authority";
1
2
  import React from "react";
2
3
  import { toast, Toaster } from "sonner";
3
4
 
4
- import { CheckCircleIcon, XCircleIcon } from "@sparkle/icons";
5
- import { cn } from "@sparkle/lib/utils";
5
+ import {
6
+ CheckCircleIcon,
7
+ InformationCircleIcon,
8
+ XCircleIcon,
9
+ } from "@sparkle/icons";
10
+ import { assertNever, cn } from "@sparkle/lib/utils";
6
11
 
7
12
  import { Icon } from "./Icon";
8
13
 
@@ -11,7 +16,7 @@ const NOTIFICATION_DELAY = 5000;
11
16
  export type NotificationType = {
12
17
  title?: string;
13
18
  description?: string;
14
- type: "success" | "error";
19
+ type: "success" | "error" | "info";
15
20
  };
16
21
 
17
22
  const NotificationsContext = React.createContext<(n: NotificationType) => void>(
@@ -21,11 +26,34 @@ export interface NotificationProps {
21
26
  className?: string;
22
27
  description?: string;
23
28
  title?: string;
24
- variant: "success" | "error";
29
+ variant: "success" | "error" | "info";
25
30
  onClick?: () => void;
26
31
  }
27
32
 
28
33
  function NotificationContent({ type, title, description }: NotificationType) {
34
+ const icon = (() => {
35
+ switch (type) {
36
+ case "success":
37
+ return CheckCircleIcon;
38
+ case "error":
39
+ return XCircleIcon;
40
+ case "info":
41
+ return InformationCircleIcon;
42
+ default:
43
+ assertNever(type);
44
+ }
45
+ })();
46
+
47
+ const variantClassName = cva("s-pt-0.5", {
48
+ variants: {
49
+ type: {
50
+ success: "s-text-success-600 dark:s-text-success-400-night",
51
+ error: "s-text-warning-500 dark:s-text-warning-500-night",
52
+ info: "s-text-info-600 dark:s-text-info-400-night",
53
+ },
54
+ },
55
+ });
56
+
29
57
  return (
30
58
  <div
31
59
  className={cn(
@@ -35,21 +63,12 @@ function NotificationContent({ type, title, description }: NotificationType) {
35
63
  "s-p-4 s-shadow-xl"
36
64
  )}
37
65
  >
38
- {type === "success" ? (
39
- <Icon
40
- size="lg"
41
- visual={CheckCircleIcon}
42
- className="s-pt-0.5 s-text-success-600 dark:s-text-success-400-night"
43
- aria-hidden="true"
44
- />
45
- ) : (
46
- <Icon
47
- size="lg"
48
- visual={XCircleIcon}
49
- className="s-pt-0.5 s-text-warning-500 dark:s-text-warning-500-night"
50
- aria-hidden="true"
51
- />
52
- )}
66
+ <Icon
67
+ size="lg"
68
+ visual={icon}
69
+ className={variantClassName({ type })}
70
+ aria-hidden="true"
71
+ />
53
72
  <div className="s-flex s-flex-col">
54
73
  <div
55
74
  className={cn(
@@ -0,0 +1,166 @@
1
+ "use client";
2
+
3
+ import { type DialogProps } from "@radix-ui/react-dialog";
4
+ import { Command as CommandPrimitive } from "cmdk";
5
+ import * as React from "react";
6
+
7
+ import { Dialog, DialogContent, Input } from "@sparkle/components";
8
+ import { menuStyleClasses } from "@sparkle/components/Dropdown";
9
+ import { cn } from "@sparkle/lib";
10
+
11
+ const Command = React.forwardRef<
12
+ React.ElementRef<typeof CommandPrimitive>,
13
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive>
14
+ >(({ className, ...props }, ref) => (
15
+ <CommandPrimitive
16
+ ref={ref}
17
+ className={cn(
18
+ "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
19
+ className
20
+ )}
21
+ {...props}
22
+ />
23
+ ));
24
+ Command.displayName = CommandPrimitive.displayName;
25
+
26
+ const CommandDialog = ({ children, ...props }: DialogProps) => {
27
+ return (
28
+ <Dialog {...props}>
29
+ <DialogContent className="s-overflow-hidden s-p-0">
30
+ <Command className="[&_[cmdk-group-heading]]:s-p-2 [&_[cmdk-group-heading]]:s-font-medium [&_[cmdk-group-heading]]:s-text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:s-pt-0 [&_[cmdk-group]]:s-px-2 [&_[cmdk-input-wrapper]_svg]:s-h-5 [&_[cmdk-input-wrapper]_svg]:s-w-5 [&_[cmdk-input]]:s-h-12 [&_[cmdk-item]]:s-px-2 [&_[cmdk-item]]:s-py-3 [&_[cmdk-item]_svg]:s-h-5 [&_[cmdk-item]_svg]:s-w-5">
31
+ {children}
32
+ </Command>
33
+ </DialogContent>
34
+ </Dialog>
35
+ );
36
+ };
37
+
38
+ const CommandInput = React.forwardRef<
39
+ React.ElementRef<typeof CommandPrimitive.Input>,
40
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
41
+ >(({ className, ...props }, ref) => (
42
+ <div
43
+ className="s-flex s-w-full s-items-center s-border-b s-p-3"
44
+ cmdk-input-wrapper=""
45
+ >
46
+ <CommandPrimitive.Input
47
+ asChild
48
+ ref={ref}
49
+ className={cn(className, "s-min-w-full")}
50
+ {...props}
51
+ >
52
+ <Input
53
+ isInCommandPalette
54
+ className="s-h-10 s-w-full s-min-w-full s-rounded-md s-bg-transparent s-py-3 s-text-sm s-outline-none placeholder:s-text-muted-foreground disabled:s-cursor-not-allowed disabled:s-opacity-50"
55
+ />
56
+ </CommandPrimitive.Input>
57
+ </div>
58
+ ));
59
+
60
+ CommandInput.displayName = CommandPrimitive.Input.displayName;
61
+
62
+ const CommandList = React.forwardRef<
63
+ React.ElementRef<typeof CommandPrimitive.List>,
64
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
65
+ >(({ className, ...props }, ref) => (
66
+ <CommandPrimitive.List
67
+ ref={ref}
68
+ className={cn(
69
+ "s-max-h-[300px] s-overflow-y-auto s-overflow-x-hidden",
70
+ className
71
+ )}
72
+ {...props}
73
+ />
74
+ ));
75
+
76
+ CommandList.displayName = CommandPrimitive.List.displayName;
77
+
78
+ const CommandEmpty = React.forwardRef<
79
+ React.ElementRef<typeof CommandPrimitive.Empty>,
80
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
81
+ >((props, ref) => (
82
+ <CommandPrimitive.Empty
83
+ ref={ref}
84
+ className="s-py-6 s-text-center s-text-sm"
85
+ {...props}
86
+ />
87
+ ));
88
+
89
+ CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
90
+
91
+ const CommandGroup = React.forwardRef<
92
+ React.ElementRef<typeof CommandPrimitive.Group>,
93
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
94
+ >(({ className, ...props }, ref) => (
95
+ <CommandPrimitive.Group
96
+ ref={ref}
97
+ className={cn(
98
+ "s-overflow-hidden s-p-1 s-text-foreground [&_[cmdk-group-heading]]:s-px-2 [&_[cmdk-group-heading]]:s-py-1.5 [&_[cmdk-group-heading]]:s-text-xs [&_[cmdk-group-heading]]:s-font-medium [&_[cmdk-group-heading]]:s-text-muted-foreground [&_[cmdk-group-items]]:s-pb-1",
99
+ menuStyleClasses,
100
+ className
101
+ )}
102
+ {...props}
103
+ />
104
+ ));
105
+
106
+ CommandGroup.displayName = CommandPrimitive.Group.displayName;
107
+
108
+ const CommandSeparator = React.forwardRef<
109
+ React.ElementRef<typeof CommandPrimitive.Separator>,
110
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
111
+ >(({ className, ...props }, ref) => (
112
+ <CommandPrimitive.Separator
113
+ ref={ref}
114
+ className={cn(
115
+ "-s-mx-1 s-h-px s-bg-border dark:s-bg-border-night",
116
+ className
117
+ )}
118
+ {...props}
119
+ />
120
+ ));
121
+ CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
122
+
123
+ const CommandItem = React.forwardRef<
124
+ React.ElementRef<typeof CommandPrimitive.Item>,
125
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
126
+ >(({ className, ...props }, ref) => (
127
+ <CommandPrimitive.Item
128
+ ref={ref}
129
+ className={cn(
130
+ "data-[selected=true]:s-bg-accent data-[selected=true]:s-text-accent-foreground [&_svg]:s-size-4 s-relative s-flex s-cursor-default s-select-none s-items-center s-gap-2 s-rounded-sm s-px-2 s-py-1.5 s-text-sm s-outline-none data-[disabled=true]:s-pointer-events-none data-[disabled=true]:s-opacity-50 [&_svg]:s-pointer-events-none [&_svg]:s-shrink-0",
131
+ // menuStyleClasses.item({ variant: "default" }),
132
+ className
133
+ )}
134
+ {...props}
135
+ />
136
+ ));
137
+
138
+ CommandItem.displayName = CommandPrimitive.Item.displayName;
139
+
140
+ const CommandShortcut = ({
141
+ className,
142
+ ...props
143
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
144
+ return (
145
+ <span
146
+ className={cn(
147
+ "s-ml-auto s-text-xs s-tracking-widest s-text-muted-foreground",
148
+ className
149
+ )}
150
+ {...props}
151
+ />
152
+ );
153
+ };
154
+ CommandShortcut.displayName = "CommandShortcut";
155
+
156
+ export {
157
+ Command as SamsCommand,
158
+ CommandDialog as SamsCommandDialog,
159
+ CommandEmpty as SamsCommandEmpty,
160
+ CommandGroup as SamsCommandGroup,
161
+ CommandInput as SamsCommandInput,
162
+ CommandItem as SamsCommandItem,
163
+ CommandList as SamsCommandList,
164
+ CommandSeparator as SamsCommandSeparator,
165
+ CommandShortcut as SamsCommandShortcut,
166
+ };
@@ -1,4 +1,5 @@
1
1
  export { AnimatedText } from "./AnimatedText";
2
+ export { AppCommandPalette } from "./AppCommandPalette";
2
3
  export { AspectRatio } from "./AspectRatio";
3
4
  export {
4
5
  AssistantCard,
@@ -3,9 +3,7 @@ import React from "react";
3
3
 
4
4
  import { cn } from "@sparkle/lib";
5
5
 
6
- export const ulBlockVariants = cva([
7
- "s-list-disc s-py-2 s-pl-8 first:s-pt-0 last:s-pb-0",
8
- ]);
6
+ export const ulBlockVariants = cva(["s-list-disc s-pb-2 s-pl-6"]);
9
7
 
10
8
  interface UlBlockProps {
11
9
  children: React.ReactNode;
@@ -19,9 +17,7 @@ export function UlBlock({ children, textColor, textSize }: UlBlockProps) {
19
17
  );
20
18
  }
21
19
 
22
- export const olBlockVariants = cva([
23
- "s-list-decimal s-py-3 s-pl-8 first:s-pt-0 last:s-pb-0",
24
- ]);
20
+ export const olBlockVariants = cva(["s-list-decimal s-pb-2 s-pl-6"]);
25
21
 
26
22
  interface OlBlockProps {
27
23
  children: React.ReactNode;
@@ -43,7 +39,7 @@ export function OlBlock({
43
39
  );
44
40
  }
45
41
 
46
- export const liBlockVariants = cva(["s-break-words first:s-pt-0 last:s-pb-0"]);
42
+ export const liBlockVariants = cva(["s-break-words"]);
47
43
 
48
44
  interface LiBlockProps {
49
45
  children: React.ReactNode;
@@ -0,0 +1,135 @@
1
+ "use client";
2
+
3
+ import React, {
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useState,
9
+ } from "react";
10
+
11
+ // Command type definition
12
+ export interface Command {
13
+ id: string;
14
+ label: string;
15
+ shortcut?: string;
16
+ icon?: React.ReactNode;
17
+ action: () => void;
18
+ category?: string;
19
+ priority?: number;
20
+ entityType?: string;
21
+ entityId?: string;
22
+ }
23
+
24
+ interface CommandPaletteContextType {
25
+ commands: Command[];
26
+ isOpen: boolean;
27
+ registerCommand: (command: Command) => void;
28
+ registerCommands: (commands: Command[]) => void;
29
+ unregisterCommand: (commandId: string) => void;
30
+ unregisterCommandsByEntityType: (entityType: string) => void;
31
+ unregisterCommandsByEntityId: (entityId: string) => void;
32
+ setOpen: (isOpen: boolean) => void;
33
+ }
34
+
35
+ const CommandPaletteContext = createContext<
36
+ CommandPaletteContextType | undefined
37
+ >(undefined);
38
+
39
+ export const CommandPaletteProvider: React.FC<{
40
+ children: React.ReactNode;
41
+ }> = ({ children }) => {
42
+ const [commands, setCommands] = useState<Command[]>([]);
43
+ const [isOpen, setIsOpen] = useState(false);
44
+
45
+ // Register a single command
46
+ const registerCommand = useCallback((command: Command) => {
47
+ setCommands((prevCommands) => {
48
+ // Check if command with same ID already exists
49
+ const index = prevCommands.findIndex((c) => c.id === command.id);
50
+ if (index >= 0) {
51
+ // Replace existing command
52
+ const newCommands = [...prevCommands];
53
+ newCommands[index] = command;
54
+ return newCommands;
55
+ }
56
+ // Add new command
57
+ return [...prevCommands, command];
58
+ });
59
+ }, []);
60
+
61
+ // Register multiple commands at once
62
+ const registerCommands = useCallback((newCommands: Command[]) => {
63
+ setCommands((prevCommands) => {
64
+ const commandMap = new Map(prevCommands.map((c) => [c.id, c]));
65
+
66
+ // Update or add new commands
67
+ newCommands.forEach((command) => {
68
+ commandMap.set(command.id, command);
69
+ });
70
+
71
+ return Array.from(commandMap.values());
72
+ });
73
+ }, []);
74
+
75
+ // Unregister a command by ID
76
+ const unregisterCommand = useCallback((commandId: string) => {
77
+ setCommands((prevCommands) =>
78
+ prevCommands.filter((c) => c.id !== commandId)
79
+ );
80
+ }, []);
81
+
82
+ // Unregister commands by entity type
83
+ const unregisterCommandsByEntityType = useCallback((entityType: string) => {
84
+ setCommands((prevCommands) =>
85
+ prevCommands.filter((c) => c.entityType !== entityType)
86
+ );
87
+ }, []);
88
+
89
+ // Unregister commands by entity ID
90
+ const unregisterCommandsByEntityId = useCallback((entityId: string) => {
91
+ setCommands((prevCommands) =>
92
+ prevCommands.filter((c) => c.entityId !== entityId)
93
+ );
94
+ }, []);
95
+
96
+ // Handle keyboard shortcut
97
+ useEffect(() => {
98
+ const handleKeyDown = (event: KeyboardEvent) => {
99
+ if ((event.metaKey || event.ctrlKey) && event.key === "k") {
100
+ event.preventDefault();
101
+ setIsOpen((prev) => !prev);
102
+ }
103
+ };
104
+
105
+ window.addEventListener("keydown", handleKeyDown);
106
+ return () => window.removeEventListener("keydown", handleKeyDown);
107
+ }, []);
108
+
109
+ const contextValue: CommandPaletteContextType = {
110
+ commands,
111
+ isOpen,
112
+ registerCommand,
113
+ registerCommands,
114
+ unregisterCommand,
115
+ unregisterCommandsByEntityType,
116
+ unregisterCommandsByEntityId,
117
+ setOpen: setIsOpen,
118
+ };
119
+
120
+ return (
121
+ <CommandPaletteContext.Provider value={contextValue}>
122
+ {children}
123
+ </CommandPaletteContext.Provider>
124
+ );
125
+ };
126
+
127
+ export const useCommandPaletteContext = (): CommandPaletteContextType => {
128
+ const context = useContext(CommandPaletteContext);
129
+ if (context === undefined) {
130
+ throw new Error(
131
+ "useCommandPaletteContext must be used within a CommandPaletteProvider"
132
+ );
133
+ }
134
+ return context;
135
+ };
@@ -0,0 +1,87 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+
5
+ import {
6
+ Command,
7
+ useCommandPaletteContext,
8
+ } from "../contexts/CommandPaletteContext";
9
+
10
+ interface UseCommandPaletteProps {
11
+ commands?: Command[];
12
+ entityType?: string;
13
+ entityId?: string;
14
+ }
15
+
16
+ /**
17
+ * Hook for interacting with the command palette
18
+ *
19
+ * Optionally register commands when the component mounts and
20
+ * automatically unregister them when the component unmounts.
21
+ */
22
+ export const useCommandPalette = (props?: UseCommandPaletteProps) => {
23
+ const {
24
+ commands: allCommands,
25
+ isOpen,
26
+ registerCommand,
27
+ registerCommands,
28
+ unregisterCommand,
29
+ unregisterCommandsByEntityType,
30
+ unregisterCommandsByEntityId,
31
+ setOpen,
32
+ } = useCommandPaletteContext();
33
+
34
+ // Register commands on mount, unregister on unmount
35
+ useEffect(() => {
36
+ if (props?.commands?.length) {
37
+ registerCommands(props.commands);
38
+ }
39
+
40
+ return () => {
41
+ // Clean up commands when component unmounts
42
+ if (props?.entityType) {
43
+ unregisterCommandsByEntityType(props.entityType);
44
+ }
45
+ if (props?.entityId) {
46
+ unregisterCommandsByEntityId(props.entityId);
47
+ }
48
+ if (props?.commands) {
49
+ props.commands.forEach((command) => {
50
+ unregisterCommand(command.id);
51
+ });
52
+ }
53
+ };
54
+ }, [
55
+ props?.commands,
56
+ props?.entityType,
57
+ props?.entityId,
58
+ registerCommands,
59
+ unregisterCommand,
60
+ unregisterCommandsByEntityType,
61
+ unregisterCommandsByEntityId,
62
+ ]);
63
+
64
+ // Utility functions
65
+ const openCommandPalette = () => setOpen(true);
66
+ const closeCommandPalette = () => setOpen(false);
67
+ const toggleCommandPalette = () => setOpen(!isOpen);
68
+
69
+ return {
70
+ // State
71
+ commands: allCommands,
72
+ isOpen,
73
+
74
+ // Register/unregister methods
75
+ registerCommand,
76
+ registerCommands,
77
+ unregisterCommand,
78
+ unregisterCommandsByEntityType,
79
+ unregisterCommandsByEntityId,
80
+
81
+ // Open/close controls
82
+ openCommandPalette,
83
+ closeCommandPalette,
84
+ toggleCommandPalette,
85
+ setOpen,
86
+ };
87
+ };
@@ -44,6 +44,16 @@ const NotificationExample = () => {
44
44
  }
45
45
  label="Show Error"
46
46
  />
47
+ <Button
48
+ onClick={() =>
49
+ sendNotification({
50
+ title: "Info",
51
+ description: "Some information",
52
+ type: "info",
53
+ })
54
+ }
55
+ label="Show Info"
56
+ />
47
57
  </div>
48
58
  );
49
59
  };