@kispace-io/extension-howto-system 0.8.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.
@@ -0,0 +1,1058 @@
1
+ import { css, html, nothing } from 'lit';
2
+ import { customElement, state } from 'lit/decorators.js';
3
+ import { PropertyValues } from 'lit';
4
+ import { styleMap } from 'lit/directives/style-map.js';
5
+ import { KWidget } from '@kispace-io/core';
6
+ import { howToService } from './howto-service';
7
+ import { HowToContribution, HowToStep } from './howto-contribution';
8
+ import { subscribe } from '@kispace-io/core';
9
+ import { commandRegistry } from '@kispace-io/core';
10
+ import { toastError, toastInfo } from '@kispace-io/core';
11
+ import { createRef, ref, Ref } from 'lit/directives/ref.js';
12
+ import { TOPIC_CONTRIBUTEIONS_CHANGED } from '@kispace-io/core';
13
+ import { HOWTO_CONTRIBUTION_TARGET } from './howto-service';
14
+ import { appSettings } from '@kispace-io/core';
15
+ import { workspaceService, TOPIC_WORKSPACE_CONNECTED } from '@kispace-io/core';
16
+
17
+ // Event topic for showing the HowTo panel
18
+ export const TOPIC_SHOW_HOWTO_PANEL = 'howto/show-panel';
19
+ export const TOPIC_TOGGLE_HOWTO_PANEL = 'howto/toggle-panel';
20
+
21
+ // Constants
22
+ const SETTINGS_KEY = 'k-howto-panel';
23
+ const DEFAULT_POSITION = { x: 100, y: 100 };
24
+ const DEFAULT_PANEL_SIZE = { width: 400, height: 300 };
25
+
26
+ interface StepState {
27
+ step: HowToStep;
28
+ status: 'pending' | 'active' | 'completed' | 'skipped' | 'failed';
29
+ preConditionMet?: boolean;
30
+ postConditionMet?: boolean;
31
+ }
32
+
33
+ @customElement('k-howto-panel')
34
+ export class KHowToPanel extends KWidget {
35
+ @state()
36
+ private contributions: HowToContribution[] = [];
37
+
38
+ @state()
39
+ private activeContributionId: string | null = null;
40
+
41
+ @state()
42
+ private stepStates: Map<string, StepState[]> = new Map();
43
+
44
+ @state()
45
+ private isMinimized: boolean = false;
46
+
47
+ @state()
48
+ private isVisible: boolean = false;
49
+
50
+ @state()
51
+ private positionX: number = DEFAULT_POSITION.x;
52
+
53
+ @state()
54
+ private positionY: number = DEFAULT_POSITION.y;
55
+
56
+ private isDragging: boolean = false;
57
+
58
+ @state()
59
+ private dragPreviewPosition: { x: number; y: number } | null = null;
60
+
61
+ private dragStartPosition: { x: number; y: number } = { x: 0, y: 0 };
62
+
63
+ private panelRef: Ref<HTMLElement> = createRef();
64
+ private howtoCleanup?: () => void;
65
+
66
+ protected async doBeforeUI() {
67
+ this.loadContributions();
68
+
69
+ subscribe(TOPIC_CONTRIBUTEIONS_CHANGED, (event: any) => {
70
+ if (event.target === HOWTO_CONTRIBUTION_TARGET) {
71
+ this.loadContributions();
72
+ this.requestUpdate();
73
+ }
74
+ });
75
+
76
+ // Subscribe to show/toggle panel events
77
+ subscribe(TOPIC_SHOW_HOWTO_PANEL, () => this.showPanel());
78
+ subscribe(TOPIC_TOGGLE_HOWTO_PANEL, () => this.toggleVisibility());
79
+
80
+ // Load position and visibility from dialogsettings
81
+ await this.loadSettings();
82
+
83
+ // If no workspace is selected and panel wasn't explicitly hidden, show it by default
84
+ const hasWorkspace = workspaceService.isConnected();
85
+ if (!hasWorkspace && this.isVisible === false) {
86
+ // Check if user has explicitly hidden it (if settings exist and visible is false)
87
+ const settings = await appSettings.getDialogSetting(SETTINGS_KEY);
88
+ if (!settings || settings.visible === undefined) {
89
+ // No explicit preference, show panel when no workspace
90
+ this.isVisible = true;
91
+ await this.saveSettings();
92
+ }
93
+ }
94
+
95
+ // Subscribe to workspace connection to hide panel when workspace is connected (if it was auto-shown)
96
+ subscribe(TOPIC_WORKSPACE_CONNECTED, () => {
97
+ // Only auto-hide if it was auto-shown (no explicit visibility setting)
98
+ const checkAutoHide = async () => {
99
+ const settings = await appSettings.getDialogSetting(SETTINGS_KEY);
100
+ if (!settings || settings.visible === undefined) {
101
+ // Was auto-shown, can auto-hide when workspace connects
102
+ // But let's keep it visible - user might want to see howtos even with workspace
103
+ }
104
+ };
105
+ checkAutoHide();
106
+ });
107
+ }
108
+
109
+ private boundHandleDragMove?: (e: MouseEvent) => void;
110
+ private boundHandleDragEnd?: () => void;
111
+
112
+ protected doInitUI() {
113
+ // Setup document-level drag handlers (these are always available)
114
+ this.boundHandleDragMove = this.handleDragMove.bind(this);
115
+ this.boundHandleDragEnd = this.handleDragEnd.bind(this);
116
+ document.addEventListener('mousemove', this.boundHandleDragMove);
117
+ document.addEventListener('mouseup', this.boundHandleDragEnd);
118
+ }
119
+
120
+ protected firstUpdated(_changedProperties: PropertyValues) {
121
+ super.firstUpdated(_changedProperties);
122
+ }
123
+
124
+ private loadContributions() {
125
+ this.contributions = howToService.getAllContributions();
126
+ }
127
+
128
+ private async loadSettings() {
129
+ const settings = await appSettings.getDialogSetting(SETTINGS_KEY);
130
+ if (settings) {
131
+ if (settings.position) {
132
+ this.positionX = settings.position.x || DEFAULT_POSITION.x;
133
+ this.positionY = settings.position.y || DEFAULT_POSITION.y;
134
+ }
135
+ if (settings.visible !== undefined) {
136
+ this.isVisible = settings.visible;
137
+ }
138
+ }
139
+ }
140
+
141
+ private async saveSettings() {
142
+ await appSettings.setDialogSetting(SETTINGS_KEY, {
143
+ position: { x: this.positionX, y: this.positionY },
144
+ visible: this.isVisible
145
+ });
146
+ }
147
+
148
+ private handleDragStart = (e: MouseEvent) => {
149
+ const target = e.target as HTMLElement;
150
+ if (this.isDragTarget(target) || !this.panelRef.value) {
151
+ return;
152
+ }
153
+
154
+ const rect = this.panelRef.value.getBoundingClientRect();
155
+ this.dragStartPosition = {
156
+ x: e.clientX - rect.left,
157
+ y: e.clientY - rect.top
158
+ };
159
+ this.isDragging = true;
160
+ this.dragPreviewPosition = { x: this.positionX, y: this.positionY };
161
+ this.requestUpdate();
162
+ e.preventDefault();
163
+ e.stopPropagation();
164
+ }
165
+
166
+ private isDragTarget(target: HTMLElement): boolean {
167
+ return !!(target.closest('.header-actions') || target.closest('wa-button'));
168
+ }
169
+
170
+ private getViewportBounds() {
171
+ const panelWidth = this.panelRef.value?.offsetWidth || DEFAULT_PANEL_SIZE.width;
172
+ const panelHeight = this.panelRef.value?.offsetHeight || DEFAULT_PANEL_SIZE.height;
173
+ return {
174
+ maxX: window.innerWidth - panelWidth,
175
+ maxY: window.innerHeight - panelHeight
176
+ };
177
+ }
178
+
179
+ private handleDragMove = (e: MouseEvent) => {
180
+ if (!this.isDragging || !this.dragPreviewPosition) {
181
+ return;
182
+ }
183
+
184
+ const bounds = this.getViewportBounds();
185
+ const newX = Math.max(0, Math.min(e.clientX - this.dragStartPosition.x, bounds.maxX));
186
+ const newY = Math.max(0, Math.min(e.clientY - this.dragStartPosition.y, bounds.maxY));
187
+
188
+ if (this.dragPreviewPosition.x !== newX || this.dragPreviewPosition.y !== newY) {
189
+ this.dragPreviewPosition = { x: newX, y: newY };
190
+ this.requestUpdate();
191
+ }
192
+ }
193
+
194
+ private handleDragEnd = async () => {
195
+ if (!this.isDragging || !this.dragPreviewPosition) {
196
+ return;
197
+ }
198
+
199
+ this.isDragging = false;
200
+ const previewPos = this.dragPreviewPosition;
201
+ this.dragPreviewPosition = null;
202
+
203
+ this.positionX = previewPos.x;
204
+ this.positionY = previewPos.y;
205
+
206
+ await this.saveSettings();
207
+ this.requestUpdate();
208
+ }
209
+
210
+ private async startHowTo(contributionId: string) {
211
+ const contribution = howToService.getContribution(contributionId);
212
+ if (!contribution) {
213
+ toastError(`HowTo "${contributionId}" not found`);
214
+ return;
215
+ }
216
+
217
+ // Clean up previous HowTo
218
+ this.cleanupHowTo();
219
+
220
+ this.activeContributionId = contributionId;
221
+ this.isMinimized = false;
222
+
223
+ // Initialize step states
224
+ const states: StepState[] = contribution.steps.map(step => ({
225
+ step,
226
+ status: 'pending'
227
+ }));
228
+
229
+ this.stepStates.set(contributionId, states);
230
+
231
+ // Call HowTo's initialize function if provided
232
+ if (contribution.initialize) {
233
+ const context = {
234
+ requestUpdate: () => this.recheckActiveStepConditions(),
235
+ contributionId: contributionId
236
+ };
237
+ this.howtoCleanup = contribution.initialize(context) || undefined;
238
+ }
239
+
240
+ this.requestUpdate();
241
+
242
+ // Check initial pre-conditions
243
+ await this.checkPreConditions(contributionId, 0);
244
+ }
245
+
246
+ private cleanupHowTo() {
247
+ if (this.howtoCleanup) {
248
+ this.howtoCleanup();
249
+ this.howtoCleanup = undefined;
250
+ }
251
+ }
252
+
253
+ private getStepState(contributionId: string, stepIndex: number): StepState | null {
254
+ const states = this.stepStates.get(contributionId);
255
+ return states && stepIndex < states.length ? states[stepIndex] : null;
256
+ }
257
+
258
+ private async checkPreConditions(contributionId: string, stepIndex: number) {
259
+ const state = this.getStepState(contributionId, stepIndex);
260
+ if (!state) return;
261
+
262
+ if (!state.step.preCondition) {
263
+ state.preConditionMet = true;
264
+ this.requestUpdate();
265
+ return;
266
+ }
267
+
268
+ try {
269
+ state.preConditionMet = await state.step.preCondition();
270
+ this.requestUpdate();
271
+ } catch (error) {
272
+ console.error(`Pre-condition check failed for step ${state.step.id}:`, error);
273
+ state.preConditionMet = false;
274
+ this.requestUpdate();
275
+ }
276
+ }
277
+
278
+ private async checkPostConditions(contributionId: string, stepIndex: number) {
279
+ const state = this.getStepState(contributionId, stepIndex);
280
+ if (!state) return;
281
+
282
+ if (!state.step.postCondition) {
283
+ this.completeStep(contributionId, stepIndex);
284
+ return;
285
+ }
286
+
287
+ try {
288
+ const result = await state.step.postCondition();
289
+ state.postConditionMet = result;
290
+ state.status = result ? 'completed' : 'failed';
291
+
292
+ if (result) {
293
+ this.activateNextStep(contributionId, stepIndex);
294
+ }
295
+ this.requestUpdate();
296
+ } catch (error) {
297
+ console.error(`Post-condition check failed for step ${state.step.id}:`, error);
298
+ state.postConditionMet = false;
299
+ state.status = 'failed';
300
+ this.requestUpdate();
301
+ }
302
+ }
303
+
304
+ private completeStep(contributionId: string, stepIndex: number) {
305
+ const state = this.getStepState(contributionId, stepIndex);
306
+ if (!state) return;
307
+
308
+ state.status = 'completed';
309
+ this.activateNextStep(contributionId, stepIndex);
310
+ this.requestUpdate();
311
+ }
312
+
313
+ private async activateNextStep(contributionId: string, stepIndex: number) {
314
+ const states = this.stepStates.get(contributionId);
315
+ if (!states || stepIndex + 1 >= states.length) return;
316
+
317
+ const nextState = states[stepIndex + 1];
318
+ nextState.status = 'active';
319
+ await this.checkPreConditions(contributionId, stepIndex + 1);
320
+ }
321
+
322
+ /**
323
+ * Re-checks conditions for active and pending steps when workspace or editor state changes
324
+ */
325
+ private async recheckActiveStepConditions() {
326
+ if (!this.activeContributionId) return;
327
+
328
+ const states = this.stepStates.get(this.activeContributionId);
329
+ if (!states) return;
330
+
331
+ // Find the active step
332
+ const activeStepIndex = states.findIndex(state => state.status === 'active');
333
+
334
+ if (activeStepIndex !== -1) {
335
+ const activeState = states[activeStepIndex];
336
+ const step = activeState.step;
337
+
338
+ // Re-check post-condition if it exists and step is active
339
+ if (step.postCondition) {
340
+ try {
341
+ const result = await step.postCondition();
342
+ if (result && activeState.status === 'active') {
343
+ // Post-condition is met, complete the step and move to next
344
+ await this.checkPostConditions(this.activeContributionId, activeStepIndex);
345
+ return; // Step completed, no need to check pre-conditions
346
+ }
347
+ } catch (error) {
348
+ // Ignore errors in post-condition checks during re-evaluation
349
+ }
350
+ }
351
+ }
352
+
353
+ // Re-check pre-conditions for pending steps (they might become available)
354
+ for (let i = 0; i < states.length; i++) {
355
+ const state = states[i];
356
+ if (state.status === 'pending' && state.step.preCondition) {
357
+ // Re-check pre-condition for pending steps
358
+ await this.checkPreConditions(this.activeContributionId, i);
359
+ }
360
+ }
361
+
362
+ this.requestUpdate();
363
+ }
364
+
365
+ private async executeStep(contributionId: string, stepIndex: number) {
366
+ const state = this.getStepState(contributionId, stepIndex);
367
+ if (!state) return;
368
+
369
+ if (!await this.validatePreConditions(state, contributionId, stepIndex)) {
370
+ return;
371
+ }
372
+
373
+ state.status = 'active';
374
+ this.requestUpdate();
375
+
376
+ if (state.step.command && !await this.executeStepCommand(state)) {
377
+ return;
378
+ }
379
+
380
+ await this.checkPostConditions(contributionId, stepIndex);
381
+ }
382
+
383
+ private async validatePreConditions(state: StepState, contributionId: string, stepIndex: number): Promise<boolean> {
384
+ if (!state.step.preCondition) {
385
+ return true;
386
+ }
387
+
388
+ if (state.preConditionMet === undefined || state.preConditionMet === false) {
389
+ await this.checkPreConditions(contributionId, stepIndex);
390
+ }
391
+
392
+ if (state.preConditionMet !== true) {
393
+ toastError(`Pre-conditions not met for step: ${state.step.title}`);
394
+ return false;
395
+ }
396
+
397
+ return true;
398
+ }
399
+
400
+ private async executeStepCommand(state: StepState): Promise<boolean> {
401
+ if (!state.step.command) {
402
+ return true;
403
+ }
404
+
405
+ try {
406
+ const execContext = commandRegistry.createExecutionContext(state.step.commandParams || {});
407
+ await commandRegistry.execute(state.step.command, execContext);
408
+ return true;
409
+ } catch (error) {
410
+ console.error(`Failed to execute command for step ${state.step.id}:`, error);
411
+ toastError(`Failed to execute step: ${state.step.title}`);
412
+ state.status = 'failed';
413
+ this.requestUpdate();
414
+ return false;
415
+ }
416
+ }
417
+
418
+ private async runStepCommand(contributionId: string, stepIndex: number) {
419
+ const state = this.getStepState(contributionId, stepIndex);
420
+ if (!state || !state.step.command) {
421
+ return;
422
+ }
423
+
424
+ // Check pre-conditions if they exist
425
+ if (state.step.preCondition) {
426
+ const preConditionMet = await state.step.preCondition();
427
+ if (!preConditionMet) {
428
+ toastError(`Pre-conditions not met for step: ${state.step.title}`);
429
+ return;
430
+ }
431
+ }
432
+
433
+ // Execute the command
434
+ const success = await this.executeStepCommand(state);
435
+ if (success) {
436
+ // Check post-conditions after command execution
437
+ await this.checkPostConditions(contributionId, stepIndex);
438
+ }
439
+ }
440
+
441
+ private skipStep(contributionId: string, stepIndex: number) {
442
+ const state = this.getStepState(contributionId, stepIndex);
443
+ if (!state || !state.step.optional) return;
444
+
445
+ state.status = 'skipped';
446
+ this.activateNextStep(contributionId, stepIndex);
447
+ this.requestUpdate();
448
+ }
449
+
450
+ private closeHowTo() {
451
+ this.cleanupHowTo();
452
+ this.activeContributionId = null;
453
+ this.stepStates.clear();
454
+ this.requestUpdate();
455
+ }
456
+
457
+ private toggleMinimize() {
458
+ this.isMinimized = !this.isMinimized;
459
+ this.requestUpdate();
460
+ }
461
+
462
+ private async showPanel() {
463
+ this.isVisible = true;
464
+ this.isMinimized = false;
465
+ await this.saveSettings();
466
+ this.requestUpdate();
467
+ }
468
+
469
+ private async hidePanel() {
470
+ this.isVisible = false;
471
+ await this.saveSettings();
472
+ this.requestUpdate();
473
+ }
474
+
475
+ private async toggleVisibility() {
476
+ if (this.isVisible) {
477
+ await this.hidePanel();
478
+ } else {
479
+ await this.showPanel();
480
+ }
481
+ }
482
+
483
+ private renderStep(state: StepState, index: number, contributionId: string) {
484
+ const { step, status, preConditionMet, postConditionMet } = state;
485
+ const isActive = status === 'active';
486
+ const isCompleted = status === 'completed';
487
+ const isFailed = status === 'failed';
488
+ const isPending = status === 'pending';
489
+ const isSkipped = status === 'skipped';
490
+
491
+ return html`
492
+ <div class="step ${status}" ?data-active=${isActive}>
493
+ <div class="step-header">
494
+ <div class="step-number">${index + 1}</div>
495
+ <div class="step-title">${step.title}</div>
496
+ <div class="step-status">
497
+ ${step.command ? html`
498
+ <wa-button
499
+ size="small"
500
+ appearance="plain"
501
+ @click=${(e: Event) => {
502
+ e.stopPropagation();
503
+ this.runStepCommand(contributionId, index);
504
+ }}
505
+ title="Run step command"
506
+ >
507
+ <wa-icon name="play"></wa-icon>
508
+ </wa-button>
509
+ ` : nothing}
510
+ ${isCompleted ? html`<wa-icon name="check-circle" class="status-icon completed"></wa-icon>` : nothing}
511
+ ${isFailed ? html`<wa-icon name="xmark-circle" class="status-icon failed"></wa-icon>` : nothing}
512
+ ${isSkipped ? html`<wa-icon name="minus-circle" class="status-icon skipped"></wa-icon>` : nothing}
513
+ ${isPending ? html`<wa-icon name="circle" class="status-icon pending"></wa-icon>` : nothing}
514
+ ${isActive ? html`<wa-icon name="play-circle" class="status-icon active"></wa-icon>` : nothing}
515
+ </div>
516
+ </div>
517
+ <div class="step-description">${step.description}</div>
518
+ ${step.preCondition && preConditionMet !== undefined ? html`
519
+ <div class="condition pre-condition ${preConditionMet ? 'met' : 'not-met'}">
520
+ <wa-icon name="${preConditionMet ? 'check' : 'xmark'}"></wa-icon>
521
+ <span>Pre-condition: ${preConditionMet ? 'Met' : 'Not met'}</span>
522
+ </div>
523
+ ` : nothing}
524
+ ${step.postCondition && postConditionMet !== undefined ? html`
525
+ <div class="condition post-condition ${postConditionMet ? 'met' : 'not-met'}">
526
+ <wa-icon name="${postConditionMet ? 'check' : 'xmark'}"></wa-icon>
527
+ <span>Post-condition: ${postConditionMet ? 'Met' : 'Not met'}</span>
528
+ </div>
529
+ ` : nothing}
530
+ ${isActive && step.optional ? html`
531
+ <div class="step-actions">
532
+ <wa-button size="small" appearance="outline" @click=${() => this.skipStep(contributionId, index)}>
533
+ <wa-icon name="forward"></wa-icon>
534
+ Skip
535
+ </wa-button>
536
+ </div>
537
+ ` : nothing}
538
+ </div>
539
+ `;
540
+ }
541
+
542
+ render() {
543
+ if (!this.isVisible) {
544
+ return nothing;
545
+ }
546
+
547
+ const activeContribution = this.activeContributionId
548
+ ? howToService.getContribution(this.activeContributionId)
549
+ : null;
550
+
551
+ const activeStepStates = this.activeContributionId
552
+ ? this.stepStates.get(this.activeContributionId) || []
553
+ : [];
554
+
555
+ return html`
556
+ ${this.dragPreviewPosition ? html`
557
+ <div
558
+ class="howto-panel-drag-preview"
559
+ style=${styleMap({
560
+ left: `${this.dragPreviewPosition.x}px`,
561
+ top: `${this.dragPreviewPosition.y}px`,
562
+ width: `${this.panelRef.value?.offsetWidth || DEFAULT_PANEL_SIZE.width}px`,
563
+ height: `${this.panelRef.value?.offsetHeight || DEFAULT_PANEL_SIZE.height}px`,
564
+ display: 'block',
565
+ visibility: 'visible'
566
+ })}
567
+ ></div>
568
+ ` : nothing}
569
+ <div
570
+ class="howto-panel ${this.isMinimized ? 'minimized' : ''} ${this.dragPreviewPosition ? 'dragging' : ''}"
571
+ style=${styleMap({
572
+ left: `${this.positionX}px`,
573
+ top: `${this.positionY}px`,
574
+ transform: 'translateZ(0)'
575
+ })}
576
+ ${ref(this.panelRef)}
577
+ >
578
+ <div class="panel-header" @mousedown=${this.handleDragStart}>
579
+ <div class="header-title">
580
+ <wa-icon name="list-check"></wa-icon>
581
+ <span>HowTo Workflows</span>
582
+ </div>
583
+ <div class="header-actions" @mousedown=${(e: MouseEvent) => e.stopPropagation()}>
584
+ <wa-button
585
+ size="small"
586
+ appearance="plain"
587
+ @click=${this.toggleMinimize}
588
+ title="${this.isMinimized ? 'Expand' : 'Minimize'}"
589
+ >
590
+ <wa-icon name="${this.isMinimized ? 'chevron-up' : 'chevron-down'}"></wa-icon>
591
+ </wa-button>
592
+ <wa-button
593
+ size="small"
594
+ appearance="plain"
595
+ @click=${this.hidePanel}
596
+ title="Hide Panel"
597
+ >
598
+ <wa-icon name="xmark"></wa-icon>
599
+ </wa-button>
600
+ </div>
601
+ </div>
602
+
603
+ ${!this.isMinimized ? html`
604
+ <div class="panel-content">
605
+ ${activeContribution ? html`
606
+ <div class="active-workflow">
607
+ <div class="workflow-header">
608
+ <div class="workflow-title-section">
609
+ <h3>${typeof activeContribution.title === 'function'
610
+ ? activeContribution.title()
611
+ : activeContribution.title}</h3>
612
+ ${activeContribution.description ? html`
613
+ <p class="workflow-description">${typeof activeContribution.description === 'function'
614
+ ? activeContribution.description()
615
+ : activeContribution.description}</p>
616
+ ` : nothing}
617
+ </div>
618
+ <wa-button
619
+ size="small"
620
+ appearance="plain"
621
+ @click=${this.closeHowTo}
622
+ title="Close HowTo"
623
+ >
624
+ <wa-icon name="xmark"></wa-icon>
625
+ </wa-button>
626
+ </div>
627
+ <div class="steps-list">
628
+ ${activeStepStates.map((state, index) =>
629
+ this.renderStep(state, index, this.activeContributionId!)
630
+ )}
631
+ </div>
632
+ </div>
633
+ ` : html`
634
+ <div class="workflows-list">
635
+ <h3>Available Workflows</h3>
636
+ ${this.contributions.length === 0 ? html`
637
+ <div class="empty-state">
638
+ <wa-icon name="list-check" style="font-size: 2em; opacity: 0.5; margin-bottom: 12px;"></wa-icon>
639
+ <p>No HowTo workflows available yet.</p>
640
+ <p style="font-size: 0.9em; opacity: 0.7;">Extensions can register workflows via the contribution registry.</p>
641
+ </div>
642
+ ` : this.contributions.map(contrib => {
643
+ const title = typeof contrib.title === 'function' ? contrib.title() : contrib.title;
644
+ const description = contrib.description
645
+ ? (typeof contrib.description === 'function' ? contrib.description() : contrib.description)
646
+ : null;
647
+ return html`
648
+ <div class="workflow-item" @click=${() => this.startHowTo(contrib.id)}>
649
+ ${contrib.icon ? html`
650
+ <wa-icon name="${contrib.icon}"></wa-icon>
651
+ ` : html`
652
+ <wa-icon name="list-check"></wa-icon>
653
+ `}
654
+ <div class="workflow-info">
655
+ <div class="workflow-title">${title}</div>
656
+ ${description ? html`
657
+ <div class="workflow-desc">${description}</div>
658
+ ` : nothing}
659
+ <div class="workflow-meta">${contrib.steps.length} step${contrib.steps.length !== 1 ? 's' : ''}</div>
660
+ </div>
661
+ </div>
662
+ `})}
663
+ </div>
664
+ `}
665
+ </div>
666
+ ` : nothing}
667
+ </div>
668
+ `;
669
+ }
670
+
671
+ static styles = css`
672
+ :host {
673
+ display: block;
674
+ position: fixed;
675
+ z-index: 10000;
676
+ pointer-events: none;
677
+ }
678
+
679
+ .howto-panel-drag-preview {
680
+ position: fixed !important;
681
+ border: 3px dashed var(--wa-color-primary-50, #0066cc) !important;
682
+ background: var(--wa-color-primary-05, rgba(0, 102, 204, 0.05)) !important;
683
+ border-radius: var(--wa-border-radius-medium, 8px);
684
+ z-index: 10001 !important;
685
+ pointer-events: none !important;
686
+ opacity: 0.8 !important;
687
+ box-sizing: border-box;
688
+ display: block !important;
689
+ visibility: visible !important;
690
+ min-width: 100px;
691
+ min-height: 100px;
692
+ }
693
+
694
+ :host-context(.wa-light) .howto-panel-drag-preview {
695
+ background: var(--wa-color-primary-95);
696
+ border-color: var(--wa-color-primary-50);
697
+ }
698
+
699
+ .howto-panel {
700
+ position: fixed !important;
701
+ width: 400px;
702
+ max-height: 600px;
703
+ background: var(--wa-color-surface-raised, var(--wa-color-neutral-05));
704
+ border: var(--wa-border-width-s, 1px) solid var(--wa-color-neutral-border-loud, var(--wa-color-neutral-25));
705
+ border-radius: var(--wa-border-radius-medium, 8px);
706
+ box-shadow: var(--wa-shadow-large, 0 8px 24px rgba(0, 0, 0, 0.8));
707
+ pointer-events: all;
708
+ display: flex;
709
+ flex-direction: column;
710
+ overflow: hidden;
711
+ }
712
+
713
+ :host-context(.wa-light) .howto-panel {
714
+ background: var(--wa-color-surface-raised, var(--wa-color-neutral-95));
715
+ border-color: var(--wa-color-neutral-border-loud, var(--wa-color-neutral-75));
716
+ box-shadow: var(--wa-shadow-large, 0 8px 24px rgba(0, 0, 0, 0.2));
717
+ }
718
+
719
+ .howto-panel.minimized {
720
+ max-height: auto;
721
+ }
722
+
723
+ .howto-panel.dragging {
724
+ opacity: 0.5;
725
+ }
726
+
727
+ .panel-header {
728
+ display: flex;
729
+ align-items: center;
730
+ justify-content: space-between;
731
+ padding: var(--wa-spacing-medium, 12px) var(--wa-spacing-large, 16px);
732
+ background: var(--wa-color-surface-lowered, var(--wa-color-neutral-10));
733
+ border-bottom: var(--wa-border-width-s, 1px) solid var(--wa-color-neutral-border-loud, var(--wa-color-neutral-25));
734
+ cursor: move;
735
+ user-select: none;
736
+ }
737
+
738
+ :host-context(.wa-light) .panel-header {
739
+ background: var(--wa-color-surface-lowered, var(--wa-color-neutral-90));
740
+ border-bottom-color: var(--wa-color-neutral-border-loud, var(--wa-color-neutral-75));
741
+ }
742
+
743
+ .header-title {
744
+ display: flex;
745
+ align-items: center;
746
+ gap: var(--wa-spacing-small, 8px);
747
+ font-weight: 600;
748
+ color: var(--wa-color-text-normal, var(--wa-color-neutral-90));
749
+ }
750
+
751
+ :host-context(.wa-light) .header-title {
752
+ color: var(--wa-color-text-normal, var(--wa-color-neutral-10));
753
+ }
754
+
755
+ .header-actions {
756
+ display: flex;
757
+ gap: var(--wa-spacing-x-small, 4px);
758
+ }
759
+
760
+ .panel-content {
761
+ flex: 1;
762
+ overflow-y: auto;
763
+ padding: var(--wa-spacing-large, 16px);
764
+ }
765
+
766
+ .workflows-list h3 {
767
+ margin: 0 0 var(--wa-spacing-medium, 12px) 0;
768
+ font-size: var(--wa-font-size-medium, 14px);
769
+ font-weight: 600;
770
+ color: var(--wa-color-text-normal, var(--wa-color-neutral-80));
771
+ }
772
+
773
+ :host-context(.wa-light) .workflows-list h3 {
774
+ color: var(--wa-color-text-normal, var(--wa-color-neutral-20));
775
+ }
776
+
777
+ .workflow-item {
778
+ display: flex;
779
+ align-items: flex-start;
780
+ gap: var(--wa-spacing-medium, 12px);
781
+ padding: var(--wa-spacing-medium, 12px);
782
+ margin-bottom: var(--wa-spacing-small, 8px);
783
+ background: var(--wa-color-surface-lowered, var(--wa-color-neutral-10));
784
+ border: var(--wa-border-width-s, 1px) solid var(--wa-color-neutral-border-subtle, var(--wa-color-neutral-20));
785
+ border-radius: var(--wa-border-radius-small, 6px);
786
+ cursor: pointer;
787
+ transition: all var(--wa-transition-medium, 0.2s);
788
+ }
789
+
790
+ :host-context(.wa-light) .workflow-item {
791
+ background: var(--wa-color-surface-lowered, var(--wa-color-neutral-90));
792
+ border-color: var(--wa-color-neutral-border-subtle, var(--wa-color-neutral-80));
793
+ }
794
+
795
+ .workflow-item:hover {
796
+ background: var(--wa-color-mix-hover, var(--wa-color-neutral-15));
797
+ border-color: var(--wa-color-neutral-border-loud, var(--wa-color-neutral-30));
798
+ }
799
+
800
+ :host-context(.wa-light) .workflow-item:hover {
801
+ background: var(--wa-color-mix-hover, var(--wa-color-neutral-85));
802
+ border-color: var(--wa-color-neutral-border-loud, var(--wa-color-neutral-70));
803
+ }
804
+
805
+ .workflow-info {
806
+ flex: 1;
807
+ }
808
+
809
+ .workflow-title {
810
+ font-weight: 600;
811
+ margin-bottom: var(--wa-spacing-x-small, 4px);
812
+ color: var(--wa-color-text-normal, var(--wa-color-neutral-90));
813
+ }
814
+
815
+ :host-context(.wa-light) .workflow-title {
816
+ color: var(--wa-color-text-normal, var(--wa-color-neutral-10));
817
+ }
818
+
819
+ .workflow-desc {
820
+ font-size: var(--wa-font-size-small, 12px);
821
+ color: var(--wa-color-text-subtle, var(--wa-color-neutral-70));
822
+ margin-bottom: var(--wa-spacing-x-small, 4px);
823
+ }
824
+
825
+ :host-context(.wa-light) .workflow-desc {
826
+ color: var(--wa-color-text-subtle, var(--wa-color-neutral-30));
827
+ }
828
+
829
+ .workflow-meta {
830
+ font-size: var(--wa-font-size-x-small, 11px);
831
+ color: var(--wa-color-text-quiet, var(--wa-color-neutral-60));
832
+ }
833
+
834
+ :host-context(.wa-light) .workflow-meta {
835
+ color: var(--wa-color-text-quiet, var(--wa-color-neutral-40));
836
+ }
837
+
838
+ .active-workflow {
839
+ display: flex;
840
+ flex-direction: column;
841
+ gap: var(--wa-spacing-large, 16px);
842
+ }
843
+
844
+ .workflow-header {
845
+ display: flex;
846
+ align-items: flex-start;
847
+ justify-content: space-between;
848
+ gap: var(--wa-spacing-medium, 12px);
849
+ margin-bottom: var(--wa-spacing-medium, 12px);
850
+ }
851
+
852
+ .workflow-title-section {
853
+ flex: 1;
854
+ }
855
+
856
+ .workflow-header h3 {
857
+ margin: 0 0 var(--wa-spacing-small, 8px) 0;
858
+ font-size: var(--wa-font-size-large, 16px);
859
+ font-weight: 600;
860
+ color: var(--wa-color-text-normal, var(--wa-color-neutral-90));
861
+ }
862
+
863
+ :host-context(.wa-light) .workflow-header h3 {
864
+ color: var(--wa-color-text-normal, var(--wa-color-neutral-10));
865
+ }
866
+
867
+ .workflow-description {
868
+ margin: 0;
869
+ font-size: var(--wa-font-size-medium, 13px);
870
+ color: var(--wa-color-text-subtle, var(--wa-color-neutral-70));
871
+ }
872
+
873
+ :host-context(.wa-light) .workflow-description {
874
+ color: var(--wa-color-text-subtle, var(--wa-color-neutral-30));
875
+ }
876
+
877
+ .steps-list {
878
+ display: flex;
879
+ flex-direction: column;
880
+ gap: var(--wa-spacing-medium, 12px);
881
+ }
882
+
883
+ .step {
884
+ padding: var(--wa-spacing-medium, 12px);
885
+ background: var(--wa-color-surface-lowered, var(--wa-color-neutral-10));
886
+ border: var(--wa-border-width-s, 1px) solid var(--wa-color-neutral-border-subtle, var(--wa-color-neutral-20));
887
+ border-radius: var(--wa-border-radius-small, 6px);
888
+ transition: all var(--wa-transition-medium, 0.2s);
889
+ }
890
+
891
+ :host-context(.wa-light) .step {
892
+ background: var(--wa-color-surface-lowered, var(--wa-color-neutral-90));
893
+ border-color: var(--wa-color-neutral-border-subtle, var(--wa-color-neutral-80));
894
+ }
895
+
896
+ .step[data-active="true"] {
897
+ border-color: var(--wa-color-primary-50);
898
+ background: var(--wa-color-primary-05);
899
+ }
900
+
901
+ :host-context(.wa-light) .step[data-active="true"] {
902
+ background: var(--wa-color-primary-95);
903
+ border-color: var(--wa-color-primary-50);
904
+ }
905
+
906
+ .step.completed {
907
+ border-color: var(--wa-color-success-50);
908
+ background: var(--wa-color-success-05);
909
+ }
910
+
911
+ :host-context(.wa-light) .step.completed {
912
+ background: var(--wa-color-success-95);
913
+ border-color: var(--wa-color-success-50);
914
+ }
915
+
916
+ .step.failed {
917
+ border-color: var(--wa-color-danger-50);
918
+ background: var(--wa-color-danger-05);
919
+ }
920
+
921
+ :host-context(.wa-light) .step.failed {
922
+ background: var(--wa-color-danger-95);
923
+ border-color: var(--wa-color-danger-50);
924
+ }
925
+
926
+ .step-header {
927
+ display: flex;
928
+ align-items: center;
929
+ gap: var(--wa-spacing-medium, 12px);
930
+ margin-bottom: var(--wa-spacing-small, 8px);
931
+ }
932
+
933
+ .step-number {
934
+ width: 24px;
935
+ height: 24px;
936
+ display: flex;
937
+ align-items: center;
938
+ justify-content: center;
939
+ background: var(--wa-color-surface-lowered, var(--wa-color-neutral-20));
940
+ border-radius: 50%;
941
+ font-size: var(--wa-font-size-small, 12px);
942
+ font-weight: 600;
943
+ color: var(--wa-color-text-normal, var(--wa-color-neutral-80));
944
+ }
945
+
946
+ :host-context(.wa-light) .step-number {
947
+ background: var(--wa-color-surface-lowered, var(--wa-color-neutral-80));
948
+ color: var(--wa-color-text-normal, var(--wa-color-neutral-20));
949
+ }
950
+
951
+ .step-title {
952
+ flex: 1;
953
+ font-weight: 600;
954
+ color: var(--wa-color-text-normal, var(--wa-color-neutral-90));
955
+ }
956
+
957
+ :host-context(.wa-light) .step-title {
958
+ color: var(--wa-color-text-normal, var(--wa-color-neutral-10));
959
+ }
960
+
961
+ .step-status {
962
+ display: flex;
963
+ align-items: center;
964
+ gap: var(--wa-spacing-small, 8px);
965
+ }
966
+
967
+ .status-icon {
968
+ width: 20px;
969
+ height: 20px;
970
+ }
971
+
972
+ .status-icon.completed {
973
+ color: var(--wa-color-success-50);
974
+ }
975
+
976
+ .status-icon.failed {
977
+ color: var(--wa-color-danger-50);
978
+ }
979
+
980
+ .status-icon.skipped {
981
+ color: var(--wa-color-neutral-50);
982
+ }
983
+
984
+ .status-icon.active {
985
+ color: var(--wa-color-primary-50);
986
+ }
987
+
988
+ .status-icon.pending {
989
+ color: var(--wa-color-neutral-50);
990
+ }
991
+
992
+ .step-description {
993
+ font-size: var(--wa-font-size-medium, 13px);
994
+ color: var(--wa-color-text-subtle, var(--wa-color-neutral-70));
995
+ margin-bottom: var(--wa-spacing-small, 8px);
996
+ line-height: 1.4;
997
+ }
998
+
999
+ :host-context(.wa-light) .step-description {
1000
+ color: var(--wa-color-text-subtle, var(--wa-color-neutral-30));
1001
+ }
1002
+
1003
+ .condition {
1004
+ display: flex;
1005
+ align-items: center;
1006
+ gap: var(--wa-spacing-x-small, 6px);
1007
+ font-size: var(--wa-font-size-small, 12px);
1008
+ padding: var(--wa-spacing-x-small, 6px) var(--wa-spacing-small, 8px);
1009
+ border-radius: var(--wa-border-radius-x-small, 4px);
1010
+ margin-bottom: var(--wa-spacing-small, 8px);
1011
+ }
1012
+
1013
+ .condition.met {
1014
+ background: var(--wa-color-success-10);
1015
+ color: var(--wa-color-success-70);
1016
+ }
1017
+
1018
+ :host-context(.wa-light) .condition.met {
1019
+ background: var(--wa-color-success-90);
1020
+ color: var(--wa-color-success-30);
1021
+ }
1022
+
1023
+ .condition.not-met {
1024
+ background: var(--wa-color-danger-10);
1025
+ color: var(--wa-color-danger-70);
1026
+ }
1027
+
1028
+ :host-context(.wa-light) .condition.not-met {
1029
+ background: var(--wa-color-danger-90);
1030
+ color: var(--wa-color-danger-30);
1031
+ }
1032
+
1033
+ .step-actions {
1034
+ display: flex;
1035
+ gap: var(--wa-spacing-small, 8px);
1036
+ margin-top: var(--wa-spacing-small, 8px);
1037
+ }
1038
+
1039
+ .empty-state {
1040
+ display: flex;
1041
+ flex-direction: column;
1042
+ align-items: center;
1043
+ justify-content: center;
1044
+ padding: var(--wa-spacing-x-large, 40px) var(--wa-spacing-large, 20px);
1045
+ text-align: center;
1046
+ color: var(--wa-color-text-subtle, var(--wa-color-neutral-70));
1047
+ }
1048
+
1049
+ :host-context(.wa-light) .empty-state {
1050
+ color: var(--wa-color-text-subtle, var(--wa-color-neutral-30));
1051
+ }
1052
+
1053
+ .empty-state p {
1054
+ margin: var(--wa-spacing-small, 8px) 0;
1055
+ }
1056
+ `;
1057
+ }
1058
+