@simmalugnt-se/payload-editor-assistant 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.2
4
+
5
+ - One control opens and closes the assistant: the text button with a sparkles icon in Admin's
6
+ header. The narrow rail shown while the panel was collapsed is gone.
7
+ - The panel starts closed, and the browser remembers whether you left it open.
8
+
3
9
  ## 0.3.1
4
10
 
5
11
  - A greeting or a question no longer fills empty fields: the assistant fills them only when asked.
@@ -1 +1,2 @@
1
+ /** The one control that opens and closes the assistant panel, in Admin's header. */
1
2
  export declare function EditorAssistantAction(): import("react").JSX.Element | null;
@@ -1,17 +1,23 @@
1
1
  "use client";
2
- import { jsx as _jsx } from "react/jsx-runtime";
3
- import { Button, useAuth } from "@payloadcms/ui";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useAuth, useTranslation } from "@payloadcms/ui";
4
4
  import { useAssistantState } from "./assistant-context.js";
5
+ import { COPY, uiLanguage } from "./copy.js";
6
+ /** The one control that opens and closes the assistant panel, in Admin's header. */
5
7
  export function EditorAssistantAction() {
6
8
  const { user } = useAuth();
7
- const { collapsed, setCollapsed, setOpen } = useAssistantState();
9
+ const { i18n } = useTranslation();
10
+ const { open, setOpen } = useAssistantState();
8
11
  if (!user) {
9
12
  return null;
10
13
  }
11
- return (_jsx(Button, { buttonStyle: "pill", className: "ea-trigger", onClick: (event) => {
14
+ return (_jsxs("button", { "aria-expanded": open, className: open ? "ea-trigger ea-trigger--open" : "ea-trigger", onClick: (event) => {
12
15
  event.preventDefault();
13
16
  event.stopPropagation();
14
- setOpen(true);
15
- setCollapsed(false);
16
- }, size: "medium", type: "button", children: collapsed ? "Open assistant" : "Assistant" }));
17
+ setOpen(!open);
18
+ }, type: "button", children: [_jsx(SparklesIcon, {}), COPY[uiLanguage(i18n.language)].assistant] }));
19
+ }
20
+ /** A large four-point star with two small ones: the usual sign for AI help. */
21
+ function SparklesIcon() {
22
+ return (_jsxs("svg", { "aria-hidden": "true", className: "ea-trigger__icon", fill: "currentColor", viewBox: "0 0 20 20", children: [_jsx("path", { d: "M8 2.5l1.3 3.7a4 4 0 0 0 2.5 2.5L15.5 10l-3.7 1.3a4 4 0 0 0-2.5 2.5L8 17.5l-1.3-3.7a4 4 0 0 0-2.5-2.5L.5 10l3.7-1.3a4 4 0 0 0 2.5-2.5L8 2.5z" }), _jsx("path", { d: "M15.5 1l.6 1.6a1.5 1.5 0 0 0 .9.9l1.6.6-1.6.6a1.5 1.5 0 0 0-.9.9l-.6 1.6-.6-1.6a1.5 1.5 0 0 0-.9-.9L12.4 4.1l1.6-.6a1.5 1.5 0 0 0 .9-.9L15.5 1z" }), _jsx("path", { d: "M16 13.5l.4 1.1a1 1 0 0 0 .6.6l1.1.4-1.1.4a1 1 0 0 0-.6.6L16 17.7l-.4-1.1a1 1 0 0 0-.6-.6l-1.1-.4 1.1-.4a1 1 0 0 0 .6-.6l.4-1.1z" })] }));
17
23
  }
@@ -1 +1 @@
1
- export declare function EditorAssistantAside(): import("react").JSX.Element;
1
+ export declare function EditorAssistantAside(): import("react").JSX.Element | null;
@@ -9,7 +9,7 @@ import { useAssistantState, } from "./assistant-context.js";
9
9
  import { COPY, uiLanguage } from "./copy.js";
10
10
  import { KEEP_SELECTION_ATTR } from "./selection-dom.js";
11
11
  export function EditorAssistantAside() {
12
- const { collapsed, setCollapsed, status, messages, shortcuts, collections, bridge, formAdapter, pendingApproval, selection, setSelection, resetConversation, setStatus, setMessages, setPendingApproval, stop, setAbortController, } = useAssistantState();
12
+ const { open, setOpen, status, messages, shortcuts, collections, bridge, formAdapter, pendingApproval, selection, setSelection, resetConversation, setStatus, setMessages, setPendingApproval, stop, setAbortController, } = useAssistantState();
13
13
  const { config, getEntityConfig } = useConfig();
14
14
  const router = useRouter();
15
15
  const locale = useLocale();
@@ -334,10 +334,11 @@ export function EditorAssistantAside() {
334
334
  void send(draft);
335
335
  }
336
336
  };
337
- if (collapsed) {
338
- return (_jsx("aside", { className: "ea-aside ea-aside--collapsed", "aria-label": copy.assistant, children: _jsxs("button", { className: "ea-aside__rail", type: "button", onClick: () => setCollapsed(false), children: [_jsx(ChevronIcon, { direction: "left" }), _jsx("span", { children: copy.assistant })] }) }));
337
+ // Closed: nothing is shown. The component stays mounted, so an applied preview survives.
338
+ if (!open) {
339
+ return null;
339
340
  }
340
- return (_jsxs("aside", { className: "ea-aside", "aria-label": copy.assistant, [KEEP_SELECTION_ATTR]: "", children: [_jsxs("div", { className: "ea-aside__head", children: [_jsxs("div", { className: "ea-aside__title", children: [_jsx("h2", { children: copy.assistant }), bridge ? _jsx("p", { className: "ea-aside__context", children: contextLine(bridge, config) }) : null] }), (messages.length > 0 || pendingApproval) && !pendingApproval?.applied ? (_jsx(Button, { buttonStyle: "subtle", margin: false, onClick: () => resetConversation(), size: "medium", children: copy.newChat })) : null, _jsx(Button, { "aria-label": copy.collapseLabel, buttonStyle: "icon-label", className: "ea-aside__collapse", icon: _jsx(ChevronIcon, { direction: "right" }), margin: false, onClick: () => setCollapsed(true), tooltip: copy.collapse })] }), _jsxs("div", { className: "ea-aside__body", children: [_jsxs("div", { className: "ea-messages", ref: threadRef, onScroll: onThreadScroll, children: [messages.length === 0 ? (_jsx("div", { className: "ea-msg ea-msg--assistant", children: introText(view, bridge, collections, config, copy) })) : null, messages.map((message) => (_jsxs("div", { className: `ea-msg ea-msg--${message.role}`, children: [message.text, message.candidates?.length ? (_jsx("div", { className: "ea-actions", children: message.candidates.map((candidate) => (_jsx(Button, { buttonStyle: "pill", margin: false, onClick: () => {
341
+ return (_jsxs("aside", { className: "ea-aside", "aria-label": copy.assistant, [KEEP_SELECTION_ATTR]: "", children: [_jsxs("div", { className: "ea-aside__head", children: [_jsxs("div", { className: "ea-aside__title", children: [_jsx("h2", { children: copy.assistant }), bridge ? _jsx("p", { className: "ea-aside__context", children: contextLine(bridge, config) }) : null] }), (messages.length > 0 || pendingApproval) && !pendingApproval?.applied ? (_jsx(Button, { buttonStyle: "subtle", margin: false, onClick: () => resetConversation(), size: "medium", children: copy.newChat })) : null, _jsx(Button, { "aria-label": copy.collapseLabel, buttonStyle: "icon-label", className: "ea-aside__collapse", icon: _jsx(ChevronIcon, { direction: "right" }), margin: false, onClick: () => setOpen(false), tooltip: copy.collapse })] }), _jsxs("div", { className: "ea-aside__body", children: [_jsxs("div", { className: "ea-messages", ref: threadRef, onScroll: onThreadScroll, children: [messages.length === 0 ? (_jsx("div", { className: "ea-msg ea-msg--assistant", children: introText(view, bridge, collections, config, copy) })) : null, messages.map((message) => (_jsxs("div", { className: `ea-msg ea-msg--${message.role}`, children: [message.text, message.candidates?.length ? (_jsx("div", { className: "ea-actions", children: message.candidates.map((candidate) => (_jsx(Button, { buttonStyle: "pill", margin: false, onClick: () => {
341
342
  router.push(candidate.href);
342
343
  }, size: "small", children: candidate.label }, candidate.href))) })) : null] }, message.id))), status === "streaming" ? (_jsxs("div", { className: "ea-msg ea-msg--working", children: [_jsxs("span", { className: "ea-dots", "aria-hidden": "true", children: [_jsx("span", {}), _jsx("span", {}), _jsx("span", {})] }), _jsx("span", { children: copy.working })] })) : null] }), pendingApproval ? (_jsxs("div", { className: "ea-approval", children: [_jsx("h3", { children: pendingApproval.title }), pendingApproval.applied ? (_jsx("p", { className: "ea-approval__hint", children: autosave ? copy.previewHintAutosave : copy.previewHint })) : null, _jsx("ul", { className: "ea-approval__diff", children: pendingApproval.summary.split("\n").map((line, index) => line.startsWith(" ") ? (
343
344
  // biome-ignore lint/suspicious/noArrayIndexKey: lines are static for one approval
@@ -10,6 +10,6 @@ export function EditorAssistantProvider({ children, shortcuts = [], collections
10
10
  return (_jsx(AssistantStateProvider, { shortcuts: shortcuts, collections: collections, children: user ? _jsx(EditorAssistantShell, { children: children }) : children }));
11
11
  }
12
12
  function EditorAssistantShell({ children }) {
13
- const { collapsed } = useAssistantState();
14
- return (_jsxs("div", { className: collapsed ? "ea-root ea-root--collapsed" : "ea-root", children: [_jsx("div", { className: "ea-main", children: children }), _jsx(EditorAssistantAside, {}), _jsx(ChangedFieldsHighlight, {})] }));
13
+ const { open } = useAssistantState();
14
+ return (_jsxs("div", { className: open ? "ea-root" : "ea-root ea-root--closed", children: [_jsx("div", { className: "ea-main", children: children }), _jsx(EditorAssistantAside, {}), _jsx(ChangedFieldsHighlight, {})] }));
15
15
  }
@@ -46,8 +46,8 @@ export type AssistantMessage = {
46
46
  }>;
47
47
  };
48
48
  type AssistantContextValue = {
49
+ /** Whether the panel is shown. Closed at first; the browser remembers the choice. */
49
50
  open: boolean;
50
- collapsed: boolean;
51
51
  status: AssistantChromeStatus;
52
52
  messages: AssistantMessage[];
53
53
  shortcuts: AssistantShortcut[];
@@ -59,7 +59,6 @@ type AssistantContextValue = {
59
59
  /** What the editor last clicked or focused in the open form. */
60
60
  selection: DomSelection | null;
61
61
  setOpen: (open: boolean) => void;
62
- setCollapsed: (collapsed: boolean) => void;
63
62
  setBridge: (bridge: DocumentBridgeSnapshot | null) => void;
64
63
  setFormAdapter: (adapter: FormAdapter | null) => void;
65
64
  setPendingApproval: (pending: PendingApproval | null) => void;
@@ -1,10 +1,9 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { createContext, useCallback, useContext, useMemo, useState } from "react";
3
+ import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react";
4
4
  const AssistantContext = createContext(null);
5
5
  export function AssistantStateProvider({ children, shortcuts, collections, }) {
6
- const [open, setOpen] = useState(true);
7
- const [collapsed, setCollapsed] = useState(false);
6
+ const [open, setOpenState] = useState(false);
8
7
  const [status, setStatus] = useState("idle");
9
8
  const [messages, setMessages] = useState([]);
10
9
  const [bridge, setBridge] = useState(null);
@@ -12,6 +11,15 @@ export function AssistantStateProvider({ children, shortcuts, collections, }) {
12
11
  const [pendingApproval, setPendingApproval] = useState(null);
13
12
  const [abortController, setAbortController] = useState(null);
14
13
  const [selection, setSelection] = useState(null);
14
+ // Read after mount, so the server render (closed) and the first client render agree.
15
+ useEffect(() => {
16
+ if (readStoredOpen())
17
+ setOpenState(true);
18
+ }, []);
19
+ const setOpen = useCallback((next) => {
20
+ setOpenState(next);
21
+ storeOpen(next);
22
+ }, []);
15
23
  const stop = useCallback(() => {
16
24
  abortController?.abort();
17
25
  setAbortController(null);
@@ -26,7 +34,6 @@ export function AssistantStateProvider({ children, shortcuts, collections, }) {
26
34
  }, [abortController]);
27
35
  const value = useMemo(() => ({
28
36
  open,
29
- collapsed,
30
37
  status,
31
38
  messages,
32
39
  shortcuts,
@@ -36,7 +43,6 @@ export function AssistantStateProvider({ children, shortcuts, collections, }) {
36
43
  pendingApproval,
37
44
  selection,
38
45
  setOpen,
39
- setCollapsed,
40
46
  setBridge,
41
47
  setFormAdapter,
42
48
  setPendingApproval,
@@ -49,7 +55,6 @@ export function AssistantStateProvider({ children, shortcuts, collections, }) {
49
55
  setAbortController,
50
56
  }), [
51
57
  open,
52
- collapsed,
53
58
  status,
54
59
  messages,
55
60
  shortcuts,
@@ -58,6 +63,7 @@ export function AssistantStateProvider({ children, shortcuts, collections, }) {
58
63
  formAdapter,
59
64
  pendingApproval,
60
65
  selection,
66
+ setOpen,
61
67
  resetConversation,
62
68
  stop,
63
69
  abortController,
@@ -71,3 +77,21 @@ export function useAssistantState() {
71
77
  }
72
78
  return value;
73
79
  }
80
+ const OPEN_KEY = "payload-editor-assistant:open";
81
+ /** Storage can be unavailable (private windows, blocked site data); the panel then starts closed. */
82
+ function readStoredOpen() {
83
+ try {
84
+ return window.localStorage.getItem(OPEN_KEY) === "true";
85
+ }
86
+ catch {
87
+ return false;
88
+ }
89
+ }
90
+ function storeOpen(open) {
91
+ try {
92
+ window.localStorage.setItem(OPEN_KEY, String(open));
93
+ }
94
+ catch {
95
+ // Not remembered; nothing else depends on it.
96
+ }
97
+ }
@@ -8,10 +8,6 @@
8
8
  overflow: hidden;
9
9
  }
10
10
 
11
- .ea-root--collapsed {
12
- --ea-aside-width: 48px;
13
- }
14
-
15
11
  html:has(.ea-root),
16
12
  html:has(.ea-root) body {
17
13
  height: 100%;
@@ -54,34 +50,6 @@ html[data-theme="dark"] .ea-aside {
54
50
  --ea-accent-tint: rgba(96, 165, 250, 0.14);
55
51
  }
56
52
 
57
- .ea-aside--collapsed {
58
- border-inline-start-color: var(--theme-elevation-100);
59
- }
60
-
61
- .ea-aside__rail {
62
- display: flex;
63
- flex-direction: column;
64
- align-items: center;
65
- gap: calc(var(--base) * 0.5);
66
- width: 100%;
67
- height: 100%;
68
- padding: calc(var(--base) * 0.8) 0;
69
- border: 0;
70
- background: transparent;
71
- color: var(--theme-elevation-500);
72
- font: inherit;
73
- cursor: pointer;
74
- }
75
-
76
- .ea-aside__rail span {
77
- writing-mode: vertical-rl;
78
- }
79
-
80
- .ea-aside__rail:hover {
81
- color: var(--theme-text);
82
- background: var(--theme-elevation-50);
83
- }
84
-
85
53
  .ea-aside__head {
86
54
  display: flex;
87
55
  align-items: center;
@@ -386,8 +354,41 @@ html[data-theme="dark"] .ea-aside {
386
354
  gap: calc(var(--base) * 0.4);
387
355
  }
388
356
 
357
+ /* Header control: text with an icon, like Admin's other header links rather than a button. */
389
358
  .ea-trigger {
359
+ display: inline-flex;
360
+ align-items: center;
361
+ gap: 6px;
390
362
  margin-inline: 8px;
363
+ padding: 6px 4px;
364
+ border: 0;
365
+ border-radius: var(--style-radius-s, 3px);
366
+ background: none;
367
+ color: var(--theme-elevation-800);
368
+ font: inherit;
369
+ font-size: 14px;
370
+ line-height: 1;
371
+ cursor: pointer;
372
+ }
373
+
374
+ .ea-trigger:hover,
375
+ .ea-trigger--open {
376
+ color: var(--theme-text);
377
+ }
378
+
379
+ .ea-trigger:focus-visible {
380
+ outline: 2px solid var(--ea-accent, #2563eb);
381
+ outline-offset: 2px;
382
+ }
383
+
384
+ .ea-trigger__icon {
385
+ width: 16px;
386
+ height: 16px;
387
+ flex-shrink: 0;
388
+ }
389
+
390
+ .ea-trigger--open .ea-trigger__icon {
391
+ color: var(--ea-accent, #2563eb);
391
392
  }
392
393
 
393
394
  .ea-stop-icon {
@@ -397,3 +398,7 @@ html[data-theme="dark"] .ea-aside {
397
398
  border-radius: 2px;
398
399
  background: currentColor;
399
400
  }
401
+
402
+ html[data-theme="dark"] .ea-trigger--open .ea-trigger__icon {
403
+ color: #60a5fa;
404
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simmalugnt-se/payload-editor-assistant",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Context-aware editor assistant plugin for Payload Admin",
5
5
  "keywords": [
6
6
  "payload",