@design.estate/dees-catalog 4.1.0 → 4.1.1

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.
@@ -22,6 +22,7 @@ import { harnessCardStyles, harnessPillStyles, harnessMonoStyles } from '../harn
22
22
  import { harnessFormatDuration, harnessTruncate } from '../harness.helpers.js';
23
23
  import '../dees-harness-content-blocks/dees-harness-content-blocks.js';
24
24
  import '../../00group-dataview/dees-dataview-codebox/dees-dataview-codebox.js';
25
+ import './dees-harness-tool-fullscreen.js';
25
26
  import '../../00group-feedback/dees-badge/dees-badge.js';
26
27
  import '../../00group-utility/dees-icon/dees-icon.js';
27
28
 
@@ -54,21 +55,23 @@ export class DeesHarnessToolCard extends DeesElement {
54
55
  @state()
55
56
  accessor outputExpanded: boolean = false;
56
57
 
57
- /** Body height cap; content beyond it clips and offers fullscreen. */
58
+ /** Body height cap; taller content scrolls in place and offers fullscreen. */
58
59
  @property({ type: Number })
59
60
  accessor bodyCapPx: number = 360;
60
61
 
61
- @state()
62
- accessor bodyOverflowing: boolean = false;
62
+ /** Set on the card inside the fullscreen modal: no cap, no maximize bar. */
63
+ @property({ type: Boolean })
64
+ accessor uncapped: boolean = false;
63
65
 
64
66
  @state()
65
- accessor maximized: boolean = false;
67
+ accessor bodyOverflowing: boolean = false;
66
68
 
67
69
  @state()
68
70
  accessor returnFlash: boolean = false;
69
71
 
70
72
  private bodyResizeObserver: ResizeObserver | null = null;
71
73
  private observedBodyInner: Element | null = null;
74
+ private fullscreenPortal: HTMLElementTagNameMap['dees-harness-tool-fullscreen'] | null = null;
72
75
 
73
76
  public static styles = [
74
77
  themeDefaultStyles,
@@ -198,22 +201,14 @@ export class DeesHarnessToolCard extends DeesElement {
198
201
  }
199
202
 
200
203
  /* Three body modes: collapsed (no body), normal (content fits the
201
- cap), capped (content taller than the cap clips behind a fade and
202
- offers fullscreen). */
204
+ cap), capped (content taller than the cap scrolls inside the frame
205
+ and offers the fullscreen modal). */
203
206
  .bodyClipFrame {
204
207
  position: relative;
205
- overflow: hidden;
206
- }
207
-
208
- .bodyClipFrame.capped::after {
209
- content: '';
210
- position: absolute;
211
- left: 0;
212
- right: 0;
213
- bottom: 0;
214
- height: 44px;
215
- background: linear-gradient(to bottom, transparent, var(--dees-color-bg-primary));
216
- pointer-events: none;
208
+ overflow-y: auto;
209
+ overflow-x: hidden;
210
+ scrollbar-width: thin;
211
+ scrollbar-color: var(--dees-color-scrollbar-thumb) transparent;
217
212
  }
218
213
 
219
214
  .maximizeBar {
@@ -237,74 +232,6 @@ export class DeesHarnessToolCard extends DeesElement {
237
232
  color: var(--dees-color-link-hover);
238
233
  }
239
234
 
240
- .fullscreenLayer {
241
- position: fixed;
242
- inset: 0;
243
- z-index: 4000;
244
- display: grid;
245
- place-items: center;
246
- padding: 4vh 3vw;
247
- background: color-mix(in srgb, var(--dees-color-bg-primary) 55%, transparent);
248
- backdrop-filter: blur(6px);
249
- }
250
-
251
- .fullscreenPanel {
252
- display: flex;
253
- flex-direction: column;
254
- width: min(1200px, 100%);
255
- height: 100%;
256
- min-height: 0;
257
- border: 1px solid var(--dees-color-border-default);
258
- border-radius: var(--dees-radius-lg);
259
- background: var(--dees-color-bg-primary);
260
- box-shadow: 0 24px 64px rgba(0, 0, 0, 0.4);
261
- overflow: hidden;
262
- }
263
-
264
- .fullscreenHeader {
265
- display: flex;
266
- flex: 0 0 auto;
267
- align-items: center;
268
- gap: var(--dees-spacing-sm);
269
- padding: var(--dees-spacing-sm) var(--dees-spacing-md);
270
- border-bottom: 1px solid var(--dees-color-border-subtle);
271
- }
272
-
273
- .fullscreenTitle {
274
- min-width: 0;
275
- overflow: hidden;
276
- text-overflow: ellipsis;
277
- white-space: nowrap;
278
- font-size: var(--dees-font-control-size-sm, 12px);
279
- font-weight: 650;
280
- }
281
-
282
- .fullscreenClose {
283
- display: grid;
284
- place-items: center;
285
- margin-left: auto;
286
- padding: 6px;
287
- border: 0;
288
- border-radius: var(--dees-radius-md);
289
- background: transparent;
290
- color: var(--dees-color-text-secondary);
291
- cursor: pointer;
292
- }
293
-
294
- .fullscreenClose:hover {
295
- background: var(--dees-color-hover);
296
- color: var(--dees-color-text-primary);
297
- }
298
-
299
- .fullscreenBody {
300
- flex: 1;
301
- min-height: 0;
302
- overflow: auto;
303
- padding: var(--dees-spacing-md);
304
- scrollbar-width: thin;
305
- scrollbar-color: var(--dees-color-scrollbar-thumb) transparent;
306
- }
307
-
308
235
  /* Locates the card after minimizing: a brief highlight pulse. */
309
236
  .harness-card.returnFlash {
310
237
  animation: harness-card-return-flash 1.4s var(--dees-ease-standard);
@@ -385,15 +312,15 @@ export class DeesHarnessToolCard extends DeesElement {
385
312
  </span>
386
313
  </span>
387
314
  </button>
388
- ${this.isOpen && !this.maximized
315
+ ${this.isOpen
389
316
  ? html`
390
317
  <div
391
- class="bodyClipFrame ${this.bodyOverflowing ? 'capped' : ''}"
392
- style=${`max-height: ${this.bodyCapPx}px;`}
318
+ class="bodyClipFrame"
319
+ style=${this.uncapped ? '' : `max-height: ${this.bodyCapPx}px;`}
393
320
  >
394
321
  <div class="body bodyInner">${this.renderBody(call)}</div>
395
322
  </div>
396
- ${this.bodyOverflowing
323
+ ${this.bodyOverflowing && !this.uncapped
397
324
  ? html`
398
325
  <button class="maximizeBar" type="button" @click=${this.openFullscreen}>
399
326
  <dees-icon icon="lucide:Maximize2" iconSize="12"></dees-icon>
@@ -404,36 +331,26 @@ export class DeesHarnessToolCard extends DeesElement {
404
331
  `
405
332
  : ''}
406
333
  </div>
407
- ${this.maximized
408
- ? html`
409
- <div class="fullscreenLayer" @click=${(event: Event) => {
410
- if (event.target === event.currentTarget) this.closeFullscreen();
411
- }}>
412
- <div class="fullscreenPanel">
413
- <header class="fullscreenHeader">
414
- <dees-icon .icon=${descriptor.icon} iconSize="15"></dees-icon>
415
- <span class="fullscreenTitle">${label}${subtitle ? ` · ${subtitle}` : ''}</span>
416
- <button class="fullscreenClose" type="button" @click=${this.closeFullscreen}>
417
- <dees-icon icon="lucide:Minimize2" iconSize="15"></dees-icon>
418
- </button>
419
- </header>
420
- <div class="fullscreenBody">${this.renderBody(call)}</div>
421
- </div>
422
- </div>
423
- `
424
- : ''}
425
334
  `;
426
335
  }
427
336
 
428
337
  private readonly openFullscreen = (): void => {
429
- this.maximized = true;
430
- window.addEventListener('keydown', this.handleFullscreenKeydown);
338
+ if (this.fullscreenPortal) return;
339
+ // Appended to document.body: content-visibility containment in the
340
+ // transcript would trap a position: fixed layer inside this card.
341
+ const portal = document.createElement('dees-harness-tool-fullscreen');
342
+ portal.call = this.enrichedCall;
343
+ portal.toolRegistry = this.toolRegistry;
344
+ portal.addEventListener('tool-fullscreen-close', this.closeFullscreen, { once: true });
345
+ document.body.append(portal);
346
+ this.fullscreenPortal = portal;
431
347
  };
432
348
 
433
349
  private readonly closeFullscreen = (): void => {
434
- window.removeEventListener('keydown', this.handleFullscreenKeydown);
435
- if (!this.maximized) return;
436
- this.maximized = false;
350
+ const portal = this.fullscreenPortal;
351
+ if (!portal) return;
352
+ this.fullscreenPortal = null;
353
+ portal.remove();
437
354
  // The flash locates the card the user was just reading in fullscreen.
438
355
  this.returnFlash = true;
439
356
  requestAnimationFrame(() => {
@@ -441,15 +358,9 @@ export class DeesHarnessToolCard extends DeesElement {
441
358
  });
442
359
  };
443
360
 
444
- private readonly handleFullscreenKeydown = (event: KeyboardEvent): void => {
445
- if (event.key === 'Escape') {
446
- event.preventDefault();
447
- this.closeFullscreen();
448
- }
449
- };
450
-
451
361
  public updated(changedProperties: Map<PropertyKey, unknown>): void {
452
362
  super.updated(changedProperties);
363
+ if (this.uncapped) return;
453
364
  const bodyInner = this.shadowRoot?.querySelector('.bodyInner') ?? null;
454
365
  if (bodyInner === this.observedBodyInner) return;
455
366
  this.bodyResizeObserver?.disconnect();
@@ -468,7 +379,8 @@ export class DeesHarnessToolCard extends DeesElement {
468
379
  }
469
380
 
470
381
  public async disconnectedCallback(): Promise<void> {
471
- window.removeEventListener('keydown', this.handleFullscreenKeydown);
382
+ this.fullscreenPortal?.remove();
383
+ this.fullscreenPortal = null;
472
384
  this.bodyResizeObserver?.disconnect();
473
385
  this.bodyResizeObserver = null;
474
386
  this.observedBodyInner = null;
@@ -0,0 +1,168 @@
1
+ import {
2
+ DeesElement,
3
+ html,
4
+ css,
5
+ customElement,
6
+ type TemplateResult,
7
+ property,
8
+ cssManager,
9
+ } from '@design.estate/dees-element';
10
+ import { themeDefaultStyles } from '../../00theme.js';
11
+ import type { IHarnessToolCall } from '../interfaces.js';
12
+ import { DeesHarnessToolRegistry, parseMcpName } from '../toolregistry.js';
13
+ import '../../00group-utility/dees-icon/dees-icon.js';
14
+
15
+ declare global {
16
+ interface HTMLElementTagNameMap {
17
+ 'dees-harness-tool-fullscreen': DeesHarnessToolFullscreen;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Fullscreen view of one tool card, appended to document.body by
23
+ * DeesHarnessToolCard so it escapes every containment context
24
+ * (content-visibility, transforms) that would trap a position: fixed layer
25
+ * inside the transcript. Emits `tool-fullscreen-close` on Escape, backdrop
26
+ * press, or the minimize button; the owning card removes it and flashes.
27
+ */
28
+ @customElement('dees-harness-tool-fullscreen')
29
+ export class DeesHarnessToolFullscreen extends DeesElement {
30
+ @property({ attribute: false })
31
+ accessor call: IHarnessToolCall = { id: '', name: '', status: 'pending' };
32
+
33
+ @property({ attribute: false })
34
+ accessor toolRegistry: DeesHarnessToolRegistry = DeesHarnessToolRegistry.default;
35
+
36
+ public static styles = [
37
+ themeDefaultStyles,
38
+ cssManager.defaultStyles,
39
+ css`
40
+ :host {
41
+ position: fixed;
42
+ inset: 0;
43
+ z-index: 4000;
44
+ display: grid;
45
+ place-items: center;
46
+ padding: 4vh 3vw;
47
+ background: color-mix(in srgb, var(--dees-color-bg-primary) 55%, transparent);
48
+ backdrop-filter: blur(6px);
49
+ font-family: var(--dees-font-family);
50
+ }
51
+
52
+ .panel {
53
+ display: flex;
54
+ flex-direction: column;
55
+ width: min(1200px, 100%);
56
+ height: 100%;
57
+ min-height: 0;
58
+ border: 1px solid var(--dees-color-border-default);
59
+ border-radius: var(--dees-radius-lg);
60
+ background: var(--dees-color-bg-primary);
61
+ box-shadow: 0 24px 64px rgba(0, 0, 0, 0.4);
62
+ overflow: hidden;
63
+ }
64
+
65
+ .panelHeader {
66
+ display: flex;
67
+ flex: 0 0 auto;
68
+ align-items: center;
69
+ gap: var(--dees-spacing-sm);
70
+ padding: var(--dees-spacing-sm) var(--dees-spacing-md);
71
+ border-bottom: 1px solid var(--dees-color-border-subtle);
72
+ color: var(--dees-color-text-primary);
73
+ }
74
+
75
+ .panelTitle {
76
+ min-width: 0;
77
+ overflow: hidden;
78
+ text-overflow: ellipsis;
79
+ white-space: nowrap;
80
+ font-size: var(--dees-font-control-size-sm, 12px);
81
+ font-weight: 650;
82
+ }
83
+
84
+ .panelClose {
85
+ display: grid;
86
+ place-items: center;
87
+ margin-left: auto;
88
+ padding: 6px;
89
+ border: 0;
90
+ border-radius: var(--dees-radius-md);
91
+ background: transparent;
92
+ color: var(--dees-color-text-secondary);
93
+ cursor: pointer;
94
+ }
95
+
96
+ .panelClose:hover {
97
+ background: var(--dees-color-hover);
98
+ color: var(--dees-color-text-primary);
99
+ }
100
+
101
+ .panelBody {
102
+ flex: 1;
103
+ min-height: 0;
104
+ overflow: auto;
105
+ padding: var(--dees-spacing-md);
106
+ scrollbar-width: thin;
107
+ scrollbar-color: var(--dees-color-scrollbar-thumb) transparent;
108
+ }
109
+
110
+ .panelBody dees-harness-tool-card {
111
+ display: block;
112
+ }
113
+ `,
114
+ ];
115
+
116
+ public render(): TemplateResult {
117
+ const parsed = this.call.mcp ? undefined : parseMcpName(this.call.name);
118
+ const enriched = parsed
119
+ ? { ...this.call, mcp: { serverName: parsed.serverName, toolName: parsed.toolName } }
120
+ : this.call;
121
+ const descriptor = this.toolRegistry.resolve(enriched);
122
+ const label = typeof descriptor.label === 'function' ? descriptor.label(enriched) : descriptor.label;
123
+ const subtitle = descriptor.subtitle?.(enriched) ?? '';
124
+ return html`
125
+ <div class="panel" @click=${(event: Event) => event.stopPropagation()}>
126
+ <header class="panelHeader">
127
+ <dees-icon .icon=${descriptor.icon} iconSize="15"></dees-icon>
128
+ <span class="panelTitle">${label}${subtitle ? ` · ${subtitle}` : ''}</span>
129
+ <button class="panelClose" type="button" @click=${this.emitClose}>
130
+ <dees-icon icon="lucide:Minimize2" iconSize="15"></dees-icon>
131
+ </button>
132
+ </header>
133
+ <div class="panelBody">
134
+ <dees-harness-tool-card
135
+ .call=${this.call}
136
+ .toolRegistry=${this.toolRegistry}
137
+ .expanded=${true}
138
+ .outputExpanded=${true}
139
+ uncapped
140
+ ></dees-harness-tool-card>
141
+ </div>
142
+ </div>
143
+ `;
144
+ }
145
+
146
+ public firstUpdated(): void {
147
+ window.addEventListener('keydown', this.handleKeydown);
148
+ this.addEventListener('click', (event) => {
149
+ if (event.target === this) this.emitClose();
150
+ });
151
+ }
152
+
153
+ private readonly handleKeydown = (event: KeyboardEvent): void => {
154
+ if (event.key === 'Escape') {
155
+ event.preventDefault();
156
+ this.emitClose();
157
+ }
158
+ };
159
+
160
+ private readonly emitClose = (): void => {
161
+ this.dispatchEvent(new CustomEvent('tool-fullscreen-close', { bubbles: false }));
162
+ };
163
+
164
+ public async disconnectedCallback(): Promise<void> {
165
+ window.removeEventListener('keydown', this.handleKeydown);
166
+ await super.disconnectedCallback();
167
+ }
168
+ }