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