@bubblydoo/uxp-toolkit-react 0.0.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/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # @bubblydoo/uxp-toolkit-react
2
+
3
+ React hooks for Photoshop UXP plugins. Generic, non–app-specific utilities built on `@bubblydoo/uxp-toolkit`.
4
+
5
+ ## Peer dependencies
6
+
7
+ - `react` (^18 or ^19)
8
+ - `@tanstack/react-query` (^5)
9
+ - `@bubblydoo/uxp-toolkit` (workspace)
10
+ - `zod` (^3 or ^4)
11
+
12
+ ## Exports
13
+
14
+ - **useEventListenerSkippable** – Subscribe to events with optional skip/filter so triggers can be queued or ignored
15
+ - **useApplicationInfoQuery** – React Query for Photoshop application info (e.g. panel list)
16
+ - **useIsPluginPanelVisible** – Whether the plugin panel is visible (optionally for a given `panelId`)
17
+ - **useOnDocumentEdited** – Run a callback when the given document is edited (select, delete, make, set, move, close, show, hide, etc.)
18
+ - **useActiveDocument** – Sync external store for the current active document
19
+ - **useOnDocumentLayersEdited** – Run a callback when layers change (delete, make, set, move, close)
20
+ - **useOnDocumentLayersSelection** – Run a callback when layer selection changes (select, deselect)
21
+ - **useOnEvent** – Run a callback for arbitrary Photoshop action events on a given document
22
+ - **useOpenDocuments** – Sync external store for the list of open documents
23
+
24
+ All document/event hooks that take a `document` only fire when that document is the active document and (where applicable) when the plugin panel is visible.
@@ -0,0 +1,54 @@
1
+ import * as _tanstack_react_query from '@tanstack/react-query';
2
+ import { Document } from 'photoshop/dom/Document';
3
+
4
+ declare function useEventListenerSkippable({ subscribe, trigger, skip, filter, }: {
5
+ subscribe: (boundTrigger: () => void) => (() => void);
6
+ trigger: () => void;
7
+ /** If true, the trigger will be queued until the skip is false */
8
+ skip: boolean;
9
+ /** If false, the trigger will be skipped altogether */
10
+ filter: boolean;
11
+ }): void;
12
+
13
+ declare function useApplicationInfoQuery(refetchInterval?: number): _tanstack_react_query.UseQueryResult<{
14
+ active: boolean;
15
+ autoShowHomeScreen: boolean;
16
+ available: number;
17
+ buildNumber: string;
18
+ documentArea: {
19
+ left: number;
20
+ top: number;
21
+ right: number;
22
+ bottom: number;
23
+ };
24
+ hostName: string;
25
+ hostVersion: {
26
+ versionMajor: number;
27
+ versionMinor: number;
28
+ versionFix: number;
29
+ };
30
+ localeInfo: {
31
+ decimalPoint: string;
32
+ };
33
+ osVersion: string;
34
+ panelList: {
35
+ ID: string;
36
+ name: string;
37
+ obscured: boolean;
38
+ visible: boolean;
39
+ }[];
40
+ }, Error>;
41
+ declare function useIsPluginPanelVisible(panelId: string): boolean | null;
42
+
43
+ declare function useOnDocumentEdited(document: Document, trigger: () => void): void;
44
+ declare function useActiveDocument(): Document | null;
45
+
46
+ declare function useOnDocumentLayersEdited(document: Document, trigger: () => void): void;
47
+
48
+ declare function useOnDocumentLayersSelection(document: Document, trigger: () => void): void;
49
+
50
+ declare function useOnEvent(document: Document, events: string[], trigger: () => void): void;
51
+
52
+ declare function useOpenDocuments(): Document[];
53
+
54
+ export { useActiveDocument, useApplicationInfoQuery, useEventListenerSkippable, useIsPluginPanelVisible, useOnDocumentEdited, useOnDocumentLayersEdited, useOnDocumentLayersSelection, useOnEvent, useOpenDocuments };
package/dist/index.js ADDED
@@ -0,0 +1,239 @@
1
+ // src/useEventListenerSkippable.ts
2
+ import { useEffect, useState } from "react";
3
+ function useEventListenerSkippable({
4
+ subscribe,
5
+ trigger,
6
+ skip,
7
+ filter
8
+ }) {
9
+ const [queuedWhileSkipped, setQueuedWhileSkipped] = useState(false);
10
+ useEffect(() => {
11
+ const unsubscribe = subscribe(() => {
12
+ if (filter) {
13
+ if (skip) {
14
+ setQueuedWhileSkipped(true);
15
+ } else {
16
+ trigger();
17
+ }
18
+ }
19
+ });
20
+ return unsubscribe;
21
+ }, [subscribe, trigger, skip, filter]);
22
+ useEffect(() => {
23
+ if (queuedWhileSkipped && !skip) {
24
+ trigger();
25
+ setQueuedWhileSkipped(false);
26
+ }
27
+ }, [queuedWhileSkipped, trigger, skip]);
28
+ }
29
+
30
+ // src/useIsPluginVisible.tsx
31
+ import { useQuery } from "@tanstack/react-query";
32
+ import { entrypoints } from "uxp";
33
+ import { photoshopGetApplicationInfo, uxpEntrypointsSchema } from "@bubblydoo/uxp-toolkit";
34
+ var pluginInfo = uxpEntrypointsSchema.parse(entrypoints)._pluginInfo;
35
+ function useApplicationInfoQuery(refetchInterval = 1e3) {
36
+ return useQuery({
37
+ queryKey: ["application-info"],
38
+ queryFn: async () => {
39
+ const appInfo = await photoshopGetApplicationInfo();
40
+ return appInfo;
41
+ },
42
+ refetchInterval
43
+ });
44
+ }
45
+ function useIsPluginPanelVisible(panelId) {
46
+ const appInfoQuery = useApplicationInfoQuery(1e3);
47
+ if (!appInfoQuery.data) return null;
48
+ const pluginPanel = appInfoQuery.data.panelList.find((panel) => {
49
+ const idParts = panel.ID.split("/");
50
+ return idParts.includes(pluginInfo.id) && idParts.includes(panelId);
51
+ });
52
+ if (!pluginPanel) return false;
53
+ return pluginPanel.visible;
54
+ }
55
+ function useIsAnyPluginPanelVisible() {
56
+ const appInfoQuery = useApplicationInfoQuery(1e3);
57
+ if (!appInfoQuery.data) return null;
58
+ const pluginPanel = appInfoQuery.data.panelList.find((panel) => {
59
+ const idParts = panel.ID.split("/");
60
+ return idParts.includes(pluginInfo.id);
61
+ });
62
+ if (!pluginPanel) return false;
63
+ return pluginPanel.visible;
64
+ }
65
+
66
+ // src/useOnDocumentEdited.tsx
67
+ import { action, app } from "photoshop";
68
+ import { useSyncExternalStore } from "react";
69
+ var EVENTS = [
70
+ "select",
71
+ "delete",
72
+ "make",
73
+ "set",
74
+ "move",
75
+ "close",
76
+ "show",
77
+ "hide",
78
+ "convertToProfile",
79
+ "selectNoLayers",
80
+ "historyStateChanged"
81
+ // this might have changed the document
82
+ ];
83
+ function useOnDocumentEdited(document, trigger) {
84
+ const isPluginPanelVisible = useIsAnyPluginPanelVisible() ?? true;
85
+ useEventListenerSkippable({
86
+ trigger,
87
+ subscribe: (boundTrigger) => {
88
+ action.addNotificationListener(EVENTS, boundTrigger);
89
+ return () => {
90
+ action.removeNotificationListener(EVENTS, boundTrigger);
91
+ };
92
+ },
93
+ skip: !isPluginPanelVisible,
94
+ filter: document === app.activeDocument
95
+ });
96
+ }
97
+ var DOCUMENT_CHANGE_EVENTS = [
98
+ "select",
99
+ "open",
100
+ "close",
101
+ "smartBrushWorkspace",
102
+ "layersFiltered"
103
+ ];
104
+ var activeDocumentExternalStore = {
105
+ subscribe: (fn) => {
106
+ action.addNotificationListener(DOCUMENT_CHANGE_EVENTS, fn);
107
+ return () => {
108
+ action.removeNotificationListener(DOCUMENT_CHANGE_EVENTS, fn);
109
+ };
110
+ },
111
+ getSnapshot: () => app.activeDocument
112
+ };
113
+ function useActiveDocument() {
114
+ return useSyncExternalStore(
115
+ activeDocumentExternalStore.subscribe,
116
+ activeDocumentExternalStore.getSnapshot
117
+ );
118
+ }
119
+
120
+ // src/useOnDocumentLayersEdited.tsx
121
+ import { action as action2, app as app2 } from "photoshop";
122
+ var EVENTS2 = [
123
+ // "select",
124
+ "delete",
125
+ "make",
126
+ "set",
127
+ "move",
128
+ "close",
129
+ // "show",
130
+ // "hide",
131
+ // "convertToProfile",
132
+ // "selectNoLayers",
133
+ "historyStateChanged"
134
+ // this might have changed the layers
135
+ ];
136
+ function useOnDocumentLayersEdited(document, trigger) {
137
+ const isPluginPanelVisible = useIsAnyPluginPanelVisible() ?? true;
138
+ useEventListenerSkippable({
139
+ trigger,
140
+ subscribe: (boundTrigger) => {
141
+ action2.addNotificationListener(EVENTS2, boundTrigger);
142
+ return () => {
143
+ action2.removeNotificationListener(EVENTS2, boundTrigger);
144
+ };
145
+ },
146
+ skip: !isPluginPanelVisible,
147
+ filter: document === app2.activeDocument
148
+ });
149
+ }
150
+
151
+ // src/useOnDocumentLayersSelection.tsx
152
+ import { action as action3, app as app3 } from "photoshop";
153
+ var EVENTS3 = [
154
+ "select",
155
+ "deselect",
156
+ // "delete",
157
+ // "make",
158
+ // "set",
159
+ // "move",
160
+ // "close",
161
+ // "show",
162
+ // "hide",
163
+ // "convertToProfile",
164
+ // "selectNoLayers",
165
+ "historyStateChanged"
166
+ // this might have changed the layers selection, e.g. when deleting an undo
167
+ ];
168
+ function useOnDocumentLayersSelection(document, trigger) {
169
+ const isPluginPanelVisible = useIsAnyPluginPanelVisible() ?? true;
170
+ useEventListenerSkippable({
171
+ trigger,
172
+ subscribe: (boundTrigger) => {
173
+ action3.addNotificationListener(EVENTS3, boundTrigger);
174
+ return () => {
175
+ action3.removeNotificationListener(EVENTS3, boundTrigger);
176
+ };
177
+ },
178
+ skip: !isPluginPanelVisible,
179
+ filter: document === app3.activeDocument
180
+ });
181
+ }
182
+
183
+ // src/useOnEvent.tsx
184
+ import { action as action4, app as app4 } from "photoshop";
185
+ function useOnEvent(document, events, trigger) {
186
+ const isPluginPanelVisible = useIsAnyPluginPanelVisible() ?? true;
187
+ useEventListenerSkippable({
188
+ trigger,
189
+ subscribe: (boundTrigger) => {
190
+ action4.addNotificationListener(events, boundTrigger);
191
+ return () => {
192
+ action4.removeNotificationListener(events, boundTrigger);
193
+ };
194
+ },
195
+ skip: !isPluginPanelVisible,
196
+ filter: document === app4.activeDocument
197
+ });
198
+ }
199
+
200
+ // src/useOpenDocuments.tsx
201
+ import { action as action5, app as app5 } from "photoshop";
202
+ import { useSyncExternalStore as useSyncExternalStore2 } from "react";
203
+ var OPEN_DOCUMENTS_EVENTS = ["open", "close"];
204
+ var cachedDocuments = null;
205
+ var cachedDocumentsSnapshot = null;
206
+ var openDocumentsExternalStore = {
207
+ subscribe: (fn) => {
208
+ action5.addNotificationListener(OPEN_DOCUMENTS_EVENTS, fn);
209
+ return () => {
210
+ action5.removeNotificationListener(OPEN_DOCUMENTS_EVENTS, fn);
211
+ };
212
+ },
213
+ getSnapshot: () => {
214
+ const currentDocuments = Array.from(app5.documents);
215
+ const currentSnapshot = currentDocuments.map((doc) => doc.id || doc.name).join(",");
216
+ if (currentSnapshot !== cachedDocumentsSnapshot) {
217
+ cachedDocuments = currentDocuments;
218
+ cachedDocumentsSnapshot = currentSnapshot;
219
+ }
220
+ return cachedDocuments || [];
221
+ }
222
+ };
223
+ function useOpenDocuments() {
224
+ return useSyncExternalStore2(
225
+ openDocumentsExternalStore.subscribe,
226
+ openDocumentsExternalStore.getSnapshot
227
+ );
228
+ }
229
+ export {
230
+ useActiveDocument,
231
+ useApplicationInfoQuery,
232
+ useEventListenerSkippable,
233
+ useIsPluginPanelVisible,
234
+ useOnDocumentEdited,
235
+ useOnDocumentLayersEdited,
236
+ useOnDocumentLayersSelection,
237
+ useOnEvent,
238
+ useOpenDocuments
239
+ };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@bubblydoo/uxp-toolkit-react",
3
+ "version": "0.0.2",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ },
11
+ "./package.json": "./package.json"
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "peerDependencies": {
20
+ "@tanstack/react-query": "^5.0.0",
21
+ "react": "^18.0.0 || ^19.0.0",
22
+ "zod": "^3.0.0 || ^4.0.0",
23
+ "@bubblydoo/uxp-toolkit": "0.0.2"
24
+ },
25
+ "devDependencies": {
26
+ "@adobe-uxp-types/uxp": "^0.0.9",
27
+ "@tanstack/react-query": "^5.0.0",
28
+ "@types/photoshop": "^25.0.2",
29
+ "@types/react": "^19.0.0",
30
+ "@types/node": "^20.8.7",
31
+ "react": "^19.0.0",
32
+ "typescript": "^5.8.3",
33
+ "tsup": "^8.5.1",
34
+ "zod": "^4.3.6",
35
+ "@bubblydoo/tsconfig": "0.0.1",
36
+ "@bubblydoo/uxp-toolkit": "0.0.2"
37
+ },
38
+ "scripts": {
39
+ "build": "tsup"
40
+ }
41
+ }