@design.estate/dees-catalog 9.3.0 → 9.4.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.
Files changed (29) hide show
  1. package/dist_bundle/bundle.js +1372 -2727
  2. package/dist_bundle/bundle.js.map +1 -1
  3. package/dist_ts_web/00_commitinfo_data.js +1 -1
  4. package/dist_ts_web/elements/00group-button/dees-button/dees-button.js +2 -1
  5. package/dist_ts_web/elements/00group-input/dees-input-quantityselector/dees-input-quantityselector.js +4 -2
  6. package/dist_ts_web/elements/00group-simple/dees-shopping-productcard/dees-shopping-productcard.d.ts +11 -9
  7. package/dist_ts_web/elements/00group-simple/dees-shopping-productcard/dees-shopping-productcard.demo.d.ts +2 -2
  8. package/dist_ts_web/elements/00group-simple/dees-shopping-productcard/dees-shopping-productcard.demo.js +148 -319
  9. package/dist_ts_web/elements/00group-simple/dees-shopping-productcard/dees-shopping-productcard.js +60 -279
  10. package/dist_ts_web/elements/00group-simple/dees-simple-appdash/dees-simple-appdash.d.ts +25 -10
  11. package/dist_ts_web/elements/00group-simple/dees-simple-appdash/dees-simple-appdash.demo.d.ts +3 -4
  12. package/dist_ts_web/elements/00group-simple/dees-simple-appdash/dees-simple-appdash.demo.js +229 -388
  13. package/dist_ts_web/elements/00group-simple/dees-simple-appdash/dees-simple-appdash.js +253 -813
  14. package/dist_ts_web/elements/00group-simple/dees-simple-login/dees-simple-login.demo.d.ts +2 -0
  15. package/dist_ts_web/elements/00group-simple/dees-simple-login/dees-simple-login.demo.js +150 -389
  16. package/dist_ts_web/elements/00group-simple/dees-simple-login/dees-simple-login.js +67 -11
  17. package/package.json +1 -1
  18. package/readme.hints.md +26 -0
  19. package/readme.md +76 -15
  20. package/scripts/bdtheme-baseline.json +0 -2
  21. package/ts_web/00_commitinfo_data.ts +1 -1
  22. package/ts_web/elements/00group-button/dees-button/dees-button.ts +1 -0
  23. package/ts_web/elements/00group-input/dees-input-quantityselector/dees-input-quantityselector.ts +3 -1
  24. package/ts_web/elements/00group-simple/dees-shopping-productcard/dees-shopping-productcard.demo.ts +67 -332
  25. package/ts_web/elements/00group-simple/dees-shopping-productcard/dees-shopping-productcard.ts +65 -322
  26. package/ts_web/elements/00group-simple/dees-simple-appdash/dees-simple-appdash.demo.ts +136 -382
  27. package/ts_web/elements/00group-simple/dees-simple-appdash/dees-simple-appdash.ts +224 -872
  28. package/ts_web/elements/00group-simple/dees-simple-login/dees-simple-login.demo.ts +116 -421
  29. package/ts_web/elements/00group-simple/dees-simple-login/dees-simple-login.ts +66 -10
@@ -1,27 +1,13 @@
1
+ import { customElement, html, DeesElement, property, state, cssManager, css, type TemplateResult } from '@design.estate/dees-element';
1
2
  import { demoFunc } from './dees-simple-appdash.demo.js';
2
-
3
- import {
4
- customElement,
5
- html,
6
- DeesElement,
7
- property,
8
- type TemplateResult,
9
- cssManager,
10
- css,
11
- unsafeCSS,
12
- type CSSResult,
13
- state,
14
- domtools,
15
- } from '@design.estate/dees-element';
3
+ import { themeDefaultStyles } from '../../00theme.js';
16
4
  import '../../00group-utility/dees-icon/dees-icon.js';
5
+ import '../../00group-button/dees-button/dees-button.js';
6
+ import '../../00group-overlay/dees-speechbubble/dees-speechbubble.js';
17
7
  import type { DeesWorkspaceTerminal } from '../../00group-workspace/dees-workspace-terminal/dees-workspace-terminal.js';
18
- import { themeDefaultStyles } from '../../00theme.js';
8
+ import type { IExecutionEnvironment } from '../../00group-runtime/index.js';
19
9
 
20
- declare global {
21
- interface HTMLElementTagNameMap {
22
- 'dees-simple-appdash': DeesSimpleAppDash;
23
- }
24
- }
10
+ declare global { interface HTMLElementTagNameMap { 'dees-simple-appdash': DeesSimpleAppDash; } }
25
11
 
26
12
  export interface IView {
27
13
  name: string;
@@ -29,15 +15,8 @@ export interface IView {
29
15
  element?: DeesElement['constructor']['prototype'];
30
16
  subViews?: IView[];
31
17
  }
32
-
33
18
  export type TGlobalMessageType = 'info' | 'success' | 'warning' | 'error';
34
-
35
- export interface IGlobalMessageAction {
36
- name: string;
37
- iconName?: string;
38
- action: () => void | Promise<void>;
39
- }
40
-
19
+ export interface IGlobalMessageAction { name: string; iconName?: string; action: () => void | Promise<void>; }
41
20
  export interface IGlobalMessage {
42
21
  id: string;
43
22
  type: TGlobalMessageType;
@@ -47,878 +26,251 @@ export interface IGlobalMessage {
47
26
  actions?: IGlobalMessageAction[];
48
27
  }
49
28
 
29
+ /** A compact app shell. Views, notices and the terminal share one bounded workspace. */
50
30
  @customElement('dees-simple-appdash')
51
31
  export class DeesSimpleAppDash extends DeesElement {
52
- // STATIC
53
32
  public static demo = demoFunc;
54
33
  public static demoGroups = ['Simple'];
55
- // INSTANCE
56
-
57
- @property()
58
- accessor name: string = 'Application Dashboard';
59
-
60
- @property({ type: Array })
61
- accessor viewTabs: IView[] = [];
62
-
63
- @property({ type: String })
64
- accessor terminalSetupCommand: string = `echo "Terminal ready"`;
65
-
66
- @property({ type: Array })
67
- accessor globalMessages: IGlobalMessage[] = [];
68
-
69
- @state()
70
- accessor selectedView!: IView;
71
-
72
- @state()
73
- accessor _activeMessages: IGlobalMessage[] = [];
74
-
75
-
76
- public static styles = [
77
- themeDefaultStyles,
78
- cssManager.defaultStyles,
79
- css`
80
- :host {
81
- color: var(--dees-color-text-primary);
82
- user-select: none;
83
- display: block;
84
- overflow: hidden;
85
- position: relative;
86
- height: 100%;
87
- width: 100%;
88
- /* scroll-fade gradient pairs — neutral shadow tints, stronger in dark
89
- (dark defaults, bright override below) */
90
- --appdash-fade-strong: rgba(0, 0, 0, 0.4);
91
- --appdash-fade-soft: rgba(0, 0, 0, 0.12);
92
- }
93
-
94
- :host([gobright]) {
95
- --appdash-fade-strong: rgba(0, 0, 0, 0.08);
96
- --appdash-fade-soft: rgba(0, 0, 0, 0.03);
97
- }
98
-
99
- .maincontainer {
100
- position: absolute;
101
- top: 0px;
102
- left: 0px;
103
- right: 0px;
104
- bottom: 0px;
105
- overflow: hidden;
106
- }
107
-
108
- .appbar {
109
- position: absolute;
110
- top: 0px;
111
- left: 0px;
112
- height: calc(100% - 24px);
113
- width: 240px;
114
- background: var(--dees-color-bg-secondary);
115
- border-right: 1px solid var(--dees-color-border-default);
116
- font-size: var(--dees-font-control-size-sm);
117
- font-family: var(--dees-font-family);
118
- z-index: 2;
119
- display: grid;
120
- grid-template-rows: auto 1fr min-content;
121
- overflow: hidden;
122
- }
123
-
124
- .sidebar-header {
125
- padding: var(--dees-spacing-lg);
126
- border-bottom: 1px solid var(--dees-color-border-default);
127
- display: flex;
128
- align-items: center;
129
- gap: var(--dees-spacing-md);
130
- }
131
-
132
- .header-icon-wrapper {
133
- display: flex;
134
- align-items: center;
135
- justify-content: center;
136
- width: 36px;
137
- height: 36px;
138
- border-radius: var(--dees-radius-lg);
139
- corner-shape: var(--dees-corner-shape);
140
- background: var(--dees-color-bg-tertiary);
141
- box-shadow: var(--dees-shadow-xs);
142
- }
143
-
144
- .header-icon-wrapper dees-icon {
145
- font-size: 18px;
146
- color: var(--dees-color-text-primary);
147
- }
148
-
149
- .appName {
150
- font-size: var(--dees-font-subhead-size);
151
- font-weight: 600;
152
- color: var(--dees-color-text-primary);
153
- white-space: nowrap;
154
- overflow: hidden;
155
- text-overflow: ellipsis;
156
- letter-spacing: -0.02em;
157
- }
158
-
159
- .viewTabs-container {
160
- overflow-y: auto;
161
- padding: 12px 8px;
162
- scrollbar-width: thin;
163
- scrollbar-color: var(--dees-color-scrollbar-thumb) transparent;
164
- }
165
-
166
- .viewTabs-container::-webkit-scrollbar {
167
- width: 6px;
168
- }
169
-
170
- .viewTabs-container::-webkit-scrollbar-track {
171
- background: transparent;
172
- }
173
-
174
- .viewTabs-container::-webkit-scrollbar-thumb {
175
- background: var(--dees-color-scrollbar-thumb);
176
- border-radius: 3px;
177
- }
178
-
179
- .viewTabs-container::-webkit-scrollbar-thumb:hover {
180
- background: var(--dees-color-scrollbar-thumb-hover);
181
- }
182
-
183
- .section-label {
184
- font-size: var(--dees-font-caption2-size);
185
- font-weight: 600;
186
- text-transform: uppercase;
187
- letter-spacing: 0.05em;
188
- color: var(--dees-color-text-secondary);
189
- padding: var(--dees-spacing-sm) var(--dees-spacing-md);
190
- margin-bottom: var(--dees-spacing-xs);
191
- }
192
-
193
- .viewTabs {
194
- display: flex;
195
- flex-direction: column;
196
- gap: 2px;
197
- }
198
-
199
- .viewTab {
200
- display: flex;
201
- align-items: center;
202
- gap: 10px;
203
- padding: 10px 12px;
204
- cursor: default;
205
- transition: all var(--dees-transition-fast) var(--dees-ease-standard);
206
- color: var(--dees-color-text-secondary);
207
- user-select: none;
208
- position: relative;
209
- border-radius: var(--dees-radius-md);
210
- corner-shape: var(--dees-corner-shape);
211
- }
212
-
213
- .viewTab:hover {
214
- background: var(--dees-color-hover);
215
- color: var(--dees-color-text-primary);
216
- }
217
-
218
- .viewTab:active {
219
- background: var(--dees-color-active);
220
- transform: scale(0.99);
221
- }
222
-
223
- .viewTab.selected {
224
- background: color-mix(in srgb, var(--dees-color-accent-primary) 12%, transparent);
225
- color: var(--dees-color-accent-primary);
226
- font-weight: 500;
227
- }
228
-
229
- .viewTab.selected::before {
230
- content: '';
231
- position: absolute;
232
- left: 0;
233
- top: 8px;
234
- bottom: 8px;
235
- width: 3px;
236
- border-radius: 0 2px 2px 0;
237
- background: var(--dees-color-accent-primary);
238
- }
239
-
240
- .viewTab dees-icon {
241
- font-size: 16px;
242
- opacity: 0.55;
243
- transition: all var(--dees-transition-fast) var(--dees-ease-standard);
244
- }
245
-
246
- .viewTab:hover dees-icon {
247
- opacity: 0.75;
248
- }
249
-
250
- .viewTab.selected dees-icon {
251
- opacity: 1;
252
- color: var(--dees-color-accent-primary);
253
- }
254
-
255
- .viewTab span {
256
- flex: 1;
257
- overflow: hidden;
258
- text-overflow: ellipsis;
259
- white-space: nowrap;
260
- }
261
-
262
- .viewTab .chevron {
263
- flex: 0 0 auto;
264
- font-size: 14px;
265
- opacity: 0.5;
266
- transform: rotate(-90deg);
267
- transition: transform 0.2s ease, opacity 0.15s ease;
268
- }
269
-
270
- .viewTab.hasSubs:hover .chevron {
271
- opacity: 0.75;
272
- }
273
-
274
- .viewTab.hasSubs.groupActive .chevron {
275
- transform: rotate(0deg);
276
- opacity: 0.9;
277
- }
278
-
279
- .subViews {
280
- display: grid;
281
- grid-template-rows: 0fr;
282
- margin-left: 12px;
283
- position: relative;
284
- transition:
285
- grid-template-rows 0.25s var(--dees-ease-standard),
286
- margin-top 0.25s var(--dees-ease-standard),
287
- margin-bottom 0.25s var(--dees-ease-standard);
288
- }
289
-
290
- .subViews.expanded {
291
- grid-template-rows: 1fr;
292
- margin-top: 2px;
293
- margin-bottom: 4px;
294
- }
295
-
296
- .subViews::before {
297
- content: '';
298
- position: absolute;
299
- left: 0;
300
- top: 4px;
301
- bottom: 4px;
302
- width: 1px;
303
- background: var(--dees-color-border-default);
304
- opacity: 0;
305
- transition: opacity 0.2s ease;
306
- }
307
-
308
- .subViews.expanded::before {
309
- opacity: 1;
310
- }
311
-
312
- .subViews-inner {
313
- min-height: 0;
314
- overflow: hidden;
315
- display: flex;
316
- flex-direction: column;
317
- gap: 2px;
318
- padding-left: 12px;
319
- }
320
-
321
- .viewTab.sub {
322
- padding: var(--dees-spacing-sm) var(--dees-spacing-md);
323
- font-size: var(--dees-font-caption1-size);
324
- }
325
-
326
- .viewTab.sub dees-icon {
327
- font-size: 14px;
328
- }
329
-
330
- .viewTab.sub.selected::before {
331
- left: -12px;
332
- }
333
-
334
- .appActions {
335
- padding: 12px 8px;
336
- border-top: 1px solid var(--dees-color-border-default);
337
- }
338
-
339
- .action {
340
- display: flex;
341
- align-items: center;
342
- gap: 10px;
343
- padding: 10px 12px;
344
- border-radius: var(--dees-radius-md);
345
- corner-shape: var(--dees-corner-shape);
346
- cursor: default;
347
- transition: all var(--dees-transition-fast) var(--dees-ease-standard);
348
- color: var(--dees-color-text-secondary);
349
- }
350
-
351
- .action:hover {
352
- background: color-mix(in srgb, var(--dees-color-accent-error) 10%, transparent);
353
- color: var(--dees-color-text-error);
354
- }
355
-
356
- .action dees-icon {
357
- font-size: 16px;
358
- opacity: 0.6;
359
- transition: all var(--dees-transition-fast) var(--dees-ease-standard);
360
- }
361
-
362
- .action:hover dees-icon {
363
- opacity: 0.9;
364
- color: var(--dees-color-text-error);
365
- }
366
-
367
- .appcontent {
368
- z-index: 1;
369
- position: absolute;
370
- top: 0px;
371
- right: 0px;
372
- height: calc(100% - 24px);
373
- bottom: 24px;
374
- width: calc(100% - 240px);
375
- overflow: auto;
376
- scrollbar-gutter: stable;
377
- background: var(--dees-color-bg-canvas);
378
- overscroll-behavior: contain;
379
- }
380
-
381
- .appcontent::before {
382
- content: '';
383
- position: sticky;
384
- top: 0;
385
- left: 0;
386
- right: 0;
387
- display: block;
388
- height: 8px;
389
- margin-bottom: -8px;
390
- z-index: 10;
391
- pointer-events: none;
392
- background: linear-gradient(
393
- to bottom,
394
- var(--appdash-fade-strong),
395
- var(--appdash-fade-soft),
396
- transparent
397
- );
398
- }
399
-
400
- .controlbar::before {
401
- content: '';
402
- position: absolute;
403
- top: -8px;
404
- left: 240px;
405
- right: 0;
406
- height: 8px;
407
- pointer-events: none;
408
- background: linear-gradient(
409
- to top,
410
- var(--appdash-fade-strong),
411
- var(--appdash-fade-soft),
412
- transparent
413
- );
414
- }
415
-
416
- .controlbar {
417
- color: var(--dees-color-text-muted);
418
- position: absolute;
419
- bottom: 0px;
420
- left: 0px;
421
- width: 100%;
422
- height: 24px;
423
- background: var(--dees-color-bg-tertiary);
424
- border-top: 1px solid var(--dees-color-border-default);
425
- z-index: 11;
426
- display: flex;
427
- justify-content: flex-end;
428
- align-items: center;
429
- flex-direction: row;
430
- font-size: var(--dees-font-caption2-size);
431
- font-weight: 500;
432
- letter-spacing: 0.01em;
433
- }
434
-
435
- .control {
436
- display: flex;
437
- align-items: center;
438
- gap: 6px;
439
- padding: 0 12px;
440
- height: 100%;
441
- white-space: nowrap;
442
- cursor: default;
443
- color: var(--dees-color-text-muted);
444
- transition: all var(--dees-transition-fast) var(--dees-ease-standard);
445
- border-left: 1px solid var(--dees-color-border-strong);
446
-
447
- }
448
-
449
- .control:first-child {
450
- border-left: none;
451
- }
452
-
453
- .control:hover {
454
- background: var(--dees-color-hover);
455
- color: var(--dees-color-text-primary);
456
- }
457
-
458
- .control dees-icon {
459
- font-size: 13px;
460
- }
461
-
462
- .control.status-connected dees-icon {
463
- color: var(--dees-color-text-success);
464
- }
465
-
466
- .control.status-terminal dees-icon {
467
- color: var(--dees-color-text-warning);
468
- }
469
-
470
- /* Global Message Banners */
471
- .messageBannerArea {
472
- position: absolute;
473
- top: 0;
474
- left: 240px;
475
- right: 0;
476
- z-index: 3;
477
- display: flex;
478
- flex-direction: column;
479
- transition: height 0.25s var(--dees-ease-standard);
480
- }
481
-
482
- .messageBanner {
483
- display: flex;
484
- align-items: center;
485
- gap: var(--dees-spacing-md);
486
- padding: 10px var(--dees-spacing-lg);
487
- font-size: var(--dees-font-footnote-size);
488
- font-family: var(--dees-font-family);
489
- font-weight: 500;
490
- border-bottom: 1px solid var(--dees-color-border-default);
491
- animation: bannerSlideDown 0.25s var(--dees-ease-standard) forwards;
492
- }
493
-
494
- .messageBanner.dismissing {
495
- animation: bannerSlideUp 0.2s var(--dees-ease-standard) forwards;
496
- }
497
-
498
- @keyframes bannerSlideDown {
499
- from {
500
- opacity: 0;
501
- transform: translateY(-100%);
502
- }
503
- to {
504
- opacity: 1;
505
- transform: translateY(0);
506
- }
507
- }
508
-
509
- @keyframes bannerSlideUp {
510
- from {
511
- opacity: 1;
512
- transform: translateY(0);
513
- }
514
- to {
515
- opacity: 0;
516
- transform: translateY(-100%);
517
- }
518
- }
519
-
520
- .messageBanner dees-icon {
521
- font-size: 16px;
522
- flex-shrink: 0;
523
- }
524
-
525
- .messageBanner-text {
526
- flex: 1;
527
- overflow: hidden;
528
- text-overflow: ellipsis;
529
- white-space: nowrap;
530
- }
531
-
532
- .messageBanner-actions {
533
- display: flex;
534
- align-items: center;
535
- gap: 6px;
536
- flex-shrink: 0;
537
- }
538
-
539
- .messageBanner-action {
540
- display: flex;
541
- align-items: center;
542
- gap: 6px;
543
- padding: var(--dees-spacing-xs) var(--dees-spacing-md);
544
- border-radius: var(--dees-radius-sm);
545
- corner-shape: var(--dees-corner-shape);
546
- cursor: default;
547
- font-size: var(--dees-font-caption1-size);
548
- font-weight: 600;
549
- letter-spacing: 0.01em;
550
- transition: all var(--dees-transition-fast) var(--dees-ease-standard);
551
- white-space: nowrap;
552
- background: var(--dees-color-pressed);
553
- color: inherit;
554
- }
555
-
556
- .messageBanner-action:hover {
557
- background: color-mix(in srgb, var(--dees-color-text-primary) 15%, transparent);
558
- }
559
-
560
- .messageBanner-action:active {
561
- transform: scale(0.97);
562
- }
563
-
564
- .messageBanner-action dees-icon {
565
- font-size: 13px;
566
- }
567
-
568
- .messageBanner-dismiss {
569
- flex-shrink: 0;
570
- display: flex;
571
- align-items: center;
572
- justify-content: center;
573
- width: 24px;
574
- height: 24px;
575
- border-radius: var(--dees-radius-sm);
576
- corner-shape: var(--dees-corner-shape);
577
- cursor: default;
578
- opacity: 0.5;
579
- transition: all var(--dees-transition-fast) var(--dees-ease-standard);
580
- }
581
-
582
- .messageBanner-dismiss:hover {
583
- opacity: 1;
584
- background: var(--dees-color-pressed);
585
- }
586
-
587
- /* Message type: info */
588
- .messageBanner-info {
589
- background: color-mix(in srgb, var(--dees-color-accent-primary) 12%, transparent);
590
- color: var(--dees-color-accent-primary);
591
- border-left: 3px solid var(--dees-color-accent-primary);
592
- }
593
- .messageBanner-info dees-icon {
594
- color: var(--dees-color-accent-primary);
595
- }
596
-
597
- /* Message type: success */
598
- .messageBanner-success {
599
- background: var(--dees-color-badge-success-bg);
600
- color: var(--dees-color-badge-success-fg);
601
- border-left: 3px solid var(--dees-color-accent-success);
602
- }
603
- .messageBanner-success dees-icon {
604
- color: var(--dees-color-accent-success);
605
- }
606
-
607
- /* Message type: warning */
608
- .messageBanner-warning {
609
- background: var(--dees-color-badge-warning-bg);
610
- color: var(--dees-color-badge-warning-fg);
611
- border-left: 3px solid var(--dees-color-accent-warning);
612
- }
613
- .messageBanner-warning dees-icon {
614
- color: var(--dees-color-accent-warning);
615
- }
616
-
617
- /* Message type: error */
618
- .messageBanner-error {
619
- background: var(--dees-color-badge-error-bg);
620
- color: var(--dees-color-badge-error-fg);
621
- border-left: 3px solid var(--dees-color-accent-error);
622
- }
623
- .messageBanner-error dees-icon {
624
- color: var(--dees-color-accent-error);
625
- }
626
-
627
- .messageBanner-dismiss:hover {
628
- background: var(--dees-color-pressed);
629
- }
34
+ @property() accessor name = 'Application Dashboard';
35
+ @property({ type: Array }) accessor viewTabs: IView[] = [];
36
+ @property() accessor terminalSetupCommand = 'echo "Terminal ready"';
37
+ /** Consumer-owned runtime used by the embedded terminal. */
38
+ @property({ type: Object }) accessor executionEnvironment: IExecutionEnvironment | null = null;
39
+ @property({ type: Array }) accessor globalMessages: IGlobalMessage[] = [];
40
+ /** Collapse to an icon rail. Narrow containers use the rail automatically. */
41
+ @property({ type: Boolean }) accessor collapsed = false;
42
+ @state() accessor selectedView!: IView;
43
+ @state() accessor _activeMessages: IGlobalMessage[] = [];
44
+ @state() private accessor compact = false;
45
+ @state() private accessor terminalOpen = false;
46
+ public currentTerminal: DeesWorkspaceTerminal | null = null;
47
+ private currentView?: DeesElement;
48
+ private terminalRequest = 0;
49
+
50
+ constructor() {
51
+ super();
52
+ this.registerStartupFunction(async signal => {
53
+ await this.updateComplete;
54
+ if (signal.aborted) return;
55
+ const observer = new ResizeObserver(entries => { this.compact = entries[0].contentRect.width < 640; });
56
+ observer.observe(this);
57
+ signal.addEventListener('abort', () => observer.disconnect(), { once: true });
58
+ });
59
+ }
630
60
 
631
- .appcontent {
632
- top: var(--banner-area-height, 0px);
633
- height: calc(100% - 24px - var(--banner-area-height, 0px));
634
- }
635
- `,
636
- ];
61
+ public static styles = [themeDefaultStyles, cssManager.defaultStyles, css`
62
+ :host { display: block; position: relative; width: 100%; height: 100%; overflow: hidden; color: var(--dees-color-text-primary); font: 13px/1.5 var(--dees-font-family); }
63
+ * { box-sizing: border-box; }
64
+ .maincontainer { position: absolute; inset: 0; display: grid; grid-template-columns: 224px minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) 32px; background: var(--dees-color-bg-canvas); }
65
+ .maincontainer.collapsed { grid-template-columns: 64px minmax(0, 1fr); }
66
+ .appbar { display: grid; grid-template-rows: 64px minmax(0, 1fr) auto; min-height: 0; background: var(--dees-color-bg-secondary); border-right: 1px solid var(--dees-color-border-subtle); }
67
+ .sidebar-header { display: flex; align-items: center; gap: 10px; padding: 0 16px; min-width: 0; }
68
+ .appName { font-size: 15px; font-weight: 600; letter-spacing: -.02em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
69
+ .sidebar-header > dees-icon { flex-shrink: 0; }
70
+ button { font: inherit; color: inherit; border: 0; background: transparent; cursor: pointer; }
71
+ button:focus-visible { outline: 2px solid var(--dees-color-focus-ring); outline-offset: -2px; }
72
+ .collapseToggle { margin-left: auto; flex-shrink: 0; width: 28px; height: 32px; display: grid; place-items: center; padding: 0; border-radius: var(--dees-radius-md); color: var(--dees-color-text-secondary); }
73
+ .viewTabs-container { overflow: auto; min-height: 0; padding: 12px 8px; scrollbar-width: thin; }
74
+ .section-label { padding: 0 12px 8px; color: var(--dees-color-text-secondary); font-size: 11px; font-weight: 500; }
75
+ .viewTabs { display: grid; gap: 3px; }
76
+ .viewTab, .action { display: flex; width: 100%; min-height: 38px; align-items: center; gap: 10px; padding: 8px 12px; text-align: left; border-radius: var(--dees-radius-lg); color: var(--dees-color-text-secondary); transition: background 140ms var(--dees-ease-standard), color 140ms var(--dees-ease-standard); }
77
+ .viewTab dees-icon, .action dees-icon { flex-shrink: 0; }
78
+ .viewTab span, .action span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
79
+ .viewTab .chevron { margin-left: auto; transform: rotate(-90deg); transition: transform 180ms var(--dees-ease-standard); }
80
+ .viewTab.groupActive .chevron, .viewTab.selected .chevron { transform: rotate(0); }
81
+ .viewTab.selected { background: var(--dees-color-nav-selected); color: var(--dees-color-accent-primary); font-weight: 600; }
82
+ .viewTab.groupActive { color: var(--dees-color-text-primary); }
83
+ .subViews { display: grid; grid-template-rows: 0fr; margin: 0 0 0 20px; border-left: 1px solid var(--dees-color-border-subtle); transition: grid-template-rows 180ms var(--dees-ease-standard), margin 180ms var(--dees-ease-standard); }
84
+ .subViews.expanded { grid-template-rows: 1fr; margin-top: 4px; margin-bottom: 8px; }
85
+ .subViews-inner { display: grid; gap: 3px; min-height: 0; overflow: hidden; padding-left: 8px; opacity: 0; transform: translateY(-4px); transition: opacity 140ms var(--dees-ease-standard), transform 180ms var(--dees-ease-standard); }
86
+ .subViews.expanded .subViews-inner { opacity: 1; transform: translateY(0); }
87
+ .viewTab.sub { min-height: 34px; font-size: 12px; }
88
+ .appActions { padding: 8px; border-top: 1px solid var(--dees-color-border-subtle); }
89
+ .workspace { display: grid; grid-template-rows: 64px auto minmax(0, 1fr); min-width: 0; min-height: 0; }
90
+ .workspaceHeader { display: flex; align-items: center; gap: 8px; padding: 0 24px; border-bottom: 1px solid var(--dees-color-border-subtle); min-width: 0; font-weight: 600; }
91
+ .workspaceHeader span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
92
+ .workspaceHeader .parent { color: var(--dees-color-text-secondary); font-weight: 400; }
93
+ .contentArea { min-height: 0; min-width: 0; position: relative; display: grid; }
94
+ .appcontent { overflow: auto; min-height: 0; min-width: 0; scrollbar-gutter: stable; overscroll-behavior: contain; }
95
+ .appcontent[inert] { visibility: hidden; }
96
+ .terminalLayer { position: absolute; inset: 0; }
97
+ .terminalLayer[hidden] { display: none; }
98
+ .terminalLayer > dees-workspace-terminal { display: block; position: absolute; inset: 0; width: 100%; height: 100%; }
99
+ .messageBannerArea { max-height: 192px; overflow: auto; }
100
+ .messageBanner { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 12px 24px; border-bottom: 1px solid var(--dees-color-border-subtle); background: var(--dees-color-bg-primary); }
101
+ .messageIcon { display: grid; place-items: center; width: 28px; height: 28px; flex-shrink: 0; border-radius: var(--dees-radius-md); background: var(--message-bg); color: var(--message-fg); }
102
+ .messageBanner-info { --message-bg: var(--dees-color-nav-selected); --message-fg: var(--dees-color-link); }
103
+ .messageBanner-success { --message-bg: var(--dees-color-badge-success-bg); --message-fg: var(--dees-color-badge-success-fg); }
104
+ .messageBanner-warning { --message-bg: var(--dees-color-badge-warning-bg); --message-fg: var(--dees-color-badge-warning-fg); }
105
+ .messageBanner-error { --message-bg: var(--dees-color-badge-error-bg); --message-fg: var(--dees-color-badge-error-fg); }
106
+ .messageBanner-text { flex: 1 1 160px; min-width: 0; overflow-wrap: anywhere; }
107
+ .messageBanner-actions { display: flex; flex-wrap: wrap; gap: 8px; }
108
+ .messageBanner-dismiss { display: grid; place-items: center; width: 28px; height: 28px; padding: 0; border-radius: var(--dees-radius-md); color: var(--dees-color-text-secondary); }
109
+ .controlbar { grid-column: 1 / -1; display: flex; align-items: center; justify-content: flex-end; border-top: 1px solid var(--dees-color-border-subtle); background: var(--dees-color-bg-secondary); font-size: 11px; color: var(--dees-color-text-secondary); }
110
+ .control { display: flex; align-items: center; gap: 6px; padding: 0 12px; height: 100%; }
111
+ .status-terminal { border-left: 1px solid var(--dees-color-border-subtle); }
112
+ .status-terminal[aria-pressed='true'] { color: var(--dees-color-accent-primary); background: var(--dees-color-nav-selected); }
113
+ .collapsed .sidebar-header { justify-content: center; padding: 0; }
114
+ .collapsed .sidebar-header > dees-icon, .collapsed .appName, .collapsed .section-label, .collapsed .viewTab span, .collapsed .viewTab .chevron, .collapsed .action span { display: none; }
115
+ .collapsed .collapseToggle { margin: 0; width: 40px; height: 38px; }
116
+ .collapsed .viewTab, .collapsed .action { justify-content: center; padding: 8px 0; }
117
+ .collapsed .subViews { margin-left: 0; padding: 0; border: 0; }
118
+ .collapsed .subViews-inner { padding-left: 0; }
119
+ .collapsed .sub { color: var(--dees-color-text-secondary); }
120
+ .collapsed .sub.selected { color: var(--dees-color-accent-primary); }
121
+ /* The content column resizes once; only sidebar contents animate during a rail toggle. */
122
+ .maincontainer:not(.collapsed) .viewTab span, .maincontainer:not(.collapsed) .appName, .maincontainer:not(.collapsed) .action span { animation: labelReveal 160ms var(--dees-ease-standard); }
123
+ .collapsed .viewTab > dees-icon:not(.chevron), .collapsed .action > dees-icon { animation: railSettle 160ms var(--dees-ease-standard); }
124
+ @keyframes labelReveal { from { opacity: 0; transform: translateX(-4px); } to { opacity: 1; transform: translateX(0); } }
125
+ @keyframes railSettle { from { transform: translateX(-4px); } to { transform: translateX(0); } }
126
+ @media (hover: hover) { button:hover:not(.selected) { background: var(--dees-color-hover); } .action:hover { color: var(--dees-color-text-error); } }
127
+ @media (pointer: coarse) { .viewTab, .viewTab.sub, .action, .collapseToggle, .messageBanner-dismiss { min-height: 44px; } .maincontainer { grid-template-rows: minmax(0, 1fr) 44px; } }
128
+ @media (prefers-reduced-motion: reduce) { .maincontainer, .viewTab, .action, .chevron, .subViews, .subViews-inner { transition: none; } .maincontainer * { animation: none !important; } }
129
+ `];
130
+
131
+ private get rail(): boolean { return this.collapsed || this.compact; }
132
+ private icon(name: string): string { return name.includes(':') ? name : `lucide:${name}`; }
133
+ private isGroupActive(view: IView): boolean { return view === this.selectedView || !!view.subViews?.includes(this.selectedView); }
134
+
135
+ private renderView(view: IView, sub = false): TemplateResult {
136
+ const selected = this.selectedView === view;
137
+ const expanded = this.isGroupActive(view);
138
+ return html`<button type="button" class="viewTab ${sub ? 'sub' : ''} ${selected ? 'selected' : ''} ${expanded && !selected ? 'groupActive' : ''}"
139
+ aria-label=${view.name} aria-current=${selected ? 'page' : 'false'}
140
+ @keydown=${this.handleNavigationKey} @click=${() => this.loadView(view)}>
141
+ <dees-icon .icon=${this.icon(view.iconName || (sub ? 'dot' : 'file'))} iconSize="17"></dees-icon><span>${view.name}</span>
142
+ ${view.subViews?.length ? html`<dees-icon class="chevron" icon="lucide:chevronDown" iconSize="12"></dees-icon>` : ''}
143
+ </button><dees-speechbubble hint .disabled=${!this.rail} .text=${view.name}></dees-speechbubble>`;
144
+ }
637
145
 
638
146
  public render(): TemplateResult {
639
- return html`
640
- <div class="maincontainer">
641
- <div class="appbar">
642
- <div class="sidebar-header">
643
- <div class="header-icon-wrapper">
644
- <dees-icon .icon="${'lucide:layoutGrid'}"></dees-icon>
645
- </div>
646
- <div class="appName">${this.name}</div>
647
- </div>
648
- <div class="viewTabs-container">
649
- <div class="section-label">Navigation</div>
650
- <div class="viewTabs">
651
- ${this.viewTabs.map((view) => {
652
- const hasSubs = !!view.subViews?.length;
653
- const groupActive = hasSubs && this.isGroupActive(view);
654
- return html`
655
- <div
656
- class="viewTab ${this.selectedView === view ? 'selected' : ''} ${hasSubs ? 'hasSubs' : ''} ${groupActive ? 'groupActive' : ''}"
657
- @click=${() => this.loadView(view)}
658
- >
659
- ${view.iconName ? html`
660
- <dees-icon .icon="${view.iconName.includes(':') ? view.iconName : `lucide:${view.iconName}`}"></dees-icon>
661
- ` : html`
662
- <dees-icon .icon="${'lucide:file'}"></dees-icon>
663
- `}
664
- <span>${view.name}</span>
665
- ${hasSubs ? html`
666
- <dees-icon class="chevron" .icon="${'lucide:chevronDown'}"></dees-icon>
667
- ` : ''}
668
- </div>
669
- ${hasSubs ? html`
670
- <div
671
- class="subViews ${groupActive ? 'expanded' : ''}"
672
- ?inert=${!groupActive}
673
- >
674
- <div class="subViews-inner">
675
- ${view.subViews!.map(
676
- (sub) => html`
677
- <div
678
- class="viewTab sub ${this.selectedView === sub ? 'selected' : ''}"
679
- @click=${(e: Event) => {
680
- e.stopPropagation();
681
- this.loadView(sub);
682
- }}
683
- >
684
- ${sub.iconName ? html`
685
- <dees-icon .icon="${sub.iconName.includes(':') ? sub.iconName : `lucide:${sub.iconName}`}"></dees-icon>
686
- ` : html`
687
- <dees-icon .icon="${'lucide:dot'}"></dees-icon>
688
- `}
689
- <span>${sub.name}</span>
690
- </div>
691
- `
692
- )}
693
- </div>
694
- </div>
695
- ` : ''}
696
- `;
697
- })}
698
- </div>
699
- </div>
700
- <div class="appActions">
701
- <div class="action" @click=${() => {
702
- this.dispatchEvent(new CustomEvent('logout', { bubbles: true, composed: true }));
703
- }}>
704
- <dees-icon .icon="${'lucide:logOut'}"></dees-icon>
705
- <span>Sign out</span>
706
- </div>
707
- </div>
708
- </div>
709
- ${this._activeMessages.length > 0 ? html`
710
- <div class="messageBannerArea">
711
- ${this._activeMessages.map(msg => html`
712
- <div
713
- class="messageBanner messageBanner-${msg.type}"
714
- data-message-id="${msg.id}"
715
- >
716
- <dees-icon .icon="${this.getMessageIcon(msg)}"></dees-icon>
717
- <span class="messageBanner-text">${msg.message}</span>
718
- ${msg.actions?.length ? html`
719
- <div class="messageBanner-actions">
720
- ${msg.actions.map(a => html`
721
- <div class="messageBanner-action" @click=${() => a.action()}>
722
- ${a.iconName ? html`<dees-icon .icon="${a.iconName.includes(':') ? a.iconName : `lucide:${a.iconName}`}"></dees-icon>` : ''}
723
- <span>${a.name}</span>
724
- </div>
725
- `)}
726
- </div>
727
- ` : ''}
728
- ${msg.dismissible !== false ? html`
729
- <div class="messageBanner-dismiss" @click=${() => this.removeMessage(msg.id)}>
730
- <dees-icon .icon="${'lucide:x'}"></dees-icon>
731
- </div>
732
- ` : ''}
733
- </div>
734
- `)}
147
+ const parent = this.viewTabs.find(view => view.subViews?.includes(this.selectedView));
148
+ return html`<div class="maincontainer ${this.rail ? 'collapsed' : ''}">
149
+ <aside class="appbar" aria-label=${this.name}>
150
+ <div class="sidebar-header"><dees-icon icon="lucide:layers" iconSize="24"></dees-icon><div class="appName">${this.name}</div>
151
+ <button type="button" class="collapseToggle" aria-label=${this.compact ? 'Compact navigation' : this.rail ? 'Expand navigation' : 'Collapse navigation'}
152
+ ?disabled=${this.compact} @click=${() => { this.collapsed = !this.collapsed; }}>
153
+ <dees-icon .icon=${this.rail ? 'lucide:panelLeftOpen' : 'lucide:panelLeftClose'} iconSize="17"></dees-icon>
154
+ </button>
735
155
  </div>
736
- ` : ''}
737
- <div class="appcontent">
738
- <!-- Content goes here -->
156
+ <nav class="viewTabs-container" aria-label="App navigation"><div class="section-label">Workspace</div><div class="viewTabs">
157
+ ${this.viewTabs.map(view => html`<div class="viewGroup">${this.renderView(view)}${view.subViews?.length ? html`<div class="subViews ${this.isGroupActive(view) ? 'expanded' : ''}" ?inert=${!this.isGroupActive(view)}>
158
+ <div class="subViews-inner">${view.subViews.map(sub => this.renderView(sub, true))}</div></div>` : ''}</div>`)}
159
+ </div></nav>
160
+ <div class="appActions"><button type="button" class="action" aria-label="Sign out" @click=${() => this.dispatchEvent(new CustomEvent('logout', { bubbles: true, composed: true }))}>
161
+ <dees-icon icon="lucide:logOut" iconSize="17"></dees-icon><span>Sign out</span>
162
+ </button><dees-speechbubble hint .disabled=${!this.rail} text="Sign out"></dees-speechbubble></div>
163
+ </aside>
164
+ <section class="workspace">
165
+ <header class="workspaceHeader">${parent ? html`<span class="parent">${parent.name}</span><dees-icon icon="lucide:chevronRight" iconSize="12"></dees-icon>` : ''}<span>${this.selectedView?.name || this.name}</span></header>
166
+ <div class="messageBannerArea" aria-live="polite">
167
+ ${this._activeMessages.map(message => html`<div class="messageBanner messageBanner-${message.type}" data-message-id=${message.id}>
168
+ <span class="messageIcon"><dees-icon .icon=${this.getMessageIcon(message)} iconSize="16"></dees-icon></span>
169
+ <span class="messageBanner-text">${message.message}</span>
170
+ ${message.actions?.length ? html`<div class="messageBanner-actions">${message.actions.map(action => html`<dees-button size="sm" type="outline" .text=${action.name} .icon=${action.iconName ? this.icon(action.iconName) : ''} @clicked=${() => action.action()}></dees-button>`)}</div>` : ''}
171
+ ${message.dismissible !== false ? html`<button type="button" class="messageBanner-dismiss" aria-label=${`Dismiss: ${message.message}`} @click=${() => this.removeMessage(message.id)}><dees-icon icon="lucide:x" iconSize="14"></dees-icon></button>` : ''}
172
+ </div>`)}
739
173
  </div>
740
- <div class="controlbar">
741
- <div class="control status-connected">
742
- <dees-icon .icon="${'lucide:wifi'}"></dees-icon>
743
- <span>Connected</span>
744
- </div>
745
- <div class="control status-terminal" @click=${this.launchTerminal}>
746
- <dees-icon .icon="${'lucide:terminal'}"></dees-icon>
747
- <span>Terminal</span>
748
- </div>
749
- </div>
750
- </div>
751
- `;
174
+ <div class="contentArea"><div class="appcontent" ?inert=${this.terminalOpen}></div><div class="terminalLayer" ?hidden=${!this.terminalOpen}></div></div>
175
+ </section>
176
+ <footer class="controlbar"><span class="control status-connected"><dees-icon icon="lucide:wifi" iconSize="12"></dees-icon>Connected</span>
177
+ <button type="button" class="control status-terminal" aria-pressed=${String(this.terminalOpen)} @click=${this.launchTerminal}><dees-icon icon="lucide:terminal" iconSize="13"></dees-icon>Terminal</button>
178
+ </footer>
179
+ </div>`;
752
180
  }
753
181
 
754
- public async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>): Promise<void> {
755
- const domtools = await this.domtoolsPromise;
756
- super.firstUpdated(_changedProperties);
757
- if (this.viewTabs && this.viewTabs.length > 0) {
758
- const viewToLoad = this.selectedView || this.viewTabs[0];
759
- await this.loadView(viewToLoad);
760
- }
182
+ private handleNavigationKey(event: KeyboardEvent): void {
183
+ if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;
184
+ event.preventDefault();
185
+ const buttons = [...this.shadowRoot!.querySelectorAll<HTMLButtonElement>('.viewTab')].filter(button => !button.closest('[inert]'));
186
+ const index = buttons.indexOf(event.currentTarget as HTMLButtonElement);
187
+ const next = event.key === 'Home' ? 0 : event.key === 'End' ? buttons.length - 1 : (index + (event.key === 'ArrowDown' ? 1 : -1) + buttons.length) % buttons.length;
188
+ buttons[next]?.focus();
761
189
  }
762
190
 
763
- public willUpdate(changedProperties: Map<string, unknown>) {
764
- if (changedProperties.has('globalMessages')) {
765
- // Sync globalMessages property into _activeMessages
766
- // Keep any messages added via addMessage() that aren't in globalMessages
767
- const propertyIds = new Set(this.globalMessages.map(m => m.id));
768
- const existingIds = new Set(this._activeMessages.map(m => m.id));
769
-
770
- // Add new messages from property that aren't already active
771
- const newMessages = this.globalMessages.filter(m => !existingIds.has(m.id));
772
-
773
- // Keep messages added via API (those not in globalMessages are kept as-is)
774
- // Remove messages that were in the previous globalMessages but are no longer
775
- const previousGlobalMessages = (changedProperties.get('globalMessages') as IGlobalMessage[]) || [];
776
- const previousIds = new Set(previousGlobalMessages.map(m => m.id));
777
- const removedIds = new Set([...previousIds].filter(id => !propertyIds.has(id)));
778
-
779
- this._activeMessages = [
780
- ...this._activeMessages.filter(m => !removedIds.has(m.id)),
781
- ...newMessages,
782
- ];
191
+ public willUpdate(changes: Map<string, unknown>): void {
192
+ if (changes.has('globalMessages')) {
193
+ const previousIds = new Set(((changes.get('globalMessages') as IGlobalMessage[]) || []).map(message => message.id));
194
+ const incoming = new Map(this.globalMessages.map(message => [message.id, message]));
195
+ this._activeMessages = [...this._activeMessages.filter(message => !previousIds.has(message.id) && !incoming.has(message.id)), ...incoming.values()];
783
196
  }
784
197
  }
785
198
 
786
- public updated(changedProperties: Map<string, unknown>) {
787
- super.updated(changedProperties);
788
- this.updateBannerOffset();
789
- }
790
-
791
- private updateBannerOffset() {
792
- requestAnimationFrame(() => {
793
- const bannerArea = this.shadowRoot?.querySelector('.messageBannerArea') as HTMLElement;
794
- const maincontainer = this.shadowRoot?.querySelector('.maincontainer') as HTMLElement;
795
- const height = bannerArea ? bannerArea.offsetHeight : 0;
796
- maincontainer?.style.setProperty('--banner-area-height', `${height}px`);
797
- });
199
+ public updated(changes: Map<string, unknown>): void {
200
+ super.updated(changes);
201
+ if (changes.has('viewTabs')) {
202
+ const views = this.viewTabs.flatMap(view => [view, ...(view.subViews || [])]);
203
+ if (!views.includes(this.selectedView)) {
204
+ this.currentView?.remove();
205
+ this.currentView = undefined;
206
+ this.selectedView = undefined!;
207
+ }
208
+ if (!this.currentView && this.viewTabs.length) void this.loadView(this.selectedView || this.viewTabs[0]);
209
+ }
798
210
  }
799
211
 
800
- private getMessageIcon(msg: IGlobalMessage): string {
801
- if (msg.icon) return msg.icon;
802
- const defaults: Record<TGlobalMessageType, string> = {
803
- info: 'lucide:info',
804
- success: 'lucide:circleCheck',
805
- warning: 'lucide:triangleAlert',
806
- error: 'lucide:circleX',
807
- };
808
- return defaults[msg.type];
212
+ private getMessageIcon(message: IGlobalMessage): string {
213
+ const icons: Record<TGlobalMessageType, string> = { info: 'info', success: 'circleCheck', warning: 'triangleAlert', error: 'circleX' };
214
+ return this.icon(message.icon || icons[message.type]);
809
215
  }
810
-
811
216
  public addMessage(message: Omit<IGlobalMessage, 'id'> & { id?: string }): string {
812
- const id = message.id || `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
813
- const fullMessage: IGlobalMessage = {
814
- dismissible: true,
815
- ...message,
816
- id,
817
- };
818
- this._activeMessages = [...this._activeMessages, fullMessage];
217
+ const id = message.id || `msg-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
218
+ const next = { dismissible: true, ...message, id };
219
+ this._activeMessages = this._activeMessages.some(entry => entry.id === id)
220
+ ? this._activeMessages.map(entry => entry.id === id ? next : entry) : [...this._activeMessages, next];
819
221
  return id;
820
222
  }
821
-
822
223
  public removeMessage(id: string): void {
823
- const bannerEl = this.shadowRoot?.querySelector(`[data-message-id="${id}"]`) as HTMLElement;
824
- if (bannerEl) {
825
- bannerEl.classList.add('dismissing');
826
- bannerEl.addEventListener('animationend', () => {
827
- this._activeMessages = this._activeMessages.filter(m => m.id !== id);
828
- this.dispatchEvent(new CustomEvent('message-dismiss', {
829
- detail: { id },
830
- bubbles: true,
831
- composed: true,
832
- }));
833
- }, { once: true });
834
- } else {
835
- this._activeMessages = this._activeMessages.filter(m => m.id !== id);
836
- }
837
- }
838
-
839
- public clearMessages(): void {
840
- this._activeMessages = [];
224
+ if (!this._activeMessages.some(message => message.id === id)) return;
225
+ this._activeMessages = this._activeMessages.filter(message => message.id !== id);
226
+ this.dispatchEvent(new CustomEvent('message-dismiss', { detail: { id }, bubbles: true, composed: true }));
841
227
  }
842
-
843
- public currentTerminal: DeesWorkspaceTerminal | null = null;
844
- public async launchTerminal() {
845
- const domtools = await this.domtoolsPromise;
846
- if (this.currentTerminal) {
847
- // If terminal already exists, remove it
848
- await this.closeTerminal();
849
- return;
850
- }
851
-
852
- const maincontainer = this.shadowRoot!.querySelector('.maincontainer')! as HTMLElement;
853
- const { DeesWorkspaceTerminal } = await import('../../00group-workspace/dees-workspace-terminal/dees-workspace-terminal.js');
854
- const terminal = new DeesWorkspaceTerminal();
855
- terminal.setupCommand = this.terminalSetupCommand;
856
- this.currentTerminal = terminal;
857
- maincontainer.appendChild(terminal);
858
- terminal.style.position = 'absolute';
859
- terminal.style.zIndex = '10';
860
- terminal.style.top = 'var(--banner-area-height, 0px)';
861
- terminal.style.left = '240px';
862
- terminal.style.right = '0px';
863
- terminal.style.height = 'auto';
864
- terminal.style.bottom = '24px';
865
- terminal.style.opacity = '0';
866
- terminal.style.transform = 'translateY(8px) scale(0.99)';
867
- terminal.style.transition = 'all 0.25s var(--dees-ease-standard)';
868
- terminal.style.boxShadow = 'var(--dees-shadow-xl), 0 0 0 1px var(--dees-color-border-subtle)';
869
-
870
- // Add close button to terminal
871
- terminal.addEventListener('close', () => this.closeTerminal());
872
-
873
- await domtools.convenience.smartdelay.delayFor(0);
874
- terminal.style.opacity = '1';
875
- terminal.style.transform = 'translateY(0) scale(1)';
876
- return terminal;
877
- }
878
-
879
- private async closeTerminal() {
880
- const domtools = await this.domtoolsPromise;
881
- if (this.currentTerminal) {
882
- this.currentTerminal.style.opacity = '0';
883
- this.currentTerminal.style.transform = 'translateY(8px) scale(0.99)';
884
- await domtools.convenience.smartdelay.delayFor(250);
885
- this.currentTerminal.remove();
886
- this.currentTerminal = null;
228
+ public clearMessages(): void { this._activeMessages = []; }
229
+
230
+ public async launchTerminal(): Promise<DeesWorkspaceTerminal | undefined> {
231
+ if (this.terminalOpen) { this.closeTerminal(); return; }
232
+ this.terminalOpen = true;
233
+ const request = ++this.terminalRequest;
234
+ try {
235
+ const { DeesWorkspaceTerminal } = await import('../../00group-workspace/dees-workspace-terminal/dees-workspace-terminal.js');
236
+ await this.updateComplete;
237
+ if (!this.isConnected || request !== this.terminalRequest) return;
238
+ const terminal = new DeesWorkspaceTerminal();
239
+ terminal.executionEnvironment = this.executionEnvironment;
240
+ terminal.setupCommand = this.terminalSetupCommand;
241
+ this.currentTerminal = terminal;
242
+ terminal.addEventListener('close', () => { if (terminal === this.currentTerminal) this.closeTerminal(); });
243
+ this.shadowRoot!.querySelector('.terminalLayer')!.appendChild(terminal);
244
+ return terminal;
245
+ } catch (error) {
246
+ if (request === this.terminalRequest) this.closeTerminal();
247
+ throw error;
887
248
  }
888
249
  }
889
-
890
-
891
- private isGroupActive(view: IView): boolean {
892
- if (this.selectedView === view) return true;
893
- return view.subViews?.some((sv) => sv === this.selectedView) ?? false;
250
+ private closeTerminal(): void {
251
+ ++this.terminalRequest;
252
+ this.currentTerminal?.remove();
253
+ this.currentTerminal = null;
254
+ this.terminalOpen = false;
894
255
  }
256
+ public async disconnectedCallback(): Promise<void> { this.closeTerminal(); await super.disconnectedCallback(); }
895
257
 
896
- private currentView!: DeesElement;
897
- public async loadView(viewArg: IView) {
898
- // Group-only parent: resolve to first sub view with an element
899
- if (!viewArg.element && viewArg.subViews?.length) {
900
- const firstNavigable = viewArg.subViews.find((sv) => sv.element);
901
- if (firstNavigable) {
902
- return this.loadView(firstNavigable);
903
- }
904
- return; // nothing navigable — ignore click
905
- }
906
- if (!viewArg.element) return; // safety: no element and no subs → no-op
907
-
908
- const appcontent = this.shadowRoot!.querySelector('.appcontent')!;
909
- const view = new viewArg.element();
910
- if (this.currentView) {
911
- this.currentView.remove();
258
+ public async loadView(view: IView): Promise<void> {
259
+ if (!view.element && view.subViews?.length) {
260
+ const child = view.subViews.find(candidate => candidate.element);
261
+ if (child) await this.loadView(child);
262
+ return;
912
263
  }
913
- appcontent.appendChild(view);
914
- this.currentView = view;
915
- this.selectedView = viewArg;
916
-
917
- // Emit view-select event
918
- this.dispatchEvent(new CustomEvent('view-select', {
919
- detail: { view: viewArg },
920
- bubbles: true,
921
- composed: true
922
- }));
264
+ if (!view.element || (view === this.selectedView && this.currentView)) return;
265
+ const content = this.shadowRoot?.querySelector('.appcontent');
266
+ if (!content) return;
267
+ const element = new view.element();
268
+ this.closeTerminal();
269
+ this.currentView?.remove();
270
+ content.appendChild(element);
271
+ content.scrollTop = 0;
272
+ this.currentView = element;
273
+ this.selectedView = view;
274
+ this.dispatchEvent(new CustomEvent('view-select', { detail: { view }, bubbles: true, composed: true }));
923
275
  }
924
276
  }