react-ai-chat-actions 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kirilinsky
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software.
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # react-ai-chat-actions
2
+
3
+ [![npm](https://img.shields.io/npm/v/react-ai-chat-actions)](https://www.npmjs.com/package/react-ai-chat-actions)
4
+ [![npm downloads](https://img.shields.io/npm/dm/react-ai-chat-actions)](https://www.npmjs.com/package/react-ai-chat-actions)
5
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/react-ai-chat-actions)](https://bundlephobia.com/package/react-ai-chat-actions)
6
+ [![license](https://img.shields.io/npm/l/react-ai-chat-actions)](./LICENSE)
7
+
8
+ <img src="https://i.ibb.co/fVNC9PSx/aichatlogo.png" alt="react-ai-chat-actions" width="400" />
9
+
10
+ Action bar for AI chat messages. Like, dislike, copy, regenerate, speak, pin — with themes, tooltips, and loading states out of the box.
11
+
12
+ **[Live demo →](https://react-ai-chat-actions.vercel.app/)**
13
+
14
+ ---
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install react-ai-chat-actions
19
+ ```
20
+
21
+ ---
22
+
23
+ <img src="https://i.ibb.co/Wvtvyvwc/image.png" alt="react-ai-chat-actions" width="400" />
24
+
25
+ ## Usage
26
+
27
+ ```tsx
28
+ import { ActionBar } from "react-ai-chat-actions";
29
+ import "react-ai-chat-actions/dist/style.css";
30
+
31
+ <ActionBar
32
+ messageId="msg-1"
33
+ visible={true}
34
+ actions={["like", "dislike", "divider", "copy", "regenerate"]}
35
+ onAction={(type, messageId) => console.log(type, messageId)}
36
+ />;
37
+ ```
38
+
39
+ ---
40
+
41
+ ## Props
42
+
43
+ | Prop | Type | Default | Description |
44
+ | ----------- | --------------------------- | ------- | ----------------------------------------- |
45
+ | `messageId` | `string` | — | Required. Passed back in `onAction` |
46
+ | `visible` | `boolean` | `true` | Show or hide the bar |
47
+ | `actions` | `ActionType[]` | — | Which buttons to render and in what order |
48
+ | `onAction` | `(type, messageId) => void` | — | Callback on any button click |
49
+ | `loading` | `ActionType[]` | `[]` | Buttons in loading state |
50
+ | `disabled` | `ActionType[]` | `[]` | Buttons in disabled state |
51
+
52
+ ### ActionType
53
+
54
+ ```ts
55
+ type ActionType =
56
+ | "like"
57
+ | "dislike"
58
+ | "copy"
59
+ | "regenerate"
60
+ | "speak"
61
+ | "options"
62
+ | "pin"
63
+ | "divider";
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Themes
69
+
70
+ Four built-in themes: `light`, `dark`, `neon`, `square`.
71
+
72
+ Apply via `data-theme` on any parent element:
73
+
74
+ ```tsx
75
+ <div data-theme="dark">
76
+ <ActionBar ... />
77
+ </div>
78
+ ```
79
+
80
+ ---
81
+
82
+ ## Roadmap
83
+
84
+ - [ ] `ChatMessageWrapper` — wrapper mode with built-in hover visibility
85
+ - [ ] Custom actions support
86
+ - [ ] More themes
87
+ - [ ] Animations
88
+
89
+ ---
90
+
91
+ ## License
92
+
93
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,3 @@
1
+ import './style.css';
2
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require(`lucide-react`),t=require(`react/jsx-runtime`),n=require(`react`);const r=({label:n,icon:r,disabled:i,loading:a,onClick:o,active:s})=>(0,t.jsx)(`button`,{className:`ca-btn`,"aria-pressed":s,disabled:i||a,onClick:o,"aria-label":n,children:a?(0,t.jsx)(e.Loader,{size:16,className:`ca-spinner`}):r}),i=(e,t)=>{if(e!==`like`&&e!==`dislike`)return t;let n=e===`like`?`dislike`:`like`;return console.log(t.filter(e=>e!==n),`activeActions.filter((a) => a !== filterOption);`),t.filter(e=>e!==n)},a=({messageId:e,onAction:t})=>{let[r,a]=(0,n.useState)([]),o=e=>r.includes(e);return console.log(r,`console.log(activeActions);`),{isActive:o,handleAction:n=>{if([`copy`,`regenerate`].includes(n)){t(e,n);return}if(o(n))a(e=>e.filter(e=>e!==n));else{let o=i(n,r);t(e,n),a([...o,n])}}}},o=({label:e,children:r,disabled:i=!1})=>{if(i)return r;let a=(0,n.useRef)(null),o=(0,n.useRef)(null),[s,c]=(0,n.useState)(`top`);return(0,t.jsxs)(`div`,{className:`ca-tooltip-wrapper ca-tooltip--${s}`,ref:a,onMouseEnter:()=>{!a.current||!o.current||setTimeout(()=>{if(!a.current||!o.current)return;let e=a.current.getBoundingClientRect(),t={top:e.top,bottom:window.innerHeight-e.bottom,left:e.left,right:window.innerWidth-e.right};c(t.top>=t.bottom?`top`:`bottom`)},0)},children:[r,(0,t.jsx)(`span`,{ref:o,className:`ca-tooltip`,children:e})]})},s={like:{icon:(0,t.jsx)(e.ThumbsUp,{size:16}),label:`Like`},dislike:{icon:(0,t.jsx)(e.ThumbsDown,{size:16}),label:`Dislike`},copy:{icon:(0,t.jsx)(e.Copy,{size:16}),label:`Copy`},regenerate:{icon:(0,t.jsx)(e.RefreshCw,{size:16}),label:`Regenerate`},speak:{icon:(0,t.jsx)(e.Volume2,{size:16}),label:`Speak`},options:{icon:(0,t.jsx)(e.MoreHorizontal,{size:16}),label:`Options`},pin:{icon:(0,t.jsx)(e.Pin,{size:16}),label:`pin`},divider:{icon:(0,t.jsx)(`div`,{className:`ca-divider`}),label:``}},c=({messageId:e,actions:n,onAction:i,visible:c,loading:l,disabled:u})=>{let{isActive:d,handleAction:f}=a({messageId:e,onAction:i});return c?(0,t.jsx)(`div`,{className:`ca-bar`,children:n.map(e=>{if(e===`divider`)return(0,t.jsx)(`div`,{className:`ca-divider`},e);let n=s[e],i=d(e);return(0,t.jsx)(o,{disabled:u?.includes(e),label:n.label,children:(0,t.jsx)(r,{...n,active:i,loading:l?.includes(e),disabled:u?.includes(e),onClick:()=>f(e)})},e)})}):null};exports.ActionBar=c;
3
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["Loader","ThumbsUp","ThumbsDown","Copy","RefreshCw","Volume2","MoreHorizontal","Pin"],"sources":["../src/components/action-button/action-button.tsx","../src/hooks/use-chat-actions.tsx","../src/components/tooltip/tooltip.tsx","../src/components/action-bar/action-bar.tsx"],"sourcesContent":["import { Loader } from \"lucide-react\";\nimport { ActionButtonProps } from \"../../types\";\n\nexport const ActionButton = ({\n label,\n icon,\n disabled,\n loading,\n onClick,\n active,\n}: ActionButtonProps) => {\n return (\n <button\n className=\"ca-btn\"\n aria-pressed={active}\n disabled={disabled || loading}\n onClick={onClick}\n aria-label={label}\n >\n {loading ? <Loader size={16} className=\"ca-spinner\" /> : icon}\n </button>\n );\n};\n\nexport default ActionButton;\n","import { useState } from \"react\";\nimport { ActionType } from \"src/types\";\n\nconst reconcile = (currentAction: ActionType, activeActions: ActionType[]) => {\n if (currentAction !== \"like\" && currentAction !== \"dislike\") {\n return activeActions;\n }\n const filterOption = currentAction === \"like\" ? \"dislike\" : \"like\";\n console.log(\n activeActions.filter((a) => a !== filterOption),\n \"activeActions.filter((a) => a !== filterOption);\",\n );\n\n return activeActions.filter((a) => a !== filterOption);\n};\n\nconst useChatActions = ({\n messageId,\n onAction,\n}: {\n messageId: string;\n onAction: (messageId: string, action: ActionType) => void;\n}) => {\n const [activeActions, setActiveActions] = useState<ActionType[]>([]);\n\n const isActive = (action: ActionType) => activeActions.includes(action);\n\n const handleAction = (action: ActionType) => {\n const noStateActions: ActionType[] = [\"copy\", \"regenerate\"];\n\n if (noStateActions.includes(action)) {\n onAction(messageId, action);\n return;\n }\n\n if (isActive(action)) {\n setActiveActions((prev) => prev.filter((a) => a !== action));\n } else {\n let reconciledActions = reconcile(action, activeActions);\n onAction(messageId, action);\n setActiveActions([...reconciledActions, action]);\n }\n };\n\n console.log(activeActions, \"console.log(activeActions);\");\n return { isActive, handleAction };\n};\n\nexport default useChatActions;\n","import { ReactNode, useRef, useState } from \"react\";\n\nexport const Tooltip = ({\n label,\n children,\n disabled = false,\n}: {\n label: string;\n children: ReactNode;\n disabled?: boolean;\n}) => {\n if (disabled) return children;\n const wrapperRef = useRef<HTMLDivElement>(null);\n const tooltipRef = useRef<HTMLSpanElement>(null);\n\n const [side, setSide] = useState<\"top\" | \"bottom\">(\"top\");\n\n const handleMouseEnter = () => {\n if (!wrapperRef.current || !tooltipRef.current) return;\n\n setTimeout(() => {\n if (!wrapperRef.current || !tooltipRef.current) return;\n\n const rect = wrapperRef.current.getBoundingClientRect();\n\n const spaces = {\n top: rect.top,\n bottom: window.innerHeight - rect.bottom,\n left: rect.left,\n right: window.innerWidth - rect.right,\n };\n\n const side = spaces.top >= spaces.bottom ? \"top\" : \"bottom\";\n\n setSide(side);\n }, 0);\n };\n\n return (\n <div\n className={`ca-tooltip-wrapper ca-tooltip--${side}`}\n ref={wrapperRef}\n onMouseEnter={handleMouseEnter}\n >\n {children}\n <span ref={tooltipRef} className=\"ca-tooltip\">\n {label}\n </span>\n </div>\n );\n};\n","import {\n Copy,\n MoreHorizontal,\n Pin,\n RefreshCw,\n ThumbsDown,\n ThumbsUp,\n Volume2,\n} from \"lucide-react\";\nimport { ActionBarProps, ActionButtonMeta, ActionType } from \"../../types\";\nimport ActionButton from \"../action-button/action-button\";\nimport useChatActions from \"../../hooks/use-chat-actions\";\nimport { Tooltip } from \"../tooltip/tooltip\";\n\nconst buttonsMeta: Record<ActionType, ActionButtonMeta> = {\n like: { icon: <ThumbsUp size={16} />, label: \"Like\" },\n dislike: { icon: <ThumbsDown size={16} />, label: \"Dislike\" },\n copy: { icon: <Copy size={16} />, label: \"Copy\" },\n regenerate: { icon: <RefreshCw size={16} />, label: \"Regenerate\" },\n speak: { icon: <Volume2 size={16} />, label: \"Speak\" },\n options: { icon: <MoreHorizontal size={16} />, label: \"Options\" },\n pin: { icon: <Pin size={16} />, label: \"pin\" },\n divider: { icon: <div className=\"ca-divider\" />, label: \"\" },\n};\n\nexport const ActionBar = ({\n messageId,\n actions,\n onAction,\n visible,\n loading,\n disabled,\n}: ActionBarProps) => {\n const { isActive, handleAction } = useChatActions({ messageId, onAction });\n\n if (!visible) return null;\n return (\n <div className=\"ca-bar\">\n {actions.map((action) => {\n if (action === \"divider\") {\n return <div key={action} className=\"ca-divider\" />;\n }\n let meta = buttonsMeta[action];\n let active = isActive(action);\n\n return (\n <Tooltip\n disabled={disabled?.includes(action)}\n label={meta.label}\n key={action}\n >\n <ActionButton\n {...meta}\n active={active}\n loading={loading?.includes(action)}\n disabled={disabled?.includes(action)}\n onClick={() => handleAction(action)}\n />\n </Tooltip>\n );\n })}\n </div>\n );\n};\n\nexport default ActionBar;\n"],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
2
+ //#region src/theme.d.ts
3
+ type ThemeToken = '--ca-barBg' | '--ca-barShadow' | '--ca-barBorder' | '--ca-barRadius' | '--ca-btnColor' | '--ca-btnHoverBg' | '--ca-btnActiveBg' | '--ca-btnActiveColor' | '--ca-btnActiveGlow' | '--ca-btnRadius' | '--ca-dividerColor' | '--ca-tooltipBg' | '--ca-tooltipColor';
4
+ type ThemeName = 'square' | 'light' | 'dark' | 'neon';
5
+ //#endregion
6
+ //#region src/types/index.d.ts
7
+ type ActionType = "like" | "dislike" | "copy" | "regenerate" | "speak" | "options" | "pin" | "divider";
8
+ type ActionTypeFiltered = Exclude<ActionType, "divider">;
9
+ type ActionBarProps = {
10
+ messageId: string;
11
+ visible?: boolean;
12
+ actions: ActionType[];
13
+ loading?: ActionTypeFiltered[];
14
+ disabled?: ActionTypeFiltered[];
15
+ onAction: (messageId: string, action: ActionType) => void;
16
+ };
17
+ //#endregion
18
+ //#region src/components/action-bar/action-bar.d.ts
19
+ declare const ActionBar: ({
20
+ messageId,
21
+ actions,
22
+ onAction,
23
+ visible,
24
+ loading,
25
+ disabled
26
+ }: ActionBarProps) => react_jsx_runtime0.JSX.Element | null;
27
+ //#endregion
28
+ export { ActionBar, type ActionBarProps, type ActionType, type ThemeName, type ThemeToken };
29
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import './style.css';
2
+ import{Copy as e,Loader as t,MoreHorizontal as n,Pin as r,RefreshCw as i,ThumbsDown as a,ThumbsUp as o,Volume2 as s}from"lucide-react";import{jsx as c,jsxs as l}from"react/jsx-runtime";import{useRef as u,useState as d}from"react";const f=({label:e,icon:n,disabled:r,loading:i,onClick:a,active:o})=>c(`button`,{className:`ca-btn`,"aria-pressed":o,disabled:r||i,onClick:a,"aria-label":e,children:i?c(t,{size:16,className:`ca-spinner`}):n}),p=(e,t)=>{if(e!==`like`&&e!==`dislike`)return t;let n=e===`like`?`dislike`:`like`;return console.log(t.filter(e=>e!==n),`activeActions.filter((a) => a !== filterOption);`),t.filter(e=>e!==n)},m=({messageId:e,onAction:t})=>{let[n,r]=d([]),i=e=>n.includes(e);return console.log(n,`console.log(activeActions);`),{isActive:i,handleAction:a=>{if([`copy`,`regenerate`].includes(a)){t(e,a);return}if(i(a))r(e=>e.filter(e=>e!==a));else{let i=p(a,n);t(e,a),r([...i,a])}}}},h=({label:e,children:t,disabled:n=!1})=>{if(n)return t;let r=u(null),i=u(null),[a,o]=d(`top`);return l(`div`,{className:`ca-tooltip-wrapper ca-tooltip--${a}`,ref:r,onMouseEnter:()=>{!r.current||!i.current||setTimeout(()=>{if(!r.current||!i.current)return;let e=r.current.getBoundingClientRect(),t={top:e.top,bottom:window.innerHeight-e.bottom,left:e.left,right:window.innerWidth-e.right};o(t.top>=t.bottom?`top`:`bottom`)},0)},children:[t,c(`span`,{ref:i,className:`ca-tooltip`,children:e})]})},g={like:{icon:c(o,{size:16}),label:`Like`},dislike:{icon:c(a,{size:16}),label:`Dislike`},copy:{icon:c(e,{size:16}),label:`Copy`},regenerate:{icon:c(i,{size:16}),label:`Regenerate`},speak:{icon:c(s,{size:16}),label:`Speak`},options:{icon:c(n,{size:16}),label:`Options`},pin:{icon:c(r,{size:16}),label:`pin`},divider:{icon:c(`div`,{className:`ca-divider`}),label:``}},_=({messageId:e,actions:t,onAction:n,visible:r,loading:i,disabled:a})=>{let{isActive:o,handleAction:s}=m({messageId:e,onAction:n});return r?c(`div`,{className:`ca-bar`,children:t.map(e=>{if(e===`divider`)return c(`div`,{className:`ca-divider`},e);let t=g[e],n=o(e);return c(h,{disabled:a?.includes(e),label:t.label,children:c(f,{...t,active:n,loading:i?.includes(e),disabled:a?.includes(e),onClick:()=>s(e)})},e)})}):null};export{_ as ActionBar};
3
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/components/action-button/action-button.tsx","../src/hooks/use-chat-actions.tsx","../src/components/tooltip/tooltip.tsx","../src/components/action-bar/action-bar.tsx"],"sourcesContent":["import { Loader } from \"lucide-react\";\nimport { ActionButtonProps } from \"../../types\";\n\nexport const ActionButton = ({\n label,\n icon,\n disabled,\n loading,\n onClick,\n active,\n}: ActionButtonProps) => {\n return (\n <button\n className=\"ca-btn\"\n aria-pressed={active}\n disabled={disabled || loading}\n onClick={onClick}\n aria-label={label}\n >\n {loading ? <Loader size={16} className=\"ca-spinner\" /> : icon}\n </button>\n );\n};\n\nexport default ActionButton;\n","import { useState } from \"react\";\nimport { ActionType } from \"src/types\";\n\nconst reconcile = (currentAction: ActionType, activeActions: ActionType[]) => {\n if (currentAction !== \"like\" && currentAction !== \"dislike\") {\n return activeActions;\n }\n const filterOption = currentAction === \"like\" ? \"dislike\" : \"like\";\n console.log(\n activeActions.filter((a) => a !== filterOption),\n \"activeActions.filter((a) => a !== filterOption);\",\n );\n\n return activeActions.filter((a) => a !== filterOption);\n};\n\nconst useChatActions = ({\n messageId,\n onAction,\n}: {\n messageId: string;\n onAction: (messageId: string, action: ActionType) => void;\n}) => {\n const [activeActions, setActiveActions] = useState<ActionType[]>([]);\n\n const isActive = (action: ActionType) => activeActions.includes(action);\n\n const handleAction = (action: ActionType) => {\n const noStateActions: ActionType[] = [\"copy\", \"regenerate\"];\n\n if (noStateActions.includes(action)) {\n onAction(messageId, action);\n return;\n }\n\n if (isActive(action)) {\n setActiveActions((prev) => prev.filter((a) => a !== action));\n } else {\n let reconciledActions = reconcile(action, activeActions);\n onAction(messageId, action);\n setActiveActions([...reconciledActions, action]);\n }\n };\n\n console.log(activeActions, \"console.log(activeActions);\");\n return { isActive, handleAction };\n};\n\nexport default useChatActions;\n","import { ReactNode, useRef, useState } from \"react\";\n\nexport const Tooltip = ({\n label,\n children,\n disabled = false,\n}: {\n label: string;\n children: ReactNode;\n disabled?: boolean;\n}) => {\n if (disabled) return children;\n const wrapperRef = useRef<HTMLDivElement>(null);\n const tooltipRef = useRef<HTMLSpanElement>(null);\n\n const [side, setSide] = useState<\"top\" | \"bottom\">(\"top\");\n\n const handleMouseEnter = () => {\n if (!wrapperRef.current || !tooltipRef.current) return;\n\n setTimeout(() => {\n if (!wrapperRef.current || !tooltipRef.current) return;\n\n const rect = wrapperRef.current.getBoundingClientRect();\n\n const spaces = {\n top: rect.top,\n bottom: window.innerHeight - rect.bottom,\n left: rect.left,\n right: window.innerWidth - rect.right,\n };\n\n const side = spaces.top >= spaces.bottom ? \"top\" : \"bottom\";\n\n setSide(side);\n }, 0);\n };\n\n return (\n <div\n className={`ca-tooltip-wrapper ca-tooltip--${side}`}\n ref={wrapperRef}\n onMouseEnter={handleMouseEnter}\n >\n {children}\n <span ref={tooltipRef} className=\"ca-tooltip\">\n {label}\n </span>\n </div>\n );\n};\n","import {\n Copy,\n MoreHorizontal,\n Pin,\n RefreshCw,\n ThumbsDown,\n ThumbsUp,\n Volume2,\n} from \"lucide-react\";\nimport { ActionBarProps, ActionButtonMeta, ActionType } from \"../../types\";\nimport ActionButton from \"../action-button/action-button\";\nimport useChatActions from \"../../hooks/use-chat-actions\";\nimport { Tooltip } from \"../tooltip/tooltip\";\n\nconst buttonsMeta: Record<ActionType, ActionButtonMeta> = {\n like: { icon: <ThumbsUp size={16} />, label: \"Like\" },\n dislike: { icon: <ThumbsDown size={16} />, label: \"Dislike\" },\n copy: { icon: <Copy size={16} />, label: \"Copy\" },\n regenerate: { icon: <RefreshCw size={16} />, label: \"Regenerate\" },\n speak: { icon: <Volume2 size={16} />, label: \"Speak\" },\n options: { icon: <MoreHorizontal size={16} />, label: \"Options\" },\n pin: { icon: <Pin size={16} />, label: \"pin\" },\n divider: { icon: <div className=\"ca-divider\" />, label: \"\" },\n};\n\nexport const ActionBar = ({\n messageId,\n actions,\n onAction,\n visible,\n loading,\n disabled,\n}: ActionBarProps) => {\n const { isActive, handleAction } = useChatActions({ messageId, onAction });\n\n if (!visible) return null;\n return (\n <div className=\"ca-bar\">\n {actions.map((action) => {\n if (action === \"divider\") {\n return <div key={action} className=\"ca-divider\" />;\n }\n let meta = buttonsMeta[action];\n let active = isActive(action);\n\n return (\n <Tooltip\n disabled={disabled?.includes(action)}\n label={meta.label}\n key={action}\n >\n <ActionButton\n {...meta}\n active={active}\n loading={loading?.includes(action)}\n disabled={disabled?.includes(action)}\n onClick={() => handleAction(action)}\n />\n </Tooltip>\n );\n })}\n </div>\n );\n};\n\nexport default ActionBar;\n"],"mappings":"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"}
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ :root,[data-theme=square]{--ca-barBg:#fff;--ca-barShadow:0 2px 8px #0001;--ca-barBorder:none;--ca-barRadius:0px;--ca-btnColor:#374151;--ca-btnHoverBg:#f3f4f6;--ca-btnActiveBg:#0e7490;--ca-btnActiveColor:#fff;--ca-btnActiveGlow:0 0 4px #0e749044;--ca-btnRadius:0px;--ca-dividerColor:#e5e7eb;--ca-tooltipBg:#1f2937;--ca-tooltipColor:#fff}[data-theme=light]{--ca-barBg:#fff;--ca-barShadow:0 2px 8px #0001;--ca-barBorder:none;--ca-barRadius:999px;--ca-btnColor:#374151;--ca-btnHoverBg:#f3f4f6;--ca-btnActiveBg:#0e7490;--ca-btnActiveColor:#fff;--ca-btnActiveGlow:0 0 4px #0e749044;--ca-btnRadius:999px;--ca-dividerColor:#e5e7eb;--ca-tooltipBg:#1f2937;--ca-tooltipColor:#fff}[data-theme=dark]{--ca-barBg:#1e1e1e;--ca-barShadow:0 2px 8px #0004;--ca-barBorder:none;--ca-barRadius:999px;--ca-btnColor:#d1d5db;--ca-btnHoverBg:#ffffff14;--ca-btnActiveBg:#0e7490;--ca-btnActiveColor:#fff;--ca-btnActiveGlow:0 0 4px #0e749066;--ca-btnRadius:999px;--ca-dividerColor:#ffffff1a;--ca-tooltipBg:#374151;--ca-tooltipColor:#f9fafb}[data-theme=neon]{--ca-barBg:#0d0d0d;--ca-barShadow:0 2px 12px #0006;--ca-barBorder:1px solid #4fffcc66;--ca-barRadius:999px;--ca-btnColor:#aaa;--ca-btnHoverBg:#ffffff0f;--ca-btnActiveBg:#4fffcc;--ca-btnActiveColor:#0d0d0d;--ca-btnActiveGlow:0 0 12px #4fffcc, 0 0 24px #4fffcc88;--ca-btnRadius:999px;--ca-dividerColor:#ffffff1a;--ca-tooltipBg:#0d2e28;--ca-tooltipColor:#4fffcc}.ca-bar{background:var(--ca-barBg);box-shadow:var(--ca-barShadow);border-radius:var(--ca-barRadius);border:var(--ca-barBorder);align-items:center;gap:2px;padding:4px;display:inline-flex}.ca-divider{background:var(--ca-dividerColor);width:1px;height:16px;margin:0 4px}.ca-btn{width:32px;height:32px;color:var(--ca-btnColor);border-radius:var(--ca-btnRadius);cursor:pointer;background:0 0;border:none;justify-content:center;align-items:center;padding:0;transition:background .15s,color .15s,box-shadow .15s;display:flex}.ca-btn:hover:not(:disabled){background:var(--ca-btnHoverBg)}.ca-btn[aria-pressed=true],.ca-btn[aria-pressed=true]:hover{background:var(--ca-btnActiveBg);color:var(--ca-btnActiveColor);box-shadow:var(--ca-btnActiveGlow)}.ca-btn:active{opacity:.8;transform:scale(.9)}.ca-btn:disabled{opacity:.4;cursor:not-allowed;pointer-events:none}.ca-spinner{animation:.8s linear infinite ca-spin}.ca-tooltip-wrapper{display:inline-flex;position:relative}.ca-tooltip{background:var(--ca-tooltipBg);color:var(--ca-tooltipColor);white-space:nowrap;pointer-events:none;opacity:0;border-radius:4px;padding:4px 8px;font-size:11px;transition:opacity .15s;position:absolute}.ca-tooltip--top .ca-tooltip{bottom:calc(100% + 6px);left:50%;transform:translate(-50%)}.ca-tooltip--bottom .ca-tooltip{top:calc(100% + 6px);left:50%;transform:translate(-50%)}.ca-tooltip-wrapper:hover .ca-tooltip{opacity:1}@keyframes ca-spin{to{transform:rotate(360deg)}}
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "react-ai-chat-actions",
3
+ "version": "0.1.0",
4
+ "description": "React actions bar for AI chat messages",
5
+ "types": "./dist/index.d.ts",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.mjs",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.cjs"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsdown",
20
+ "dev": "vite dev",
21
+ "storybook": "storybook dev -p 6006",
22
+ "build-storybook": "npx storybook build"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/kirilinsky/react-ai-chat-actions.git"
27
+ },
28
+ "keywords": [
29
+ "react",
30
+ "next",
31
+ "ai",
32
+ "chat",
33
+ "actions",
34
+ "like",
35
+ "dislike",
36
+ "copy",
37
+ "message",
38
+ "toolbar",
39
+ "reactions"
40
+ ],
41
+ "license": "MIT",
42
+ "type": "module",
43
+ "bugs": {
44
+ "url": "https://github.com/kirilinsky/react-ai-chat-actions/issues"
45
+ },
46
+ "author": "Kirilinsky (https://github.com/kirilinsky)",
47
+ "homepage": "https://github.com/kirilinsky/react-ai-chat-actions#readme",
48
+ "peerDependencies": {
49
+ "react": ">=18",
50
+ "react-dom": ">=18",
51
+ "var-th": ">=0"
52
+ },
53
+ "devDependencies": {
54
+ "@tsdown/css": "^0.21.4",
55
+ "@types/node": "^25.5.0",
56
+ "@types/react": "^19.2.14",
57
+ "@types/react-dom": "^19.2.3",
58
+ "react": "^19.2.4",
59
+ "react-dom": "^19.2.4",
60
+ "tsdown": "^0.21.3",
61
+ "typescript": "^5.9.3",
62
+ "storybook": "^10.3.1",
63
+ "@storybook/react-vite": "^10.3.1"
64
+ },
65
+ "dependencies": {
66
+ "lucide-react": "^0.577.0"
67
+ }
68
+ }