@design.estate/wcctools 5.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/.smartconfig.json +65 -0
- package/changelog.md +414 -0
- package/dist_bundle/bundle.js +92419 -0
- package/dist_bundle/bundle.js.map +1 -0
- package/dist_ts_web/00_commitinfo_data.d.ts +8 -0
- package/dist_ts_web/00_commitinfo_data.js +9 -0
- package/dist_ts_web/elements/wcc-contextmenu.d.ts +41 -0
- package/dist_ts_web/elements/wcc-contextmenu.js +329 -0
- package/dist_ts_web/elements/wcc-dashboard.d.ts +119 -0
- package/dist_ts_web/elements/wcc-dashboard.js +700 -0
- package/dist_ts_web/elements/wcc-frame.d.ts +22 -0
- package/dist_ts_web/elements/wcc-frame.js +256 -0
- package/dist_ts_web/elements/wcc-properties.d.ts +43 -0
- package/dist_ts_web/elements/wcc-properties.js +1147 -0
- package/dist_ts_web/elements/wcc-record-button.d.ts +12 -0
- package/dist_ts_web/elements/wcc-record-button.js +165 -0
- package/dist_ts_web/elements/wcc-recording-panel.d.ts +44 -0
- package/dist_ts_web/elements/wcc-recording-panel.js +1103 -0
- package/dist_ts_web/elements/wcc-sidebar.d.ts +91 -0
- package/dist_ts_web/elements/wcc-sidebar.js +1497 -0
- package/dist_ts_web/elements/wcctools.helpers.d.ts +46 -0
- package/dist_ts_web/elements/wcctools.helpers.js +106 -0
- package/dist_ts_web/index.d.ts +18 -0
- package/dist_ts_web/index.js +30 -0
- package/dist_ts_web/pages/index.d.ts +1 -0
- package/dist_ts_web/pages/index.js +2 -0
- package/dist_ts_web/services/recorder.service.d.ts +55 -0
- package/dist_ts_web/services/recorder.service.js +353 -0
- package/dist_ts_web/wcctools.interfaces.d.ts +29 -0
- package/dist_ts_web/wcctools.interfaces.js +2 -0
- package/dist_ts_web/wcctools.plugins.d.ts +2 -0
- package/dist_ts_web/wcctools.plugins.js +3 -0
- package/license.md +19 -0
- package/package.json +64 -0
- package/readme.md +568 -0
- package/ts_web/00_commitinfo_data.ts +8 -0
- package/ts_web/elements/wcc-contextmenu.ts +291 -0
- package/ts_web/elements/wcc-dashboard.ts +672 -0
- package/ts_web/elements/wcc-frame.ts +189 -0
- package/ts_web/elements/wcc-properties.ts +1089 -0
- package/ts_web/elements/wcc-record-button.ts +108 -0
- package/ts_web/elements/wcc-recording-panel.ts +1010 -0
- package/ts_web/elements/wcc-sidebar.ts +1470 -0
- package/ts_web/elements/wcctools.helpers.ts +153 -0
- package/ts_web/index.ts +38 -0
- package/ts_web/pages/index.ts +1 -0
- package/ts_web/readme.md +177 -0
- package/ts_web/services/recorder.service.ts +451 -0
- package/ts_web/tspublish.json +3 -0
- package/ts_web/types/dom-mediacapture-stub/index.d.ts +12 -0
- package/ts_web/types/dom-mediacapture-stub/package.json +6 -0
- package/ts_web/types/dom-webcodecs-stub/index.d.ts +2 -0
- package/ts_web/types/dom-webcodecs-stub/package.json +6 -0
- package/ts_web/wcctools.interfaces.ts +31 -0
- package/ts_web/wcctools.plugins.ts +5 -0
|
@@ -0,0 +1,1470 @@
|
|
|
1
|
+
import * as plugins from '../wcctools.plugins.js';
|
|
2
|
+
import { DeesElement, property, html, customElement, type TemplateResult, state } from '@design.estate/dees-element';
|
|
3
|
+
import { WccDashboard, getSectionItems, getSectionItemsWithoutDemo } from './wcc-dashboard.js';
|
|
4
|
+
import type { TTemplateFactory } from './wcctools.helpers.js';
|
|
5
|
+
import { getDemoCount, getElementDemoGroups, getElementTagName, hasMultipleDemos } from './wcctools.helpers.js';
|
|
6
|
+
import type { IWccSection, TElementType } from '../wcctools.interfaces.js';
|
|
7
|
+
import { WccContextmenu, type IContextMenuItem } from './wcc-contextmenu.js';
|
|
8
|
+
import type { WccProperties } from './wcc-properties.js';
|
|
9
|
+
|
|
10
|
+
const SIDEBAR_MIN_WIDTH = 150;
|
|
11
|
+
const SIDEBAR_MAX_WIDTH = 400;
|
|
12
|
+
const PINNED_SECTION = '__pinned__';
|
|
13
|
+
|
|
14
|
+
@customElement('wcc-sidebar')
|
|
15
|
+
export class WccSidebar extends DeesElement {
|
|
16
|
+
@property({ attribute: false })
|
|
17
|
+
accessor selectedItem: DeesElement | TTemplateFactory;
|
|
18
|
+
|
|
19
|
+
@property({ attribute: false })
|
|
20
|
+
accessor selectedType: TElementType;
|
|
21
|
+
|
|
22
|
+
@property()
|
|
23
|
+
accessor dashboardRef: WccDashboard;
|
|
24
|
+
|
|
25
|
+
@property()
|
|
26
|
+
accessor isNative: boolean = false;
|
|
27
|
+
|
|
28
|
+
// Track which elements are expanded (for multi-demo elements)
|
|
29
|
+
@state()
|
|
30
|
+
accessor expandedElements: Set<string> = new Set();
|
|
31
|
+
|
|
32
|
+
// Track which sections are collapsed
|
|
33
|
+
@state()
|
|
34
|
+
accessor collapsedSections: Set<string> = new Set();
|
|
35
|
+
|
|
36
|
+
// Sections whose "Without demo" group is open
|
|
37
|
+
@state()
|
|
38
|
+
accessor expandedWithoutDemo: Set<string> = new Set();
|
|
39
|
+
|
|
40
|
+
// Search query for filtering sidebar items
|
|
41
|
+
@property()
|
|
42
|
+
accessor searchQuery: string = '';
|
|
43
|
+
|
|
44
|
+
// Pinned items as Set of "sectionName::itemName"
|
|
45
|
+
@property({ attribute: false })
|
|
46
|
+
accessor pinnedItems: Set<string> = new Set();
|
|
47
|
+
|
|
48
|
+
// Sidebar width (resizable)
|
|
49
|
+
@property({ type: Number })
|
|
50
|
+
accessor sidebarWidth: number = 200;
|
|
51
|
+
|
|
52
|
+
// Track if currently resizing
|
|
53
|
+
@state()
|
|
54
|
+
accessor isResizing: boolean = false;
|
|
55
|
+
|
|
56
|
+
// Delayed hide for native mode transition
|
|
57
|
+
@state()
|
|
58
|
+
accessor isHidden: boolean = false;
|
|
59
|
+
|
|
60
|
+
// Track if menu is scrolled for header shadow
|
|
61
|
+
@state()
|
|
62
|
+
accessor isMenuScrolled: boolean = false;
|
|
63
|
+
|
|
64
|
+
private sectionsInitialized = false;
|
|
65
|
+
|
|
66
|
+
/** data-key of the tree item that holds the roving tab stop */
|
|
67
|
+
private focusedKey: string | null = null;
|
|
68
|
+
|
|
69
|
+
private hideTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
70
|
+
private highlightedGroup: Element | null = null;
|
|
71
|
+
private groupHighlightTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
72
|
+
|
|
73
|
+
public async disconnectedCallback() {
|
|
74
|
+
const disconnected = super.disconnectedCallback();
|
|
75
|
+
// A removed sidebar has no frame transition to wait for
|
|
76
|
+
clearTimeout(this.hideTimeout);
|
|
77
|
+
this.isHidden = this.isNative;
|
|
78
|
+
this.endGroupHighlight();
|
|
79
|
+
return disconnected;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Returns the scrollable container element (.menu) for external scroll management
|
|
84
|
+
*/
|
|
85
|
+
public get scrollableContainer(): HTMLElement | null {
|
|
86
|
+
return this.shadowRoot?.querySelector('.menu') as HTMLElement | null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public render(): TemplateResult {
|
|
90
|
+
return html`
|
|
91
|
+
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" rel="stylesheet" />
|
|
92
|
+
<style>
|
|
93
|
+
:host {
|
|
94
|
+
/* CSS Variables - Always dark theme to match wcc-properties */
|
|
95
|
+
--background: #0a0a0a;
|
|
96
|
+
--foreground: #e5e5e5;
|
|
97
|
+
--card: #0f0f0f;
|
|
98
|
+
--card-foreground: #f0f0f0;
|
|
99
|
+
--muted: #1a1a1a;
|
|
100
|
+
--muted-foreground: #666;
|
|
101
|
+
--accent: #222;
|
|
102
|
+
--accent-foreground: #fff;
|
|
103
|
+
--border: rgba(255, 255, 255, 0.06);
|
|
104
|
+
--input: #141414;
|
|
105
|
+
--primary: #3b82f6;
|
|
106
|
+
--primary-foreground: #fff;
|
|
107
|
+
--ring: #3b82f6;
|
|
108
|
+
--radius: 4px;
|
|
109
|
+
|
|
110
|
+
display: ${this.isHidden ? 'none' : 'flex'};
|
|
111
|
+
flex-direction: column;
|
|
112
|
+
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
|
113
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
|
114
|
+
font-size: 14px;
|
|
115
|
+
box-sizing: border-box;
|
|
116
|
+
position: absolute;
|
|
117
|
+
left: 0px;
|
|
118
|
+
width: ${this.sidebarWidth}px;
|
|
119
|
+
top: 0px;
|
|
120
|
+
bottom: 0px;
|
|
121
|
+
overflow: hidden;
|
|
122
|
+
background: var(--background);
|
|
123
|
+
color: var(--foreground);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.sidebar-header {
|
|
127
|
+
flex-shrink: 0;
|
|
128
|
+
transition: box-shadow 0.2s ease, border-color 0.2s ease;
|
|
129
|
+
border-bottom: 1px solid transparent;
|
|
130
|
+
position: relative;
|
|
131
|
+
z-index: 1;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.sidebar-header.scrolled {
|
|
135
|
+
box-shadow: 0 8px 24px -2px rgba(0, 0, 0, 1);
|
|
136
|
+
border-bottom-color: var(--border);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.menu {
|
|
140
|
+
flex: 1;
|
|
141
|
+
min-height: 0;
|
|
142
|
+
overflow-y: auto;
|
|
143
|
+
overflow-x: hidden;
|
|
144
|
+
padding: 0.5rem 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.section-header {
|
|
148
|
+
padding: 0.3rem 0.75rem;
|
|
149
|
+
font-size: 0.65rem;
|
|
150
|
+
font-weight: 500;
|
|
151
|
+
text-transform: uppercase;
|
|
152
|
+
letter-spacing: 0.08em;
|
|
153
|
+
color: #888;
|
|
154
|
+
margin: 0;
|
|
155
|
+
margin-top: 0.5rem;
|
|
156
|
+
background: rgba(59, 130, 246, 0.03);
|
|
157
|
+
border-bottom: 1px solid var(--border);
|
|
158
|
+
border-top: 1px solid var(--border);
|
|
159
|
+
display: flex;
|
|
160
|
+
align-items: center;
|
|
161
|
+
gap: 0.5rem;
|
|
162
|
+
cursor: pointer;
|
|
163
|
+
user-select: none;
|
|
164
|
+
transition: all 0.15s ease;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.section-header:first-child {
|
|
168
|
+
margin-top: 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.section-header:hover {
|
|
172
|
+
background: rgba(59, 130, 246, 0.08);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.section-header .expand-icon {
|
|
176
|
+
font-size: 14px;
|
|
177
|
+
opacity: 0.5;
|
|
178
|
+
transition: transform 0.2s ease;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.section-header.collapsed .expand-icon {
|
|
182
|
+
transform: rotate(-90deg);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.section-header .section-icon {
|
|
186
|
+
font-size: 14px;
|
|
187
|
+
opacity: 0.6;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.section-content {
|
|
191
|
+
overflow: hidden;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.section-content.collapsed {
|
|
195
|
+
display: none;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.material-symbols-outlined {
|
|
199
|
+
font-family: 'Material Symbols Outlined';
|
|
200
|
+
font-weight: normal;
|
|
201
|
+
font-style: normal;
|
|
202
|
+
font-size: 16px;
|
|
203
|
+
display: inline-block;
|
|
204
|
+
line-height: 1;
|
|
205
|
+
text-transform: none;
|
|
206
|
+
letter-spacing: normal;
|
|
207
|
+
word-wrap: normal;
|
|
208
|
+
white-space: nowrap;
|
|
209
|
+
direction: ltr;
|
|
210
|
+
font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 24;
|
|
211
|
+
opacity: 0.5;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.selectOption {
|
|
215
|
+
user-select: none;
|
|
216
|
+
position: relative;
|
|
217
|
+
margin: 0.125rem 0.5rem;
|
|
218
|
+
padding: 0.5rem 0.75rem;
|
|
219
|
+
transition: all 0.15s ease;
|
|
220
|
+
display: grid;
|
|
221
|
+
grid-template-columns: 20px 1fr;
|
|
222
|
+
align-items: center;
|
|
223
|
+
gap: 0.5rem;
|
|
224
|
+
border-radius: var(--radius);
|
|
225
|
+
cursor: pointer;
|
|
226
|
+
font-size: 0.75rem;
|
|
227
|
+
color: #999;
|
|
228
|
+
background: transparent;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.selectOption.folder {
|
|
232
|
+
grid-template-columns: 16px 1fr;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.selectOption.folder .text {
|
|
236
|
+
margin-left: 4px;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.selectOption .expand-icon {
|
|
240
|
+
font-size: 14px;
|
|
241
|
+
opacity: 0.5;
|
|
242
|
+
transition: transform 0.2s ease;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.selectOption.expanded .expand-icon {
|
|
246
|
+
transform: rotate(90deg);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.selectOption:hover {
|
|
250
|
+
background: rgba(59, 130, 246, 0.05);
|
|
251
|
+
color: #bbb;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.selectOption:hover .material-symbols-outlined {
|
|
255
|
+
opacity: 0.7;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.selectOption.selected {
|
|
259
|
+
background: rgba(59, 130, 246, 0.15);
|
|
260
|
+
color: var(--primary);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.selectOption.selected .material-symbols-outlined {
|
|
264
|
+
opacity: 1;
|
|
265
|
+
font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.selectOption.selected:hover {
|
|
269
|
+
background: rgba(59, 130, 246, 0.2);
|
|
270
|
+
color: var(--primary);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.selectOption .text {
|
|
274
|
+
display: block;
|
|
275
|
+
overflow: hidden;
|
|
276
|
+
text-overflow: ellipsis;
|
|
277
|
+
white-space: nowrap;
|
|
278
|
+
font-weight: 400;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.demo-children {
|
|
282
|
+
margin-left: 1rem;
|
|
283
|
+
overflow: hidden;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.demo-child {
|
|
287
|
+
user-select: none;
|
|
288
|
+
position: relative;
|
|
289
|
+
margin: 0.125rem 0.5rem;
|
|
290
|
+
padding: 0.35rem 0.75rem;
|
|
291
|
+
transition: all 0.15s ease;
|
|
292
|
+
display: grid;
|
|
293
|
+
grid-template-columns: 16px 1fr;
|
|
294
|
+
align-items: center;
|
|
295
|
+
gap: 0.5rem;
|
|
296
|
+
border-radius: var(--radius);
|
|
297
|
+
cursor: pointer;
|
|
298
|
+
font-size: 0.7rem;
|
|
299
|
+
color: #777;
|
|
300
|
+
background: transparent;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.demo-child:hover {
|
|
304
|
+
background: rgba(59, 130, 246, 0.05);
|
|
305
|
+
color: #bbb;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.demo-child.selected {
|
|
309
|
+
background: rgba(59, 130, 246, 0.15);
|
|
310
|
+
color: var(--primary);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.demo-child .material-symbols-outlined {
|
|
314
|
+
font-size: 14px;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
::-webkit-scrollbar {
|
|
318
|
+
width: 8px;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
::-webkit-scrollbar-track {
|
|
322
|
+
background: transparent;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
::-webkit-scrollbar-thumb {
|
|
326
|
+
background: rgba(255, 255, 255, 0.1);
|
|
327
|
+
border-radius: 4px;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
::-webkit-scrollbar-thumb:hover {
|
|
331
|
+
background: rgba(255, 255, 255, 0.2);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.search-container {
|
|
335
|
+
padding: 0.5rem;
|
|
336
|
+
border-bottom: 1px solid var(--border);
|
|
337
|
+
position: relative;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.search-input {
|
|
341
|
+
width: 100%;
|
|
342
|
+
box-sizing: border-box;
|
|
343
|
+
background: var(--input);
|
|
344
|
+
border: 1px solid var(--border);
|
|
345
|
+
border-radius: var(--radius);
|
|
346
|
+
padding: 0.5rem 1.75rem 0.5rem 0.75rem;
|
|
347
|
+
color: var(--foreground);
|
|
348
|
+
font-size: 0.75rem;
|
|
349
|
+
font-family: inherit;
|
|
350
|
+
outline: none;
|
|
351
|
+
transition: border-color 0.15s ease;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.search-input:focus {
|
|
355
|
+
border-color: var(--primary);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.search-input::placeholder {
|
|
359
|
+
color: var(--muted-foreground);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.search-clear {
|
|
363
|
+
position: absolute;
|
|
364
|
+
right: 0.75rem;
|
|
365
|
+
top: 50%;
|
|
366
|
+
transform: translateY(-50%);
|
|
367
|
+
background: none;
|
|
368
|
+
border: none;
|
|
369
|
+
padding: 0.25rem;
|
|
370
|
+
cursor: pointer;
|
|
371
|
+
color: var(--muted-foreground);
|
|
372
|
+
display: flex;
|
|
373
|
+
align-items: center;
|
|
374
|
+
justify-content: center;
|
|
375
|
+
border-radius: 2px;
|
|
376
|
+
transition: color 0.15s ease, background 0.15s ease;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.search-clear:hover {
|
|
380
|
+
color: var(--foreground);
|
|
381
|
+
background: rgba(255, 255, 255, 0.1);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.search-clear .material-symbols-outlined {
|
|
385
|
+
font-size: 14px;
|
|
386
|
+
opacity: 1;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.highlight {
|
|
390
|
+
background: rgba(59, 130, 246, 0.3);
|
|
391
|
+
border-radius: 2px;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/* Pinned item highlight in original section */
|
|
395
|
+
.selectOption.pinned {
|
|
396
|
+
background: rgba(245, 158, 11, 0.08);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.selectOption.pinned:hover {
|
|
400
|
+
background: rgba(245, 158, 11, 0.12);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.selectOption.pinned.selected {
|
|
404
|
+
background: rgba(245, 158, 11, 0.18);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/* Pinned section styling */
|
|
408
|
+
.section-header.pinned-section {
|
|
409
|
+
background: rgba(245, 158, 11, 0.08);
|
|
410
|
+
color: #f59e0b;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.section-header.pinned-section:hover {
|
|
414
|
+
background: rgba(245, 158, 11, 0.12);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.section-header.pinned-section .section-icon {
|
|
418
|
+
opacity: 0.8;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/* Section tag pill for pinned items */
|
|
422
|
+
.section-tag {
|
|
423
|
+
font-size: 0.5rem;
|
|
424
|
+
color: #888;
|
|
425
|
+
margin-left: auto;
|
|
426
|
+
text-transform: uppercase;
|
|
427
|
+
letter-spacing: 0.02em;
|
|
428
|
+
background: rgba(255, 255, 255, 0.06);
|
|
429
|
+
padding: 0.15rem 0.4rem;
|
|
430
|
+
border-radius: 9999px;
|
|
431
|
+
white-space: nowrap;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/* Group container */
|
|
435
|
+
.item-group {
|
|
436
|
+
margin: 0.375rem 0.375rem;
|
|
437
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
438
|
+
border-radius: 6px;
|
|
439
|
+
padding: 0.25rem 0;
|
|
440
|
+
background: rgba(255, 255, 255, 0.01);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.item-group-legend {
|
|
444
|
+
font-size: 0.55rem;
|
|
445
|
+
text-transform: uppercase;
|
|
446
|
+
letter-spacing: 0.05em;
|
|
447
|
+
color: #555;
|
|
448
|
+
padding: 0.125rem 0.625rem 0.25rem;
|
|
449
|
+
display: block;
|
|
450
|
+
cursor: context-menu;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.item-group .selectOption {
|
|
454
|
+
margin-left: 0.25rem;
|
|
455
|
+
margin-right: 0.25rem;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.item-group.group-highlight {
|
|
459
|
+
background: rgba(59, 130, 246, 0.15);
|
|
460
|
+
transition: background 0.3s ease;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.item-group.group-filter-match {
|
|
464
|
+
border-color: rgba(245, 158, 11, 0.5);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/* Resize handle */
|
|
468
|
+
.resize-handle {
|
|
469
|
+
position: absolute;
|
|
470
|
+
top: 0;
|
|
471
|
+
right: 0;
|
|
472
|
+
bottom: 0;
|
|
473
|
+
width: 4px;
|
|
474
|
+
cursor: col-resize;
|
|
475
|
+
background: transparent;
|
|
476
|
+
transition: background 0.15s ease;
|
|
477
|
+
z-index: 10;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.resize-handle:hover {
|
|
481
|
+
background: rgba(59, 130, 246, 0.3);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.resize-handle.active {
|
|
485
|
+
background: var(--primary);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
@container wccDashboard (max-width: 600px) {
|
|
489
|
+
:host {
|
|
490
|
+
right: 0;
|
|
491
|
+
bottom: auto;
|
|
492
|
+
width: 100%;
|
|
493
|
+
height: 120px;
|
|
494
|
+
border-right: 0;
|
|
495
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.resize-handle {
|
|
499
|
+
display: none;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
nav.sidebar-nav {
|
|
504
|
+
flex: 1;
|
|
505
|
+
min-height: 0;
|
|
506
|
+
display: flex;
|
|
507
|
+
flex-direction: column;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.tree {
|
|
511
|
+
flex: 1;
|
|
512
|
+
min-height: 0;
|
|
513
|
+
display: flex;
|
|
514
|
+
flex-direction: column;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
.treeitem {
|
|
518
|
+
outline: none;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.treeitem:focus-visible {
|
|
522
|
+
outline: 2px solid var(--ring);
|
|
523
|
+
outline-offset: -2px;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.search-input:focus-visible {
|
|
527
|
+
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.35);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.search-input::-webkit-search-cancel-button {
|
|
531
|
+
-webkit-appearance: none;
|
|
532
|
+
display: none;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.search-clear:focus-visible {
|
|
536
|
+
outline: 2px solid var(--ring);
|
|
537
|
+
outline-offset: 0;
|
|
538
|
+
color: var(--foreground);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.resize-handle:focus-visible {
|
|
542
|
+
outline: none;
|
|
543
|
+
background: var(--primary);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/* Elements without a demo: listed for coverage, not navigable */
|
|
547
|
+
.nodemo-header {
|
|
548
|
+
user-select: none;
|
|
549
|
+
margin: 0.375rem 0.5rem 0.125rem;
|
|
550
|
+
padding: 0.4rem 0.75rem;
|
|
551
|
+
display: grid;
|
|
552
|
+
grid-template-columns: 16px 1fr auto;
|
|
553
|
+
align-items: center;
|
|
554
|
+
gap: 0.5rem;
|
|
555
|
+
border-radius: var(--radius);
|
|
556
|
+
cursor: pointer;
|
|
557
|
+
font-size: 0.65rem;
|
|
558
|
+
text-transform: uppercase;
|
|
559
|
+
letter-spacing: 0.05em;
|
|
560
|
+
color: #666;
|
|
561
|
+
border-top: 1px dashed rgba(255, 255, 255, 0.08);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.nodemo-header:hover {
|
|
565
|
+
color: #999;
|
|
566
|
+
background: rgba(255, 255, 255, 0.03);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.nodemo-header .expand-icon {
|
|
570
|
+
font-size: 14px;
|
|
571
|
+
opacity: 0.5;
|
|
572
|
+
transition: transform 0.2s ease;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.nodemo-header[aria-expanded='true'] .expand-icon {
|
|
576
|
+
transform: rotate(90deg);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.nodemo-count {
|
|
580
|
+
font-size: 0.6rem;
|
|
581
|
+
letter-spacing: 0;
|
|
582
|
+
color: #777;
|
|
583
|
+
background: rgba(255, 255, 255, 0.06);
|
|
584
|
+
padding: 0.05rem 0.4rem;
|
|
585
|
+
border-radius: 9999px;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.selectOption.nodemo {
|
|
589
|
+
cursor: default;
|
|
590
|
+
color: #5c5c5c;
|
|
591
|
+
padding-top: 0.35rem;
|
|
592
|
+
padding-bottom: 0.35rem;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.selectOption.nodemo:hover {
|
|
596
|
+
background: transparent;
|
|
597
|
+
color: #5c5c5c;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
.selectOption.nodemo .material-symbols-outlined {
|
|
601
|
+
opacity: 0.35;
|
|
602
|
+
}
|
|
603
|
+
</style>
|
|
604
|
+
<nav class="sidebar-nav" aria-label="Catalog">
|
|
605
|
+
<div class="sidebar-header ${this.isMenuScrolled ? 'scrolled' : ''}">
|
|
606
|
+
<div class="search-container" role="search">
|
|
607
|
+
<input
|
|
608
|
+
type="search"
|
|
609
|
+
class="search-input"
|
|
610
|
+
placeholder="Search..."
|
|
611
|
+
aria-label="Search catalog"
|
|
612
|
+
aria-controls="wcc-tree"
|
|
613
|
+
.value=${this.searchQuery}
|
|
614
|
+
@input=${this.handleSearchInput}
|
|
615
|
+
@keydown=${this.handleSearchKeydown}
|
|
616
|
+
/>
|
|
617
|
+
${this.searchQuery ? html`
|
|
618
|
+
<button type="button" class="search-clear" aria-label="Clear search" @click=${this.clearSearch}>
|
|
619
|
+
<i class="material-symbols-outlined" aria-hidden="true">close</i>
|
|
620
|
+
</button>
|
|
621
|
+
` : null}
|
|
622
|
+
</div>
|
|
623
|
+
</div>
|
|
624
|
+
<div
|
|
625
|
+
class="tree"
|
|
626
|
+
id="wcc-tree"
|
|
627
|
+
role="tree"
|
|
628
|
+
aria-label="Catalog entries"
|
|
629
|
+
@keydown=${this.handleTreeKeydown}
|
|
630
|
+
@focusin=${this.handleTreeFocusin}
|
|
631
|
+
>
|
|
632
|
+
${this.renderPinnedSection()}
|
|
633
|
+
<div class="menu" @scroll=${this.handleMenuScroll}>
|
|
634
|
+
${this.renderSections()}
|
|
635
|
+
</div>
|
|
636
|
+
</div>
|
|
637
|
+
</nav>
|
|
638
|
+
<div
|
|
639
|
+
class="resize-handle ${this.isResizing ? 'active' : ''}"
|
|
640
|
+
role="separator"
|
|
641
|
+
tabindex="0"
|
|
642
|
+
aria-orientation="vertical"
|
|
643
|
+
aria-label="Resize sidebar"
|
|
644
|
+
aria-controls="wcc-tree"
|
|
645
|
+
aria-valuemin=${SIDEBAR_MIN_WIDTH}
|
|
646
|
+
aria-valuemax=${SIDEBAR_MAX_WIDTH}
|
|
647
|
+
aria-valuenow=${this.sidebarWidth}
|
|
648
|
+
@mousedown=${this.startResize}
|
|
649
|
+
@keydown=${this.handleResizeKeydown}
|
|
650
|
+
></div>
|
|
651
|
+
`;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// ============ Tree item keys ============
|
|
655
|
+
|
|
656
|
+
private getSectionKey(sectionName: string): string {
|
|
657
|
+
return `section:${sectionName}`;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
private getItemKey(sectionName: string, groupName: string | null, itemName: string): string {
|
|
661
|
+
return `item:${sectionName}::${groupName ?? ''}::${itemName}`;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Initialize collapsed sections from section config
|
|
666
|
+
*/
|
|
667
|
+
private initCollapsedSections() {
|
|
668
|
+
if (this.sectionsInitialized || !this.dashboardRef?.sections) return;
|
|
669
|
+
|
|
670
|
+
const collapsed = new Set<string>();
|
|
671
|
+
for (const section of this.dashboardRef.sections) {
|
|
672
|
+
if (section.collapsed) {
|
|
673
|
+
collapsed.add(section.name);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
this.collapsedSections = collapsed;
|
|
677
|
+
this.sectionsInitialized = true;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// ============ Pinning helpers ============
|
|
681
|
+
|
|
682
|
+
private getPinKey(sectionName: string, itemName: string): string {
|
|
683
|
+
return `${sectionName}::${itemName}`;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
private isPinned(sectionName: string, itemName: string): boolean {
|
|
687
|
+
return this.pinnedItems.has(this.getPinKey(sectionName, itemName));
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
private togglePin(sectionName: string, itemName: string) {
|
|
691
|
+
const key = this.getPinKey(sectionName, itemName);
|
|
692
|
+
const newPinned = new Set(this.pinnedItems);
|
|
693
|
+
if (newPinned.has(key)) {
|
|
694
|
+
newPinned.delete(key);
|
|
695
|
+
} else {
|
|
696
|
+
newPinned.add(key);
|
|
697
|
+
}
|
|
698
|
+
this.pinnedItems = newPinned;
|
|
699
|
+
this.dispatchEvent(new CustomEvent('pinnedChanged', { detail: newPinned }));
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
private showContextMenu(e: MouseEvent, sectionName: string, itemName: string) {
|
|
703
|
+
const isPinned = this.isPinned(sectionName, itemName);
|
|
704
|
+
const section = this.dashboardRef?.sections?.find(s => s.name === sectionName);
|
|
705
|
+
const sectionEntries = section ? getSectionItems(section) : [];
|
|
706
|
+
const foundEntry = sectionEntries.find(([name]) => name === itemName);
|
|
707
|
+
const item = foundEntry?.[1];
|
|
708
|
+
const groups = getElementDemoGroups(item);
|
|
709
|
+
|
|
710
|
+
const menuItems: IContextMenuItem[] = [
|
|
711
|
+
{
|
|
712
|
+
name: isPinned ? 'Unpin' : 'Pin',
|
|
713
|
+
iconName: 'push_pin',
|
|
714
|
+
action: () => this.togglePin(sectionName, itemName),
|
|
715
|
+
},
|
|
716
|
+
];
|
|
717
|
+
|
|
718
|
+
if (groups.length > 0) {
|
|
719
|
+
menuItems.push({
|
|
720
|
+
name: 'Show in Group:',
|
|
721
|
+
iconName: 'folder',
|
|
722
|
+
action: () => {},
|
|
723
|
+
disabled: true,
|
|
724
|
+
});
|
|
725
|
+
for (const groupName of groups) {
|
|
726
|
+
menuItems.push({
|
|
727
|
+
name: groupName,
|
|
728
|
+
iconName: 'label',
|
|
729
|
+
action: () => this.scrollToGroup(sectionName, groupName),
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const owner = (e.currentTarget as HTMLElement | null)?.closest<HTMLElement>('[role="treeitem"]') ?? undefined;
|
|
735
|
+
WccContextmenu.show(e, menuItems, { ownerElement: owner });
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
private showGroupContextMenu(e: MouseEvent, groupName: string) {
|
|
739
|
+
WccContextmenu.show(e, [
|
|
740
|
+
{
|
|
741
|
+
name: `Show "${groupName}"`,
|
|
742
|
+
iconName: 'filter_alt',
|
|
743
|
+
action: () => {
|
|
744
|
+
this.searchQuery = groupName;
|
|
745
|
+
this.dispatchEvent(new CustomEvent('searchChanged', { detail: this.searchQuery }));
|
|
746
|
+
},
|
|
747
|
+
},
|
|
748
|
+
]);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Render the PINNED section (only if there are pinned items)
|
|
753
|
+
*/
|
|
754
|
+
private renderPinnedSection() {
|
|
755
|
+
if (!this.dashboardRef?.sections || this.pinnedItems.size === 0) {
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const isCollapsed = this.collapsedSections.has(PINNED_SECTION);
|
|
760
|
+
|
|
761
|
+
// Collect pinned items with their original section info
|
|
762
|
+
// Pinned items are NOT filtered by search - they always remain visible
|
|
763
|
+
const pinnedEntries: Array<{ sectionName: string; itemName: string; item: any; section: IWccSection }> = [];
|
|
764
|
+
|
|
765
|
+
for (const key of this.pinnedItems) {
|
|
766
|
+
const [sectionName, itemName] = key.split('::');
|
|
767
|
+
const section = this.dashboardRef.sections.find(s => s.name === sectionName);
|
|
768
|
+
if (section) {
|
|
769
|
+
const found = getSectionItems(section).find(([name]) => name === itemName);
|
|
770
|
+
if (found) {
|
|
771
|
+
pinnedEntries.push({ sectionName, itemName, item: found[1], section });
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (pinnedEntries.length === 0) {
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
return html`
|
|
781
|
+
<div
|
|
782
|
+
class="treeitem section-header pinned-section ${isCollapsed ? 'collapsed' : ''}"
|
|
783
|
+
role="treeitem"
|
|
784
|
+
tabindex="-1"
|
|
785
|
+
aria-level="1"
|
|
786
|
+
aria-expanded=${isCollapsed ? 'false' : 'true'}
|
|
787
|
+
data-key=${this.getSectionKey(PINNED_SECTION)}
|
|
788
|
+
@click=${() => this.toggleSectionCollapsed(PINNED_SECTION)}
|
|
789
|
+
>
|
|
790
|
+
<i class="material-symbols-outlined expand-icon" aria-hidden="true">expand_more</i>
|
|
791
|
+
<i class="material-symbols-outlined section-icon" aria-hidden="true">push_pin</i>
|
|
792
|
+
<span>Pinned</span>
|
|
793
|
+
</div>
|
|
794
|
+
<div class="section-content ${isCollapsed ? 'collapsed' : ''}" role="group" aria-label="Pinned">
|
|
795
|
+
${pinnedEntries.map(({ sectionName, itemName, item, section }) => {
|
|
796
|
+
const isSelected = this.selectedItem === item;
|
|
797
|
+
const type = section.type === 'elements' ? 'element' : 'page';
|
|
798
|
+
const icon = section.type === 'elements'
|
|
799
|
+
? (getElementDemoGroups(item).length > 1 ? 'library_books' : 'featured_video')
|
|
800
|
+
: 'insert_drive_file';
|
|
801
|
+
|
|
802
|
+
return html`
|
|
803
|
+
<div
|
|
804
|
+
class="treeitem selectOption ${isSelected ? 'selected' : ''}"
|
|
805
|
+
role="treeitem"
|
|
806
|
+
tabindex="-1"
|
|
807
|
+
aria-level="2"
|
|
808
|
+
aria-selected=${isSelected ? 'true' : 'false'}
|
|
809
|
+
aria-current=${isSelected ? 'page' : 'false'}
|
|
810
|
+
data-key=${`pinned:${sectionName}::${itemName}`}
|
|
811
|
+
title=${itemName}
|
|
812
|
+
@click=${async () => {
|
|
813
|
+
await plugins.deesDomtools.DomTools.setupDomTools();
|
|
814
|
+
this.selectItem(type, itemName, item, 0, section);
|
|
815
|
+
}}
|
|
816
|
+
@contextmenu=${(e: MouseEvent) => this.showContextMenu(e, sectionName, itemName)}
|
|
817
|
+
>
|
|
818
|
+
<i class="material-symbols-outlined" aria-hidden="true">${icon}</i>
|
|
819
|
+
<div class="text">${this.highlightMatch(itemName)}</div>
|
|
820
|
+
<span class="section-tag">${sectionName}</span>
|
|
821
|
+
</div>
|
|
822
|
+
`;
|
|
823
|
+
})}
|
|
824
|
+
</div>
|
|
825
|
+
`;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Render all sections
|
|
830
|
+
*/
|
|
831
|
+
private renderSections() {
|
|
832
|
+
if (!this.dashboardRef?.sections) {
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
this.initCollapsedSections();
|
|
837
|
+
|
|
838
|
+
return this.dashboardRef.sections.map((section) => {
|
|
839
|
+
const listedEntries = getSectionItems(section).filter(([name, item]) => this.entryMatchesSearch(name, item));
|
|
840
|
+
const withoutDemoEntries = getSectionItemsWithoutDemo(section).filter(([name, item]) => this.entryMatchesSearch(name, item));
|
|
841
|
+
|
|
842
|
+
// Hide section if no items match the search
|
|
843
|
+
if (listedEntries.length === 0 && withoutDemoEntries.length === 0 && this.searchQuery) {
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const isCollapsed = this.collapsedSections.has(section.name);
|
|
848
|
+
|
|
849
|
+
return html`
|
|
850
|
+
<div
|
|
851
|
+
class="treeitem section-header ${isCollapsed ? 'collapsed' : ''}"
|
|
852
|
+
role="treeitem"
|
|
853
|
+
tabindex="-1"
|
|
854
|
+
aria-level="1"
|
|
855
|
+
aria-expanded=${isCollapsed ? 'false' : 'true'}
|
|
856
|
+
data-key=${this.getSectionKey(section.name)}
|
|
857
|
+
@click=${() => this.toggleSectionCollapsed(section.name)}
|
|
858
|
+
>
|
|
859
|
+
<i class="material-symbols-outlined expand-icon" aria-hidden="true">expand_more</i>
|
|
860
|
+
${section.icon ? html`<i class="material-symbols-outlined section-icon" aria-hidden="true">${section.icon}</i>` : null}
|
|
861
|
+
<span>${section.name}</span>
|
|
862
|
+
</div>
|
|
863
|
+
<div class="section-content ${isCollapsed ? 'collapsed' : ''}" role="group" aria-label=${section.name}>
|
|
864
|
+
${this.renderSectionItems(section, listedEntries)}
|
|
865
|
+
${this.renderWithoutDemoGroup(section, withoutDemoEntries)}
|
|
866
|
+
</div>
|
|
867
|
+
`;
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Render the listed (navigable) items of a section
|
|
873
|
+
*/
|
|
874
|
+
private renderSectionItems(section: IWccSection, filteredEntries: Array<[string, any]>) {
|
|
875
|
+
if (section.type === 'pages') {
|
|
876
|
+
return filteredEntries.map(([pageName, item]) => {
|
|
877
|
+
const isPinned = this.isPinned(section.name, pageName);
|
|
878
|
+
const isSelected = this.selectedItem === item;
|
|
879
|
+
return html`
|
|
880
|
+
<div
|
|
881
|
+
class="treeitem selectOption ${isSelected ? 'selected' : ''} ${isPinned ? 'pinned' : ''}"
|
|
882
|
+
role="treeitem"
|
|
883
|
+
tabindex="-1"
|
|
884
|
+
aria-level="2"
|
|
885
|
+
aria-selected=${isSelected ? 'true' : 'false'}
|
|
886
|
+
aria-current=${isSelected ? 'page' : 'false'}
|
|
887
|
+
data-key=${this.getItemKey(section.name, null, pageName)}
|
|
888
|
+
title=${pageName}
|
|
889
|
+
@click=${async () => {
|
|
890
|
+
await plugins.deesDomtools.DomTools.setupDomTools();
|
|
891
|
+
this.selectItem('page', pageName, item, 0, section);
|
|
892
|
+
}}
|
|
893
|
+
@contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, pageName)}
|
|
894
|
+
>
|
|
895
|
+
<i class="material-symbols-outlined" aria-hidden="true">insert_drive_file</i>
|
|
896
|
+
<div class="text">${this.highlightMatch(pageName)}</div>
|
|
897
|
+
</div>
|
|
898
|
+
`;
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// type === 'elements' - group by demoGroups (supports string | string[])
|
|
903
|
+
const groupedItems = new Map<string | null, Array<[string, any]>>();
|
|
904
|
+
|
|
905
|
+
for (const entry of filteredEntries) {
|
|
906
|
+
const groups = getElementDemoGroups(entry[1]);
|
|
907
|
+
for (const group of groups.length > 0 ? groups : [null]) {
|
|
908
|
+
if (!groupedItems.has(group)) {
|
|
909
|
+
groupedItems.set(group, []);
|
|
910
|
+
}
|
|
911
|
+
groupedItems.get(group)!.push(entry);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// Build a unified list of render items (ungrouped elements and groups)
|
|
916
|
+
// Each item has a sortKey (element name or group name)
|
|
917
|
+
type RenderItem =
|
|
918
|
+
| { type: 'element'; entry: [string, any]; sortKey: string }
|
|
919
|
+
| { type: 'group'; groupName: string; items: Array<[string, any]>; sortKey: string };
|
|
920
|
+
|
|
921
|
+
const renderItems: RenderItem[] = [];
|
|
922
|
+
|
|
923
|
+
for (const entry of groupedItems.get(null) || []) {
|
|
924
|
+
renderItems.push({ type: 'element', entry, sortKey: entry[0].toLowerCase() });
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
for (const [groupName, items] of groupedItems) {
|
|
928
|
+
if (groupName === null) continue;
|
|
929
|
+
renderItems.push({ type: 'group', groupName, items, sortKey: groupName.toLowerCase() });
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Sort all items alphabetically by sortKey
|
|
933
|
+
renderItems.sort((a, b) => a.sortKey.localeCompare(b.sortKey));
|
|
934
|
+
|
|
935
|
+
return renderItems.map((item) => {
|
|
936
|
+
if (item.type === 'element') {
|
|
937
|
+
return this.renderElementItem(item.entry, section, null);
|
|
938
|
+
}
|
|
939
|
+
return html`
|
|
940
|
+
<div
|
|
941
|
+
class="item-group ${this.isGroupFilterMatch(item.groupName) ? 'group-filter-match' : ''}"
|
|
942
|
+
role="group"
|
|
943
|
+
aria-label=${item.groupName}
|
|
944
|
+
data-group="${item.groupName}"
|
|
945
|
+
>
|
|
946
|
+
<span
|
|
947
|
+
class="item-group-legend"
|
|
948
|
+
aria-hidden="true"
|
|
949
|
+
@contextmenu=${(e: MouseEvent) => this.showGroupContextMenu(e, item.groupName)}
|
|
950
|
+
>${item.groupName}</span>
|
|
951
|
+
${item.items.map((entry) => this.renderElementItem(entry, section, item.groupName))}
|
|
952
|
+
</div>
|
|
953
|
+
`;
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Render the "Without demo" group: registered elements that have no demo.
|
|
959
|
+
* Listed for coverage; the entries are not navigable.
|
|
960
|
+
*/
|
|
961
|
+
private renderWithoutDemoGroup(section: IWccSection, entries: Array<[string, any]>) {
|
|
962
|
+
if (section.type !== 'elements' || entries.length === 0) {
|
|
963
|
+
return null;
|
|
964
|
+
}
|
|
965
|
+
// A search opens the group so its matches are visible
|
|
966
|
+
const isOpen = !!this.searchQuery || this.expandedWithoutDemo.has(section.name);
|
|
967
|
+
|
|
968
|
+
return html`
|
|
969
|
+
<div
|
|
970
|
+
class="treeitem nodemo-header"
|
|
971
|
+
role="treeitem"
|
|
972
|
+
tabindex="-1"
|
|
973
|
+
aria-level="2"
|
|
974
|
+
aria-expanded=${isOpen ? 'true' : 'false'}
|
|
975
|
+
data-key=${`nodemo-group:${section.name}`}
|
|
976
|
+
@click=${() => this.toggleWithoutDemo(section.name)}
|
|
977
|
+
>
|
|
978
|
+
<i class="material-symbols-outlined expand-icon" aria-hidden="true">chevron_right</i>
|
|
979
|
+
<span>Without demo</span>
|
|
980
|
+
<span class="nodemo-count" aria-label=${`${entries.length} elements`}>${entries.length}</span>
|
|
981
|
+
</div>
|
|
982
|
+
${isOpen ? html`
|
|
983
|
+
<div class="nodemo-list" role="group" aria-label="Without demo">
|
|
984
|
+
${entries.map(([elementName, item]) => {
|
|
985
|
+
const tagName = getElementTagName(item);
|
|
986
|
+
return html`
|
|
987
|
+
<div
|
|
988
|
+
class="treeitem selectOption nodemo"
|
|
989
|
+
role="treeitem"
|
|
990
|
+
tabindex="-1"
|
|
991
|
+
aria-level="3"
|
|
992
|
+
aria-disabled="true"
|
|
993
|
+
aria-description="No demo"
|
|
994
|
+
data-key=${`nodemo:${section.name}::${elementName}`}
|
|
995
|
+
title=${tagName ? `<${tagName}> has no demo` : `${elementName} has no demo`}
|
|
996
|
+
>
|
|
997
|
+
<i class="material-symbols-outlined" aria-hidden="true">block</i>
|
|
998
|
+
<div class="text">${this.highlightMatch(elementName)}</div>
|
|
999
|
+
</div>
|
|
1000
|
+
`;
|
|
1001
|
+
})}
|
|
1002
|
+
</div>
|
|
1003
|
+
` : null}
|
|
1004
|
+
`;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Render a single element item (used by renderSectionItems)
|
|
1009
|
+
*/
|
|
1010
|
+
private renderElementItem(entry: [string, any], section: IWccSection, groupName: string | null): TemplateResult {
|
|
1011
|
+
const [elementName, item] = entry;
|
|
1012
|
+
const demoCount = getDemoCount(item.demo);
|
|
1013
|
+
const isMultiDemo = hasMultipleDemos(item.demo);
|
|
1014
|
+
const isExpanded = this.expandedElements.has(elementName);
|
|
1015
|
+
const isSelected = this.selectedItem === item;
|
|
1016
|
+
const isPinned = this.isPinned(section.name, elementName);
|
|
1017
|
+
const itemKey = this.getItemKey(section.name, groupName, elementName);
|
|
1018
|
+
|
|
1019
|
+
if (isMultiDemo) {
|
|
1020
|
+
// Multi-demo element - render as expandable folder
|
|
1021
|
+
return html`
|
|
1022
|
+
<div
|
|
1023
|
+
class="treeitem selectOption folder ${isExpanded ? 'expanded' : ''} ${isSelected ? 'selected' : ''} ${isPinned ? 'pinned' : ''}"
|
|
1024
|
+
role="treeitem"
|
|
1025
|
+
tabindex="-1"
|
|
1026
|
+
aria-level="2"
|
|
1027
|
+
aria-expanded=${isExpanded ? 'true' : 'false'}
|
|
1028
|
+
data-key=${itemKey}
|
|
1029
|
+
title=${elementName}
|
|
1030
|
+
@click=${() => this.toggleExpanded(elementName)}
|
|
1031
|
+
@contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, elementName)}
|
|
1032
|
+
>
|
|
1033
|
+
<i class="material-symbols-outlined expand-icon" aria-hidden="true">chevron_right</i>
|
|
1034
|
+
<div class="text">${this.highlightMatch(elementName)}</div>
|
|
1035
|
+
</div>
|
|
1036
|
+
${isExpanded ? html`
|
|
1037
|
+
<div class="demo-children" role="group" aria-label=${`${elementName} demos`}>
|
|
1038
|
+
${Array.from({ length: demoCount }, (_, demoIndex) => {
|
|
1039
|
+
const isThisDemoSelected = isSelected && this.dashboardRef.selectedDemoIndex === demoIndex;
|
|
1040
|
+
return html`
|
|
1041
|
+
<div
|
|
1042
|
+
class="treeitem demo-child ${isThisDemoSelected ? 'selected' : ''}"
|
|
1043
|
+
role="treeitem"
|
|
1044
|
+
tabindex="-1"
|
|
1045
|
+
aria-level="3"
|
|
1046
|
+
aria-selected=${isThisDemoSelected ? 'true' : 'false'}
|
|
1047
|
+
aria-current=${isThisDemoSelected ? 'page' : 'false'}
|
|
1048
|
+
data-key=${`${itemKey}::demo${demoIndex}`}
|
|
1049
|
+
@click=${async () => {
|
|
1050
|
+
await plugins.deesDomtools.DomTools.setupDomTools();
|
|
1051
|
+
this.selectItem('element', elementName, item, demoIndex, section);
|
|
1052
|
+
}}
|
|
1053
|
+
>
|
|
1054
|
+
<i class="material-symbols-outlined" aria-hidden="true">play_circle</i>
|
|
1055
|
+
<div class="text">demo${demoIndex + 1}</div>
|
|
1056
|
+
</div>
|
|
1057
|
+
`;
|
|
1058
|
+
})}
|
|
1059
|
+
</div>
|
|
1060
|
+
` : null}
|
|
1061
|
+
`;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Single demo element
|
|
1065
|
+
const icon = getElementDemoGroups(item).length > 1 ? 'library_books' : 'featured_video';
|
|
1066
|
+
return html`
|
|
1067
|
+
<div
|
|
1068
|
+
class="treeitem selectOption ${isSelected ? 'selected' : ''} ${isPinned ? 'pinned' : ''}"
|
|
1069
|
+
role="treeitem"
|
|
1070
|
+
tabindex="-1"
|
|
1071
|
+
aria-level="2"
|
|
1072
|
+
aria-selected=${isSelected ? 'true' : 'false'}
|
|
1073
|
+
aria-current=${isSelected ? 'page' : 'false'}
|
|
1074
|
+
data-key=${itemKey}
|
|
1075
|
+
title=${elementName}
|
|
1076
|
+
@click=${async () => {
|
|
1077
|
+
await plugins.deesDomtools.DomTools.setupDomTools();
|
|
1078
|
+
this.selectItem('element', elementName, item, 0, section);
|
|
1079
|
+
}}
|
|
1080
|
+
@contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, elementName)}
|
|
1081
|
+
>
|
|
1082
|
+
<i class="material-symbols-outlined" aria-hidden="true">${icon}</i>
|
|
1083
|
+
<div class="text">${this.highlightMatch(elementName)}</div>
|
|
1084
|
+
</div>
|
|
1085
|
+
`;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
private toggleSectionCollapsed(sectionName: string) {
|
|
1089
|
+
const newSet = new Set(this.collapsedSections);
|
|
1090
|
+
if (newSet.has(sectionName)) {
|
|
1091
|
+
newSet.delete(sectionName);
|
|
1092
|
+
} else {
|
|
1093
|
+
newSet.add(sectionName);
|
|
1094
|
+
}
|
|
1095
|
+
this.collapsedSections = newSet;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
private toggleExpanded(elementName: string) {
|
|
1099
|
+
const newSet = new Set(this.expandedElements);
|
|
1100
|
+
if (newSet.has(elementName)) {
|
|
1101
|
+
newSet.delete(elementName);
|
|
1102
|
+
} else {
|
|
1103
|
+
newSet.add(elementName);
|
|
1104
|
+
}
|
|
1105
|
+
this.expandedElements = newSet;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
private toggleWithoutDemo(sectionName: string) {
|
|
1109
|
+
if (this.searchQuery) {
|
|
1110
|
+
// Search keeps the group open while it has matches
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
const newSet = new Set(this.expandedWithoutDemo);
|
|
1114
|
+
if (newSet.has(sectionName)) {
|
|
1115
|
+
newSet.delete(sectionName);
|
|
1116
|
+
} else {
|
|
1117
|
+
newSet.add(sectionName);
|
|
1118
|
+
}
|
|
1119
|
+
this.expandedWithoutDemo = newSet;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
private handleSearchInput(e: Event) {
|
|
1123
|
+
const input = e.target as HTMLInputElement;
|
|
1124
|
+
this.searchQuery = input.value;
|
|
1125
|
+
this.dispatchEvent(new CustomEvent('searchChanged', { detail: this.searchQuery }));
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
private handleSearchKeydown(e: KeyboardEvent) {
|
|
1129
|
+
if (e.key === 'ArrowDown') {
|
|
1130
|
+
const target = this.getRovingTreeItem();
|
|
1131
|
+
if (target) {
|
|
1132
|
+
e.preventDefault();
|
|
1133
|
+
this.focusTreeItem(target);
|
|
1134
|
+
}
|
|
1135
|
+
} else if (e.key === 'Escape' && this.searchQuery) {
|
|
1136
|
+
e.preventDefault();
|
|
1137
|
+
this.clearSearch();
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
private clearSearch() {
|
|
1142
|
+
this.searchQuery = '';
|
|
1143
|
+
this.dispatchEvent(new CustomEvent('searchChanged', { detail: this.searchQuery }));
|
|
1144
|
+
const input = this.shadowRoot.querySelector('.search-input') as HTMLInputElement;
|
|
1145
|
+
if (input) input.focus();
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
private handleMenuScroll(e: Event) {
|
|
1149
|
+
const target = e.target as HTMLElement;
|
|
1150
|
+
this.isMenuScrolled = target.scrollTop > 0;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
private matchesSearch(name: string): boolean {
|
|
1154
|
+
if (!this.searchQuery) return true;
|
|
1155
|
+
return name.toLowerCase().includes(this.searchQuery.toLowerCase());
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/** An entry matches by name, registered tag name, or one of its demo groups. */
|
|
1159
|
+
private entryMatchesSearch(name: string, item: unknown): boolean {
|
|
1160
|
+
if (this.matchesSearch(name)) return true;
|
|
1161
|
+
const tagName = getElementTagName(item);
|
|
1162
|
+
if (tagName && this.matchesSearch(tagName)) return true;
|
|
1163
|
+
return getElementDemoGroups(item).some((groupName) => this.matchesSearch(groupName));
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
private isGroupFilterMatch(groupName: string): boolean {
|
|
1167
|
+
return !!this.searchQuery && groupName.toLowerCase() === this.searchQuery.toLowerCase();
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
private scrollToGroup(sectionName: string, groupName: string) {
|
|
1171
|
+
// Ensure the section is not collapsed
|
|
1172
|
+
this.collapsedSections.delete(sectionName);
|
|
1173
|
+
// Clear any active search so all groups are visible
|
|
1174
|
+
this.searchQuery = '';
|
|
1175
|
+
this.requestUpdate();
|
|
1176
|
+
|
|
1177
|
+
// After render, scroll to the group element
|
|
1178
|
+
this.updateComplete.then(() => {
|
|
1179
|
+
const groupEl = this.shadowRoot?.querySelector(
|
|
1180
|
+
`.item-group[data-group="${groupName}"]`
|
|
1181
|
+
);
|
|
1182
|
+
if (groupEl && this.isConnected) {
|
|
1183
|
+
groupEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
1184
|
+
// Brief highlight flash
|
|
1185
|
+
this.endGroupHighlight();
|
|
1186
|
+
groupEl.classList.add('group-highlight');
|
|
1187
|
+
this.highlightedGroup = groupEl;
|
|
1188
|
+
this.groupHighlightTimeout = setTimeout(() => this.endGroupHighlight(), 1500);
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
private endGroupHighlight() {
|
|
1194
|
+
clearTimeout(this.groupHighlightTimeout);
|
|
1195
|
+
this.highlightedGroup?.classList.remove('group-highlight');
|
|
1196
|
+
this.highlightedGroup = null;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
private highlightMatch(text: string): TemplateResult {
|
|
1200
|
+
if (!this.searchQuery) return html`${text}`;
|
|
1201
|
+
const lowerText = text.toLowerCase();
|
|
1202
|
+
const lowerQuery = this.searchQuery.toLowerCase();
|
|
1203
|
+
const index = lowerText.indexOf(lowerQuery);
|
|
1204
|
+
if (index === -1) return html`${text}`;
|
|
1205
|
+
const before = text.slice(0, index);
|
|
1206
|
+
const match = text.slice(index, index + this.searchQuery.length);
|
|
1207
|
+
const after = text.slice(index + this.searchQuery.length);
|
|
1208
|
+
return html`${before}<span class="highlight">${match}</span>${after}`;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// ============ Keyboard navigation (ARIA tree, roving tabindex) ============
|
|
1212
|
+
|
|
1213
|
+
private getTreeItems(): HTMLElement[] {
|
|
1214
|
+
return Array.from(this.shadowRoot?.querySelectorAll<HTMLElement>('[role="treeitem"]') ?? []);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
private getVisibleTreeItems(): HTMLElement[] {
|
|
1218
|
+
return this.getTreeItems().filter((itemArg) => itemArg.getClientRects().length > 0);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/** The tree item that should hold the tab stop: last focused, else selected, else first. */
|
|
1222
|
+
private getRovingTreeItem(): HTMLElement | null {
|
|
1223
|
+
const visible = this.getVisibleTreeItems();
|
|
1224
|
+
return visible.find((itemArg) => itemArg.dataset.key === this.focusedKey)
|
|
1225
|
+
?? visible.find((itemArg) => itemArg.getAttribute('aria-selected') === 'true')
|
|
1226
|
+
?? visible[0]
|
|
1227
|
+
?? null;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
private syncRovingTabindex() {
|
|
1231
|
+
const active = this.getRovingTreeItem();
|
|
1232
|
+
for (const item of this.getTreeItems()) {
|
|
1233
|
+
item.tabIndex = item === active ? 0 : -1;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
private focusTreeItem(itemArg: HTMLElement) {
|
|
1238
|
+
this.focusedKey = itemArg.dataset.key ?? null;
|
|
1239
|
+
for (const item of this.getTreeItems()) {
|
|
1240
|
+
item.tabIndex = item === itemArg ? 0 : -1;
|
|
1241
|
+
}
|
|
1242
|
+
itemArg.focus();
|
|
1243
|
+
itemArg.scrollIntoView({ block: 'nearest' });
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
private handleTreeFocusin(e: FocusEvent) {
|
|
1247
|
+
const item = (e.target as HTMLElement).closest<HTMLElement>('[role="treeitem"]');
|
|
1248
|
+
if (item) {
|
|
1249
|
+
this.focusedKey = item.dataset.key ?? null;
|
|
1250
|
+
for (const other of this.getTreeItems()) {
|
|
1251
|
+
other.tabIndex = other === item ? 0 : -1;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
private handleTreeKeydown(e: KeyboardEvent) {
|
|
1257
|
+
const current = (e.target as HTMLElement).closest<HTMLElement>('[role="treeitem"]');
|
|
1258
|
+
if (!current) return;
|
|
1259
|
+
const items = this.getVisibleTreeItems();
|
|
1260
|
+
const index = items.indexOf(current);
|
|
1261
|
+
const level = Number(current.getAttribute('aria-level'));
|
|
1262
|
+
const expanded = current.getAttribute('aria-expanded');
|
|
1263
|
+
|
|
1264
|
+
// The menu key and Shift+F10 open the entry's context menu, anchored to the entry
|
|
1265
|
+
if (e.key === 'ContextMenu' || (e.key === 'F10' && e.shiftKey)) {
|
|
1266
|
+
e.preventDefault();
|
|
1267
|
+
const rect = current.getBoundingClientRect();
|
|
1268
|
+
current.dispatchEvent(new MouseEvent('contextmenu', {
|
|
1269
|
+
bubbles: true,
|
|
1270
|
+
composed: true,
|
|
1271
|
+
cancelable: true,
|
|
1272
|
+
clientX: rect.left,
|
|
1273
|
+
clientY: rect.bottom,
|
|
1274
|
+
}));
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
switch (e.key) {
|
|
1279
|
+
case 'ArrowDown':
|
|
1280
|
+
if (items[index + 1]) this.focusTreeItem(items[index + 1]);
|
|
1281
|
+
break;
|
|
1282
|
+
case 'ArrowUp':
|
|
1283
|
+
if (index > 0) this.focusTreeItem(items[index - 1]);
|
|
1284
|
+
break;
|
|
1285
|
+
case 'Home':
|
|
1286
|
+
if (items[0]) this.focusTreeItem(items[0]);
|
|
1287
|
+
break;
|
|
1288
|
+
case 'End':
|
|
1289
|
+
if (items.length > 0) this.focusTreeItem(items[items.length - 1]);
|
|
1290
|
+
break;
|
|
1291
|
+
case 'ArrowRight':
|
|
1292
|
+
if (expanded === 'false') {
|
|
1293
|
+
current.click();
|
|
1294
|
+
} else if (expanded === 'true') {
|
|
1295
|
+
const next = items[index + 1];
|
|
1296
|
+
if (next && Number(next.getAttribute('aria-level')) > level) this.focusTreeItem(next);
|
|
1297
|
+
}
|
|
1298
|
+
break;
|
|
1299
|
+
case 'ArrowLeft':
|
|
1300
|
+
if (expanded === 'true') {
|
|
1301
|
+
current.click();
|
|
1302
|
+
} else {
|
|
1303
|
+
const parent = items.slice(0, index).reverse()
|
|
1304
|
+
.find((itemArg) => Number(itemArg.getAttribute('aria-level')) < level);
|
|
1305
|
+
if (parent) this.focusTreeItem(parent);
|
|
1306
|
+
}
|
|
1307
|
+
break;
|
|
1308
|
+
case 'Enter':
|
|
1309
|
+
case ' ':
|
|
1310
|
+
if (current.getAttribute('aria-disabled') !== 'true') {
|
|
1311
|
+
current.click();
|
|
1312
|
+
}
|
|
1313
|
+
break;
|
|
1314
|
+
default:
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
e.preventDefault();
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
protected updated(changedProperties: Map<string, unknown>) {
|
|
1321
|
+
super.updated(changedProperties);
|
|
1322
|
+
|
|
1323
|
+
// Handle delayed hide for native mode transition
|
|
1324
|
+
if (changedProperties.has('isNative')) {
|
|
1325
|
+
// A hide still pending from an earlier switch to native mode no longer applies
|
|
1326
|
+
clearTimeout(this.hideTimeout);
|
|
1327
|
+
if (this.isNative && this.isConnected) {
|
|
1328
|
+
// Delay hiding until frame animation completes
|
|
1329
|
+
this.hideTimeout = setTimeout(() => {
|
|
1330
|
+
this.isHidden = true;
|
|
1331
|
+
}, 300);
|
|
1332
|
+
} else {
|
|
1333
|
+
// Show immediately when exiting native mode; a removed sidebar has nothing to wait for
|
|
1334
|
+
this.isHidden = this.isNative;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// Auto-expand folder when a multi-demo element is selected
|
|
1339
|
+
if (changedProperties.has('selectedItem') && this.selectedItem && this.dashboardRef?.sections) {
|
|
1340
|
+
for (const section of this.dashboardRef.sections) {
|
|
1341
|
+
if (section.type !== 'elements') continue;
|
|
1342
|
+
|
|
1343
|
+
const found = getSectionItems(section).find(([_, item]) => item === this.selectedItem);
|
|
1344
|
+
if (found) {
|
|
1345
|
+
const [elementName, item] = found;
|
|
1346
|
+
if (hasMultipleDemos(item.demo) && !this.expandedElements.has(elementName)) {
|
|
1347
|
+
const newSet = new Set(this.expandedElements);
|
|
1348
|
+
newSet.add(elementName);
|
|
1349
|
+
this.expandedElements = newSet;
|
|
1350
|
+
}
|
|
1351
|
+
break;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
this.syncRovingTabindex();
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// ============ Resize functionality ============
|
|
1360
|
+
|
|
1361
|
+
/** Apply a sidebar width to the sidebar, frame and properties panel. */
|
|
1362
|
+
private applyWidth(widthArg: number) {
|
|
1363
|
+
const newWidth = Math.min(SIDEBAR_MAX_WIDTH, Math.max(SIDEBAR_MIN_WIDTH, Math.round(widthArg)));
|
|
1364
|
+
this.sidebarWidth = newWidth;
|
|
1365
|
+
const frame = this.dashboardRef?.shadowRoot?.querySelector('wcc-frame');
|
|
1366
|
+
const properties = this.dashboardRef?.shadowRoot?.querySelector<WccProperties>('wcc-properties');
|
|
1367
|
+
if (frame) {
|
|
1368
|
+
frame.sidebarWidth = newWidth;
|
|
1369
|
+
}
|
|
1370
|
+
if (properties) {
|
|
1371
|
+
properties.sidebarWidth = newWidth;
|
|
1372
|
+
}
|
|
1373
|
+
return newWidth;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
private handleResizeKeydown(e: KeyboardEvent) {
|
|
1377
|
+
const step = e.shiftKey ? 50 : 10;
|
|
1378
|
+
let target: number;
|
|
1379
|
+
switch (e.key) {
|
|
1380
|
+
case 'ArrowLeft':
|
|
1381
|
+
target = this.sidebarWidth - step;
|
|
1382
|
+
break;
|
|
1383
|
+
case 'ArrowRight':
|
|
1384
|
+
target = this.sidebarWidth + step;
|
|
1385
|
+
break;
|
|
1386
|
+
case 'Home':
|
|
1387
|
+
target = SIDEBAR_MIN_WIDTH;
|
|
1388
|
+
break;
|
|
1389
|
+
case 'End':
|
|
1390
|
+
target = SIDEBAR_MAX_WIDTH;
|
|
1391
|
+
break;
|
|
1392
|
+
default:
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
e.preventDefault();
|
|
1396
|
+
const newWidth = this.applyWidth(target);
|
|
1397
|
+
this.dispatchEvent(new CustomEvent('widthChanged', { detail: newWidth }));
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
private startResize = (e: MouseEvent) => {
|
|
1401
|
+
e.preventDefault();
|
|
1402
|
+
this.isResizing = true;
|
|
1403
|
+
const startX = e.clientX;
|
|
1404
|
+
const startWidth = this.sidebarWidth;
|
|
1405
|
+
|
|
1406
|
+
const frame = this.dashboardRef?.shadowRoot?.querySelector('wcc-frame');
|
|
1407
|
+
|
|
1408
|
+
// Disable frame transition during resize
|
|
1409
|
+
if (frame) {
|
|
1410
|
+
frame.isResizing = true;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
const onMouseMove = (e: MouseEvent) => {
|
|
1414
|
+
this.applyWidth(startWidth + (e.clientX - startX));
|
|
1415
|
+
};
|
|
1416
|
+
|
|
1417
|
+
const onMouseUp = () => {
|
|
1418
|
+
this.isResizing = false;
|
|
1419
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
1420
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
1421
|
+
// Re-enable frame transition
|
|
1422
|
+
if (frame) {
|
|
1423
|
+
frame.isResizing = false;
|
|
1424
|
+
}
|
|
1425
|
+
// Dispatch event on release for URL persistence
|
|
1426
|
+
this.dispatchEvent(new CustomEvent('widthChanged', { detail: this.sidebarWidth }));
|
|
1427
|
+
};
|
|
1428
|
+
|
|
1429
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
1430
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
1431
|
+
};
|
|
1432
|
+
|
|
1433
|
+
public selectItem(
|
|
1434
|
+
typeArg: TElementType,
|
|
1435
|
+
itemNameArg: string,
|
|
1436
|
+
itemArg: TTemplateFactory | DeesElement,
|
|
1437
|
+
demoIndex: number = 0,
|
|
1438
|
+
section?: IWccSection
|
|
1439
|
+
) {
|
|
1440
|
+
this.selectedItem = itemArg;
|
|
1441
|
+
this.selectedType = typeArg;
|
|
1442
|
+
this.dashboardRef.selectedDemoIndex = demoIndex;
|
|
1443
|
+
|
|
1444
|
+
// Set the selected section on dashboard
|
|
1445
|
+
if (section) {
|
|
1446
|
+
this.dashboardRef.selectedSection = section;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
this.dispatchEvent(
|
|
1450
|
+
new CustomEvent('selectedType', {
|
|
1451
|
+
detail: typeArg
|
|
1452
|
+
})
|
|
1453
|
+
);
|
|
1454
|
+
this.dispatchEvent(
|
|
1455
|
+
new CustomEvent('selectedItemName', {
|
|
1456
|
+
detail: itemNameArg
|
|
1457
|
+
})
|
|
1458
|
+
);
|
|
1459
|
+
this.dispatchEvent(
|
|
1460
|
+
new CustomEvent('selectedItem', {
|
|
1461
|
+
detail: itemArg
|
|
1462
|
+
})
|
|
1463
|
+
);
|
|
1464
|
+
|
|
1465
|
+
this.dashboardRef.buildUrl();
|
|
1466
|
+
|
|
1467
|
+
// Force re-render to update demo child selection indicator
|
|
1468
|
+
this.requestUpdate();
|
|
1469
|
+
}
|
|
1470
|
+
}
|