@design.estate/dees-wcctools 3.3.0 → 3.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@design.estate/dees-wcctools",
3
- "version": "3.3.0",
3
+ "version": "3.5.0",
4
4
  "private": false,
5
5
  "description": "A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.",
6
6
  "exports": {
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@design.estate/dees-wcctools',
6
- version: '3.3.0',
6
+ version: '3.5.0',
7
7
  description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.'
8
8
  }
@@ -0,0 +1,211 @@
1
+ import { DeesElement, property, html, customElement, type TemplateResult, state, css, cssManager } from '@design.estate/dees-element';
2
+
3
+ export interface IContextMenuItem {
4
+ name: string;
5
+ iconName?: string;
6
+ action: () => void | Promise<void>;
7
+ disabled?: boolean;
8
+ }
9
+
10
+ @customElement('wcc-contextmenu')
11
+ export class WccContextmenu extends DeesElement {
12
+ // Static method to show context menu at position
13
+ public static async show(
14
+ event: MouseEvent,
15
+ menuItems: IContextMenuItem[]
16
+ ): Promise<void> {
17
+ event.preventDefault();
18
+ event.stopPropagation();
19
+
20
+ // Remove any existing context menu
21
+ const existing = document.querySelector('wcc-contextmenu');
22
+ if (existing) {
23
+ existing.remove();
24
+ }
25
+
26
+ const menu = new WccContextmenu();
27
+ menu.menuItems = menuItems;
28
+ menu.x = event.clientX;
29
+ menu.y = event.clientY;
30
+
31
+ document.body.appendChild(menu);
32
+
33
+ // Wait for render then adjust position if needed
34
+ await menu.updateComplete;
35
+ menu.adjustPosition();
36
+ }
37
+
38
+ @property({ type: Array })
39
+ accessor menuItems: IContextMenuItem[] = [];
40
+
41
+ @property({ type: Number })
42
+ accessor x: number = 0;
43
+
44
+ @property({ type: Number })
45
+ accessor y: number = 0;
46
+
47
+ @state()
48
+ accessor visible: boolean = false;
49
+
50
+ private boundHandleOutsideClick = this.handleOutsideClick.bind(this);
51
+ private boundHandleKeydown = this.handleKeydown.bind(this);
52
+
53
+ public static styles = [
54
+ css`
55
+ :host {
56
+ position: fixed;
57
+ z-index: 10000;
58
+ opacity: 0;
59
+ transform: scale(0.95) translateY(-5px);
60
+ transition: opacity 0.15s ease, transform 0.15s ease;
61
+ pointer-events: none;
62
+ }
63
+
64
+ :host(.visible) {
65
+ opacity: 1;
66
+ transform: scale(1) translateY(0);
67
+ pointer-events: auto;
68
+ }
69
+
70
+ .menu {
71
+ min-width: 160px;
72
+ background: #0f0f0f;
73
+ border: 1px solid rgba(255, 255, 255, 0.1);
74
+ border-radius: 6px;
75
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
76
+ padding: 4px 0;
77
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
78
+ font-size: 12px;
79
+ }
80
+
81
+ .menu-item {
82
+ display: flex;
83
+ align-items: center;
84
+ gap: 8px;
85
+ padding: 8px 12px;
86
+ color: #ccc;
87
+ cursor: pointer;
88
+ transition: background 0.1s ease;
89
+ user-select: none;
90
+ }
91
+
92
+ .menu-item:hover {
93
+ background: rgba(59, 130, 246, 0.15);
94
+ color: #fff;
95
+ }
96
+
97
+ .menu-item.disabled {
98
+ opacity: 0.4;
99
+ cursor: not-allowed;
100
+ pointer-events: none;
101
+ }
102
+
103
+ .menu-item .icon {
104
+ font-family: 'Material Symbols Outlined';
105
+ font-size: 16px;
106
+ font-weight: normal;
107
+ font-style: normal;
108
+ line-height: 1;
109
+ letter-spacing: normal;
110
+ text-transform: none;
111
+ white-space: nowrap;
112
+ word-wrap: normal;
113
+ direction: ltr;
114
+ font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 24;
115
+ opacity: 0.7;
116
+ }
117
+
118
+ .menu-item:hover .icon {
119
+ opacity: 1;
120
+ }
121
+
122
+ .menu-item .label {
123
+ flex: 1;
124
+ }
125
+ `
126
+ ];
127
+
128
+ public render(): TemplateResult {
129
+ return html`
130
+ <div class="menu">
131
+ ${this.menuItems.map(item => html`
132
+ <div
133
+ class="menu-item ${item.disabled ? 'disabled' : ''}"
134
+ @click=${() => this.handleItemClick(item)}
135
+ >
136
+ ${item.iconName ? html`<span class="icon">${item.iconName}</span>` : null}
137
+ <span class="label">${item.name}</span>
138
+ </div>
139
+ `)}
140
+ </div>
141
+ `;
142
+ }
143
+
144
+ async connectedCallback() {
145
+ await super.connectedCallback();
146
+ // Delay adding listeners to avoid immediate close
147
+ requestAnimationFrame(() => {
148
+ document.addEventListener('click', this.boundHandleOutsideClick);
149
+ document.addEventListener('contextmenu', this.boundHandleOutsideClick);
150
+ document.addEventListener('keydown', this.boundHandleKeydown);
151
+ this.classList.add('visible');
152
+ });
153
+ }
154
+
155
+ async disconnectedCallback() {
156
+ await super.disconnectedCallback();
157
+ document.removeEventListener('click', this.boundHandleOutsideClick);
158
+ document.removeEventListener('contextmenu', this.boundHandleOutsideClick);
159
+ document.removeEventListener('keydown', this.boundHandleKeydown);
160
+ }
161
+
162
+ private adjustPosition() {
163
+ const rect = this.getBoundingClientRect();
164
+ const windowWidth = window.innerWidth;
165
+ const windowHeight = window.innerHeight;
166
+
167
+ let x = this.x;
168
+ let y = this.y;
169
+
170
+ // Adjust if menu goes off right edge
171
+ if (x + rect.width > windowWidth - 10) {
172
+ x = windowWidth - rect.width - 10;
173
+ }
174
+
175
+ // Adjust if menu goes off bottom edge
176
+ if (y + rect.height > windowHeight - 10) {
177
+ y = windowHeight - rect.height - 10;
178
+ }
179
+
180
+ // Ensure not off left or top
181
+ if (x < 10) x = 10;
182
+ if (y < 10) y = 10;
183
+
184
+ this.style.left = `${x}px`;
185
+ this.style.top = `${y}px`;
186
+ }
187
+
188
+ private handleOutsideClick(e: Event) {
189
+ const path = e.composedPath();
190
+ if (!path.includes(this)) {
191
+ this.close();
192
+ }
193
+ }
194
+
195
+ private handleKeydown(e: KeyboardEvent) {
196
+ if (e.key === 'Escape') {
197
+ this.close();
198
+ }
199
+ }
200
+
201
+ private async handleItemClick(item: IContextMenuItem) {
202
+ if (item.disabled) return;
203
+ await item.action();
204
+ this.close();
205
+ }
206
+
207
+ private close() {
208
+ this.classList.remove('visible');
209
+ setTimeout(() => this.remove(), 150);
210
+ }
211
+ }
@@ -59,6 +59,13 @@ export class WccDashboard extends DeesElement {
59
59
  @property()
60
60
  accessor selectedTheme: TTheme = 'dark';
61
61
 
62
+ @property()
63
+ accessor searchQuery: string = '';
64
+
65
+ // Pinned items as Set of "sectionName::itemName"
66
+ @property({ attribute: false })
67
+ accessor pinnedItems: Set<string> = new Set();
68
+
62
69
  // Derived from selectedViewport - no need for separate property
63
70
  public get isNative(): boolean {
64
71
  return this.selectedViewport === 'native';
@@ -118,6 +125,8 @@ export class WccDashboard extends DeesElement {
118
125
  <wcc-sidebar
119
126
  .dashboardRef=${this}
120
127
  .selectedItem=${this.selectedItem}
128
+ .searchQuery=${this.searchQuery}
129
+ .pinnedItems=${this.pinnedItems}
121
130
  .isNative=${this.isNative}
122
131
  @selectedType=${(eventArg) => {
123
132
  this.selectedType = eventArg.detail;
@@ -128,6 +137,14 @@ export class WccDashboard extends DeesElement {
128
137
  @selectedItem=${(eventArg) => {
129
138
  this.selectedItem = eventArg.detail;
130
139
  }}
140
+ @searchChanged=${(eventArg: CustomEvent) => {
141
+ this.searchQuery = eventArg.detail;
142
+ this.updateUrlWithScrollState();
143
+ }}
144
+ @pinnedChanged=${(eventArg: CustomEvent) => {
145
+ this.pinnedItems = eventArg.detail;
146
+ this.updateUrlWithScrollState();
147
+ }}
131
148
  ></wcc-sidebar>
132
149
  <wcc-properties
133
150
  .dashboardRef=${this}
@@ -224,22 +241,45 @@ export class WccDashboard extends DeesElement {
224
241
  }
225
242
  }
226
243
 
227
- // Restore scroll positions from query parameters
244
+ // Restore state from query parameters
228
245
  if (routeInfo.queryParams) {
246
+ const search = routeInfo.queryParams.search;
229
247
  const frameScrollY = routeInfo.queryParams.frameScrollY;
230
248
  const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
249
+ const pinned = routeInfo.queryParams.pinned;
231
250
 
251
+ if (search) {
252
+ this.searchQuery = search;
253
+ } else {
254
+ this.searchQuery = '';
255
+ }
232
256
  if (frameScrollY) {
233
257
  this.frameScrollY = parseInt(frameScrollY);
234
258
  }
235
259
  if (sidebarScrollY) {
236
260
  this.sidebarScrollY = parseInt(sidebarScrollY);
237
261
  }
262
+ if (pinned) {
263
+ const newPinned = new Set(pinned.split(',').filter(Boolean));
264
+ // Only update if actually different to avoid update loops
265
+ if (this.pinnedItems.size !== newPinned.size ||
266
+ ![...newPinned].every(k => this.pinnedItems.has(k))) {
267
+ this.pinnedItems = newPinned;
268
+ }
269
+ } else if (this.pinnedItems.size > 0) {
270
+ this.pinnedItems = new Set();
271
+ }
238
272
 
239
273
  // Apply scroll positions after a short delay to ensure DOM is ready
240
274
  setTimeout(() => {
241
275
  this.applyScrollPositions();
242
276
  }, 100);
277
+ } else {
278
+ this.searchQuery = '';
279
+ // Only clear if not already empty to avoid update loops
280
+ if (this.pinnedItems.size > 0) {
281
+ this.pinnedItems = new Set();
282
+ }
243
283
  }
244
284
 
245
285
  const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
@@ -280,22 +320,45 @@ export class WccDashboard extends DeesElement {
280
320
  }
281
321
  }
282
322
 
283
- // Restore scroll positions from query parameters
323
+ // Restore state from query parameters
284
324
  if (routeInfo.queryParams) {
325
+ const search = routeInfo.queryParams.search;
285
326
  const frameScrollY = routeInfo.queryParams.frameScrollY;
286
327
  const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
328
+ const pinned = routeInfo.queryParams.pinned;
287
329
 
330
+ if (search) {
331
+ this.searchQuery = search;
332
+ } else {
333
+ this.searchQuery = '';
334
+ }
288
335
  if (frameScrollY) {
289
336
  this.frameScrollY = parseInt(frameScrollY);
290
337
  }
291
338
  if (sidebarScrollY) {
292
339
  this.sidebarScrollY = parseInt(sidebarScrollY);
293
340
  }
341
+ if (pinned) {
342
+ const newPinned = new Set(pinned.split(',').filter(Boolean));
343
+ // Only update if actually different to avoid update loops
344
+ if (this.pinnedItems.size !== newPinned.size ||
345
+ ![...newPinned].every(k => this.pinnedItems.has(k))) {
346
+ this.pinnedItems = newPinned;
347
+ }
348
+ } else if (this.pinnedItems.size > 0) {
349
+ this.pinnedItems = new Set();
350
+ }
294
351
 
295
352
  // Apply scroll positions after a short delay to ensure DOM is ready
296
353
  setTimeout(() => {
297
354
  this.applyScrollPositions();
298
355
  }, 100);
356
+ } else {
357
+ this.searchQuery = '';
358
+ // Only clear if not already empty to avoid update loops
359
+ if (this.pinnedItems.size > 0) {
360
+ this.pinnedItems = new Set();
361
+ }
299
362
  }
300
363
 
301
364
  const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
@@ -369,12 +432,18 @@ export class WccDashboard extends DeesElement {
369
432
  const baseUrl = `/wcctools-route/${sectionName}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
370
433
  const queryParams = new URLSearchParams();
371
434
 
435
+ if (this.searchQuery) {
436
+ queryParams.set('search', this.searchQuery);
437
+ }
372
438
  if (this.frameScrollY > 0) {
373
439
  queryParams.set('frameScrollY', this.frameScrollY.toString());
374
440
  }
375
441
  if (this.sidebarScrollY > 0) {
376
442
  queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
377
443
  }
444
+ if (this.pinnedItems.size > 0) {
445
+ queryParams.set('pinned', Array.from(this.pinnedItems).join(','));
446
+ }
378
447
 
379
448
  const queryString = queryParams.toString();
380
449
  const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
@@ -426,12 +495,18 @@ export class WccDashboard extends DeesElement {
426
495
  const baseUrl = `/wcctools-route/${sectionName}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
427
496
  const queryParams = new URLSearchParams();
428
497
 
498
+ if (this.searchQuery) {
499
+ queryParams.set('search', this.searchQuery);
500
+ }
429
501
  if (this.frameScrollY > 0) {
430
502
  queryParams.set('frameScrollY', this.frameScrollY.toString());
431
503
  }
432
504
  if (this.sidebarScrollY > 0) {
433
505
  queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
434
506
  }
507
+ if (this.pinnedItems.size > 0) {
508
+ queryParams.set('pinned', Array.from(this.pinnedItems).join(','));
509
+ }
435
510
 
436
511
  const queryString = queryParams.toString();
437
512
  const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;