Skip to content

Commit 74449b0

Browse files
committed
refactor: convert toolbar package from TypeScript to vanilla JavaScript
Eliminate TypeScript compilation requirement and convert to vanilla JS following the CLI package pattern. Changes: - Convert src/index.ts → src/core/Toolbar.js (with JSDoc types) - Convert src/react.ts → src/react.js (with JSDoc types) - Create modular structure: src/core/Toolbar.js and src/core/VanillaRenderer.js - Update package.json to point to src/ instead of dist/ - Remove TypeScript configuration (tsconfig.json) - Remove build scripts and TypeScript dependencies - Fix Next.js example dev.sh to use npx Benefits: ✅ No build step required ✅ Direct source consumption ✅ Faster development (no compilation watch) ✅ Simpler package structure ✅ JSDoc provides IDE autocomplete/intellisense ✅ Same pattern as CLI package
1 parent cb03ad7 commit 74449b0

File tree

9 files changed

+444
-341
lines changed

9 files changed

+444
-341
lines changed

examples/next/toolbar-demo/example-app/dev.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
set -a
33
source .env
44
set +a
5-
next dev --port "$PORT"
5+
npx next dev --port "$PORT"

examples/next/toolbar-demo/example-app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@
1818
"@types/node": "^20",
1919
"@types/react": "^19",
2020
"@types/react-dom": "^19",
21-
"typescript": "^5"
21+
"typescript": "^5.8.3"
2222
}
2323
}

packages/toolbar/package.json

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,15 @@
33
"version": "0.0.1",
44
"description": "Framework-agnostic toolbar for headless WordPress applications",
55
"type": "module",
6-
"main": "./dist/index.js",
7-
"types": "./dist/index.d.ts",
6+
"main": "./src/index.js",
87
"exports": {
9-
".": {
10-
"types": "./dist/index.d.ts",
11-
"default": "./dist/index.js"
12-
},
13-
"./react": {
14-
"types": "./dist/react.d.ts",
15-
"default": "./dist/react.js"
16-
},
8+
".": "./src/index.js",
9+
"./react": "./src/react.js",
1710
"./styles": "./src/toolbar.css"
1811
},
1912
"files": [
20-
"dist",
21-
"src/toolbar.css"
13+
"src"
2214
],
23-
"scripts": {
24-
"build": "tsc",
25-
"dev": "tsc --watch",
26-
"clean": "rm -rf dist"
27-
},
2815
"keywords": [
2916
"wordpress",
3017
"headless",
@@ -44,9 +31,7 @@
4431
}
4532
},
4633
"devDependencies": {
47-
"@types/react": "^18.3.0",
48-
"react": "^18.3.1",
49-
"typescript": "^5.8.3"
34+
"react": "^18.3.1"
5035
},
5136
"engines": {
5237
"node": ">=18"
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
/**
2+
* Headless WordPress Toolbar - Core Class
3+
* @package @wpengine/hwp-toolbar
4+
*/
5+
6+
/**
7+
* @typedef {Object} WordPressUser
8+
* @property {number} id
9+
* @property {string} name
10+
* @property {string} [email]
11+
* @property {string} [avatar]
12+
*/
13+
14+
/**
15+
* @typedef {Object} WordPressPost
16+
* @property {number} id
17+
* @property {string} title
18+
* @property {string} type
19+
* @property {string} status
20+
* @property {string} slug
21+
*/
22+
23+
/**
24+
* @typedef {Object} WordPressSite
25+
* @property {string} url
26+
* @property {string} adminUrl
27+
*/
28+
29+
/**
30+
* @typedef {Object} ToolbarState
31+
* @property {WordPressUser|null} user
32+
* @property {WordPressPost|null} post
33+
* @property {WordPressSite|null} site
34+
* @property {boolean} preview
35+
* @property {boolean} isHeadless
36+
*/
37+
38+
/**
39+
* @typedef {Object} ToolbarBranding
40+
* @property {string} [logo]
41+
* @property {string} [title]
42+
* @property {string} [url]
43+
* @property {'left'|'center'|'right'} [position]
44+
*/
45+
46+
/**
47+
* @typedef {Object} ToolbarTheme
48+
* @property {Record<string, string>} [variables]
49+
* @property {string} [className]
50+
*/
51+
52+
/**
53+
* @typedef {Object} ToolbarConfig
54+
* @property {'top'|'bottom'} [position]
55+
* @property {ToolbarBranding} [branding]
56+
* @property {ToolbarTheme} [theme]
57+
* @property {(enabled: boolean) => void} [onPreviewChange]
58+
*/
59+
60+
/**
61+
* @typedef {() => void} NodeCallback
62+
*/
63+
64+
/**
65+
* @typedef {() => string} LabelFunction
66+
*/
67+
68+
/**
69+
* @typedef {'button'|'link'|'image'|'dropdown'|'divider'|'custom'} NodeType
70+
*/
71+
72+
/**
73+
* @typedef {'left'|'center'|'right'} NodePosition
74+
*/
75+
76+
/**
77+
* @typedef {(state: ToolbarState) => HTMLElement} CustomRenderFunction
78+
*/
79+
80+
/**
81+
* @typedef {Object} ToolbarNode
82+
* @property {string} id
83+
* @property {NodeType} [type]
84+
* @property {string|LabelFunction} [label]
85+
* @property {NodeCallback} [onClick]
86+
* @property {string} [href]
87+
* @property {string} [target]
88+
* @property {string} [src]
89+
* @property {string} [alt]
90+
* @property {ToolbarNode[]} [items]
91+
* @property {CustomRenderFunction} [render]
92+
* @property {string} [className]
93+
* @property {NodePosition} [position]
94+
*/
95+
96+
/**
97+
* Core Toolbar Class
98+
* Framework-agnostic state management for headless WordPress toolbar
99+
*/
100+
export class Toolbar {
101+
/**
102+
* @param {ToolbarConfig} [config={}]
103+
*/
104+
constructor(config = {}) {
105+
/** @type {Map<string, ToolbarNode>} */
106+
this.nodes = new Map();
107+
108+
/** @type {ToolbarState} */
109+
this.state = {
110+
user: null,
111+
post: null,
112+
site: null,
113+
preview: false,
114+
isHeadless: true
115+
};
116+
117+
/** @type {Set<(nodes: ToolbarNode[], state: ToolbarState) => void>} */
118+
this.listeners = new Set();
119+
120+
/** @type {ToolbarConfig} */
121+
this.config = {
122+
position: 'bottom',
123+
...config
124+
};
125+
126+
this.registerDefaultNodes();
127+
}
128+
129+
/**
130+
* Get current configuration
131+
* @returns {ToolbarConfig}
132+
*/
133+
getConfig() {
134+
return { ...this.config };
135+
}
136+
137+
/**
138+
* Update configuration
139+
* @param {Partial<ToolbarConfig>} updates
140+
* @returns {this}
141+
*/
142+
setConfig(updates) {
143+
this.config = { ...this.config, ...updates };
144+
this.notify();
145+
return this;
146+
}
147+
148+
/**
149+
* Register default WordPress-specific nodes
150+
* @private
151+
*/
152+
registerDefaultNodes() {
153+
// Edit Post
154+
this.register('edit-post', () => {
155+
const p = this.state.post;
156+
return p ? `Edit ${p.type}` : 'Edit';
157+
}, () => {
158+
const { post, site } = this.state;
159+
if (post && site) {
160+
window.open(`${site.adminUrl}/post.php?post=${post.id}&action=edit`, '_blank');
161+
}
162+
});
163+
164+
// WP Admin
165+
this.register('wp-admin', 'WP Admin', () => {
166+
if (this.state.site?.adminUrl) {
167+
window.open(this.state.site.adminUrl, '_blank');
168+
}
169+
});
170+
171+
// Preview Toggle
172+
this.register('preview', () => {
173+
return this.state.preview ? 'Exit Preview' : 'Preview';
174+
}, () => {
175+
this.setState({ preview: !this.state.preview });
176+
if (this.config.onPreviewChange) {
177+
this.config.onPreviewChange(this.state.preview);
178+
}
179+
});
180+
}
181+
182+
/**
183+
* Register a toolbar node
184+
* @param {string} id
185+
* @param {string|LabelFunction|Partial<ToolbarNode>} labelOrNode
186+
* @param {NodeCallback} [onClick]
187+
* @returns {this}
188+
*/
189+
register(id, labelOrNode, onClick) {
190+
if (typeof labelOrNode === 'object') {
191+
// register(id, nodeConfig)
192+
this.nodes.set(id, {
193+
id,
194+
type: 'button',
195+
...labelOrNode
196+
});
197+
} else if (typeof labelOrNode === 'function' && !onClick) {
198+
// register(id, onClick)
199+
this.nodes.set(id, { id, type: 'button', label: id, onClick: labelOrNode });
200+
} else {
201+
// register(id, label, onClick)
202+
this.nodes.set(id, {
203+
id,
204+
type: 'button',
205+
label: labelOrNode,
206+
onClick: onClick || (() => {})
207+
});
208+
}
209+
this.notify();
210+
return this;
211+
}
212+
213+
/**
214+
* Unregister a toolbar node
215+
* @param {string} id
216+
* @returns {this}
217+
*/
218+
unregister(id) {
219+
this.nodes.delete(id);
220+
this.notify();
221+
return this;
222+
}
223+
224+
/**
225+
* Clear all custom nodes and reset to defaults
226+
* @returns {this}
227+
*/
228+
clear() {
229+
this.nodes.clear();
230+
this.registerDefaultNodes();
231+
this.notify();
232+
return this;
233+
}
234+
235+
/**
236+
* Update state
237+
* @param {Partial<ToolbarState>} updates
238+
* @returns {this}
239+
*/
240+
setState(updates) {
241+
this.state = { ...this.state, ...updates };
242+
this.notify();
243+
return this;
244+
}
245+
246+
/**
247+
* Set WordPress-specific context (user, post, site)
248+
* @param {Object} context
249+
* @param {WordPressUser|null} [context.user]
250+
* @param {WordPressPost|null} [context.post]
251+
* @param {WordPressSite|null} [context.site]
252+
* @returns {this}
253+
*/
254+
setWordPressContext(context) {
255+
return this.setState(context);
256+
}
257+
258+
/**
259+
* Get current state
260+
* @returns {ToolbarState}
261+
*/
262+
getState() {
263+
return { ...this.state };
264+
}
265+
266+
/**
267+
* Get visible nodes based on current state
268+
* @returns {ToolbarNode[]}
269+
*/
270+
getVisibleNodes() {
271+
return Array.from(this.nodes.values()).filter(node => {
272+
if (node.id === 'edit-post') return this.state.post && this.state.user;
273+
if (node.id === 'wp-admin') return this.state.user;
274+
if (node.id === 'preview') return this.state.post || this.state.user;
275+
return true;
276+
}).map(node => ({
277+
...node,
278+
label: typeof node.label === 'function' ? node.label() : node.label
279+
}));
280+
}
281+
282+
/**
283+
* Subscribe to state changes
284+
* @param {(nodes: ToolbarNode[], state: ToolbarState) => void} callback
285+
* @returns {() => void} Unsubscribe function
286+
*/
287+
subscribe(callback) {
288+
this.listeners.add(callback);
289+
callback(this.getVisibleNodes(), this.getState());
290+
return () => this.listeners.delete(callback);
291+
}
292+
293+
/**
294+
* Notify all listeners of state changes
295+
* @private
296+
*/
297+
notify() {
298+
const nodes = this.getVisibleNodes();
299+
const state = this.getState();
300+
this.listeners.forEach(cb => {
301+
try {
302+
cb(nodes, state);
303+
} catch (error) {
304+
console.error('Toolbar error:', error);
305+
}
306+
});
307+
}
308+
309+
/**
310+
* Clean up toolbar instance
311+
*/
312+
destroy() {
313+
this.nodes.clear();
314+
this.listeners.clear();
315+
}
316+
}

0 commit comments

Comments
 (0)