@design.estate/dees-catalog 12.0.0 → 12.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_bundle/bundle.js +1712 -1223
- package/dist_bundle/bundle.js.map +1 -1
- package/dist_ts_web/00_commitinfo_data.js +3 -3
- package/dist_ts_web/elements/00group-harness/dees-harness-session-sidebar/dees-harness-session-sidebar.js +3 -107
- package/dist_ts_web/elements/00group-harness/dees-harness-sidebar/dees-harness-sidebar.d.ts +70 -0
- package/dist_ts_web/elements/00group-harness/dees-harness-sidebar/dees-harness-sidebar.demo.d.ts +1 -0
- package/dist_ts_web/elements/00group-harness/dees-harness-sidebar/dees-harness-sidebar.demo.js +78 -0
- package/dist_ts_web/elements/00group-harness/dees-harness-sidebar/dees-harness-sidebar.js +261 -0
- package/dist_ts_web/elements/00group-harness/dees-harness-sidebar/index.d.ts +1 -0
- package/dist_ts_web/elements/00group-harness/dees-harness-sidebar/index.js +2 -0
- package/dist_ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.d.ts +101 -7
- package/dist_ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.demo.js +38 -7
- package/dist_ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.js +252 -22
- package/dist_ts_web/elements/00group-harness/harness.styles.d.ts +10 -0
- package/dist_ts_web/elements/00group-harness/harness.styles.js +116 -1
- package/dist_ts_web/elements/00group-harness/index.d.ts +1 -0
- package/dist_ts_web/elements/00group-harness/index.js +2 -1
- package/package.json +3 -3
- package/readme.md +49 -1
- package/ts_web/00_commitinfo_data.ts +3 -3
- package/ts_web/elements/00group-harness/dees-harness-session-sidebar/dees-harness-session-sidebar.ts +2 -106
- package/ts_web/elements/00group-harness/dees-harness-sidebar/dees-harness-sidebar.demo.ts +87 -0
- package/ts_web/elements/00group-harness/dees-harness-sidebar/dees-harness-sidebar.ts +245 -0
- package/ts_web/elements/00group-harness/dees-harness-sidebar/index.ts +1 -0
- package/ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.demo.ts +48 -12
- package/ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.ts +283 -22
- package/ts_web/elements/00group-harness/harness.styles.ts +116 -0
- package/ts_web/elements/00group-harness/index.ts +1 -0
package/readme.md
CHANGED
|
@@ -174,6 +174,7 @@ task guide and examples deliberately; the generated table does not replace them.
|
|
|
174
174
|
| `dees-harness-reasoning` | harness | [DeesHarnessReasoning](#deesharnessreasoning) | [Source](ts_web/elements/00group-harness/dees-harness-reasoning/dees-harness-reasoning.ts) |
|
|
175
175
|
| `dees-harness-session-list` | harness | [DeesHarnessSessionList](#deesharnesssessionlist) | [Source](ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.ts) |
|
|
176
176
|
| `dees-harness-session-sidebar` | harness | [DeesHarnessSessionSidebar](#deesharnesssessionsidebar) | [Source](ts_web/elements/00group-harness/dees-harness-session-sidebar/dees-harness-session-sidebar.ts) |
|
|
177
|
+
| `dees-harness-sidebar` | harness | [DeesHarnessSidebar](#deesharnesssidebar) | [Source](ts_web/elements/00group-harness/dees-harness-sidebar/dees-harness-sidebar.ts) |
|
|
177
178
|
| `dees-harness-status` | harness | [DeesHarnessStatus](#deesharnessstatus) | [Source](ts_web/elements/00group-harness/dees-harness-status/dees-harness-status.ts) |
|
|
178
179
|
| `dees-harness-todos` | harness | [DeesHarnessTodos](#deesharnesstodos) | [Source](ts_web/elements/00group-harness/dees-harness-todos/dees-harness-todos.ts) |
|
|
179
180
|
| `dees-harness-tool-card` | harness | [DeesHarnessToolCard](#deesharnesstoolcard) | [Source](ts_web/elements/00group-harness/dees-harness-tool-card/dees-harness-tool-card.ts) |
|
|
@@ -2754,10 +2755,28 @@ Token usage chip (`in · out · total`, `compact` shows the total only); cache r
|
|
|
2754
2755
|
|
|
2755
2756
|
#### `DeesTerminalView`
|
|
2756
2757
|
|
|
2757
|
-
Transport-agnostic xterm.js surface. The host pipes remote PTY output in with `write(data)` and receives keystrokes as `terminal-input` (`{ data }`); there is no process, runtime or socket coupling. `terminal-resize` (`{ rows, cols }`) reports the grid after every refit so the host can negotiate a PTY size, and `currentSize` reads it back (`undefined` while no terminal is live). `fontSize` sets the grid font, `disabled` stops `terminal-input` from being emitted, and `focus()`
|
|
2758
|
+
Transport-agnostic xterm.js surface. The host pipes remote PTY output in with `write(data)` and receives keystrokes as `terminal-input` (`{ data }`); there is no process, runtime or socket coupling. `terminal-resize` (`{ rows, cols }`) reports the grid after every refit so the host can negotiate a PTY size, and `currentSize` reads it back (`undefined` while no terminal is live). `fontSize` sets the grid font, `disabled` stops `terminal-input` from being emitted, and `focus()` moves the caret into the terminal's input surface. xterm is loaded through `DeesServiceLibLoader`; CSP-locked or offline hosts call `provideXtermModules()` first to supply their own bundled copy instead of the CDN.
|
|
2758
2759
|
|
|
2759
2760
|
`write()` is safe before the terminal exists and while the element is detached: output is buffered and replayed in order on the next connection, bounded by the exported `maxPendingWriteBytes` (8 MiB — `Uint8Array` chunks count their `byteLength`, strings their UTF-16 memory). Past that bound the oldest chunks are dropped and the replay emits RIS first, so a truncated prefix is never rendered as if it were contiguous. `clear()` resets screen and scrollback through the write stream (RIS, `ESC c`) rather than around it, so it is ordered against output already queued in xterm — write, `clear()`, write leaves only the later output on screen.
|
|
2760
2761
|
|
|
2762
|
+
`restore(data, { cols, rows })` shows a serialized terminal state, for example one the host keeps with xterm's serialize addon, in one step, so reattaching to a long-running terminal does not replay its history in front of the user. A snapshot is exact only at the grid it was taken at, so the terminal is fully reset (screen, scrollback, modes, alternate buffer), set to that grid, fed the snapshot while invisible, then fitted to its container and revealed. `terminal-resize` fires at the end when the fitted grid differs from the snapshot's, which is the host's cue to resize the PTY so the application redraws. Until then `currentSize` reports the snapshot's grid, so the closing event, or the resolved promise, is what a host negotiates a PTY size on. The terminal is hidden with `opacity`, so it keeps focus and keeps forwarding input meanwhile. The restore is part of the output stream: `write()` calls issued after it are applied after the snapshot, at the fitted size, so the host does not wait before streaming live output from the snapshot's offset. The resize observer does not refit until the snapshot is parsed. Before the terminal exists, a restore is buffered like `write()`: it replaces everything buffered before it and counts towards `maxPendingWriteBytes`. It is the oldest buffered entry, so it is dropped first, and a snapshot that alone exceeds the bound is dropped by the next buffered write.
|
|
2763
|
+
|
|
2764
|
+
The promise resolves once the snapshot is on screen. It also resolves, without revealing anything, when a newer `restore()` supersedes it, or when `clear()` replaces it while it is still buffered. It rejects when the element is disconnected while the snapshot is being parsed (the terminal and the snapshot are disposed together, as with any live output), when output buffered after it pushes it out of the buffer, or when xterm refuses the data (`terminal-error` carries the same error). None of these surfaces as an unhandled rejection. A size that is not an integer `cols` in 2–65535 and `rows` in 1–65535 is a programming error: the promise rejects with a `RangeError` and nothing changes; unlike the reasons above this one is not pre-handled, so a `restore()` nobody observes reports it as an unhandled rejection. There is a fourth outcome: a restore that is still buffered when the element is disconnected stays pending, because the snapshot survives the move like buffered output and is applied on the next connection — a host that discards the element instead of reattaching it must not await that promise.
|
|
2765
|
+
|
|
2766
|
+
```typescript
|
|
2767
|
+
// Reattach through the host's own transport: paint the snapshot, then keep writing
|
|
2768
|
+
// live output from the stream offset the snapshot was taken at.
|
|
2769
|
+
const snapshot = await transport.attachTerminal(terminalId);
|
|
2770
|
+
// Not awaited, so live output queues behind the snapshot. A rejected restore leaves only
|
|
2771
|
+
// the live tail on screen, so take a fresh snapshot rather than ignore the rejection.
|
|
2772
|
+
view
|
|
2773
|
+
.restore(snapshot.data, { cols: snapshot.cols, rows: snapshot.rows })
|
|
2774
|
+
.catch(() => transport.reattachTerminal(terminalId));
|
|
2775
|
+
transport.streamOutput(terminalId, snapshot.offset, (chunk) => view.write(chunk));
|
|
2776
|
+
```
|
|
2777
|
+
|
|
2778
|
+
`focus()` is the call a host makes when the user selects a terminal — after a click, or after `Enter`/`Space` on a row — so the next keystroke goes to the pty instead of the page. Because the terminal is created asynchronously on first connection, a `focus()` that arrives before it exists is remembered and applied exactly once when it becomes ready; `focusPending` reads that state back. The request belongs to the connection it was made in and is dropped on disconnect, so an element moved between containers never grabs the caret back from wherever the user went — issue a fresh `focus()` after the move. Nothing else moves focus: re-renders, buffered output and background writes never steal it.
|
|
2779
|
+
|
|
2761
2780
|
`ready` resolves once the terminal of the current connection is live; a fresh promise is created per connection, so a reattached element can be awaited again. Every connection builds a terminal and every disconnection disposes it, so an element moved between containers, or returned to, is a live surface rather than a dead one; buffered output survives the move. A failed xterm load is not an unhandled rejection: it dispatches the bubbling, composed `terminal-error` (`ITerminalErrorDetail { error }`), rejects `ready` with the same error, leaves the element writable, and is retried on the next connection. xterm's own flow-control refusal is reported the same way. Sizing comes from the host element — give it a bounded height, as the demo does.
|
|
2762
2781
|
|
|
2763
2782
|
#### `DeesHarnessSessionSidebar`
|
|
@@ -2768,6 +2787,35 @@ Scratchpad edits emit `harness-scratchpad-save` with `{ text, expectedRevision }
|
|
|
2768
2787
|
|
|
2769
2788
|
Direct sidebar hosts can set `intelligenceAvailabilityStatus`, `intelligenceUnavailableReason`, and `intelligenceHeading`; `intelligenceEnabled` still controls whether the section exists. The properties default to `available`, an empty reason, and `Session Intelligence`. A blank heading falls back to `Session Intelligence`, and unavailable state without a reason renders `<heading> is unavailable.` Only `available` permits `harness-session-intelligence-ask`; checking and unavailable states retain history and expose accessible status text. The availability union is exported as `THarnessIntelligenceAvailabilityStatus`.
|
|
2770
2789
|
|
|
2790
|
+
#### `DeesHarnessSidebar`
|
|
2791
|
+
|
|
2792
|
+
The sidebar shell behind [DeesHarnessSessionSidebar](#deesharnesssessionsidebar)'s chrome, with host-declared sections instead of session-specific ones: the same paddings, heading rows, chevron affordance, typography and single-open accordion, for a panel the catalog knows nothing about. It owns the accordion and nothing else — every section body is the host's own content, slotted as `slot="section-<key>"`, so the host keeps its state, its updates and its presentation.
|
|
2793
|
+
|
|
2794
|
+
Set `.sections` to `IHarnessSidebarSection[]`, where each section is `{ key, label, icon?, badge? }`: `key` is the section's stable identity and names its content slot, `icon` renders before the label (e.g. `lucide:Gauge`), and `badge` is the right-aligned summary the session panel uses for counts. Sections render in array order, and a section whose slot holds no content is not offered at all — no heading row, no expandable region — so the divider follows the last visible section and a panel can declare more sections than the host currently fills.
|
|
2795
|
+
|
|
2796
|
+
`expandedSection` is the key of the open section. It is host-settable and reflects what the user opened; it defaults to the first section that has content. A user toggle emits `harness-sidebar-section-change` with `IHarnessSidebarSectionChangeDetail` (`{ expandedSection }`), where `undefined` means the user collapsed the panel — it stays collapsed until something opens a section again. Host writes never echo an event, so a host that binds the property should take the event's value back into its own state to keep the two in step. A section that has no content yet stays collapsed without losing the key and opens the moment its body is slotted, so a body the host renders conditionally costs neither the host nor the user their choice; only withdrawing the open section from `sections` moves the panel on to the first populated section. `disabled` freezes the accordion with its headings still visible.
|
|
2797
|
+
|
|
2798
|
+
Section bodies stay mounted while collapsed and keep their identity across renders, so a live panel behaves: updating `sections` for a new badge, or mutating the slotted content itself, neither collapses the open section nor resets its scroll position. The host mounts and unmounts the element to show or hide the panel — there is no toggle button or open animation — and an element that is re-mounted reopens the section it had. Give the panel a bounded height from its container, as the session panel's column does; the element fills it and scrolls the expanded body.
|
|
2799
|
+
|
|
2800
|
+
```typescript
|
|
2801
|
+
import type { IHarnessSidebarSection, IHarnessSidebarSectionChangeDetail } from '@design.estate/dees-catalog';
|
|
2802
|
+
|
|
2803
|
+
const sections: IHarnessSidebarSection[] = [
|
|
2804
|
+
{ key: 'screencast', label: 'Screencast', icon: 'lucide:MonitorPlay', badge: '4.4 ms' },
|
|
2805
|
+
];
|
|
2806
|
+
|
|
2807
|
+
html`
|
|
2808
|
+
<dees-harness-sidebar
|
|
2809
|
+
.sections=${sections}
|
|
2810
|
+
@harness-sidebar-section-change=${(event: CustomEvent<IHarnessSidebarSectionChangeDetail>) => {
|
|
2811
|
+
this.openSection = event.detail.expandedSection;
|
|
2812
|
+
}}
|
|
2813
|
+
>
|
|
2814
|
+
<div slot="section-screencast">${this.renderScreencastStats()}</div>
|
|
2815
|
+
</dees-harness-sidebar>
|
|
2816
|
+
`;
|
|
2817
|
+
```
|
|
2818
|
+
|
|
2771
2819
|
#### `DeesHarnessConversationPicker`
|
|
2772
2820
|
|
|
2773
2821
|
Keyboard-first picker for one conversation, or one attachable resource, used wherever a menu would otherwise list every possible target. It is presentation only: it never mutates a session, a resource, or an association — it resolves one choice and leaves the change to the host.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* autocreated commitinfo by @push.rocks/commitinfo
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
|
-
name:
|
|
6
|
-
version:
|
|
7
|
-
description:
|
|
5
|
+
name: "@design.estate/dees-catalog",
|
|
6
|
+
version: "12.2.0",
|
|
7
|
+
description: "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript."
|
|
8
8
|
}
|
package/ts_web/elements/00group-harness/dees-harness-session-sidebar/dees-harness-session-sidebar.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
directives,
|
|
12
12
|
} from '@design.estate/dees-element';
|
|
13
13
|
import { themeDefaultStyles } from '../../00theme.js';
|
|
14
|
-
import { harnessControlStyles } from '../harness.styles.js';
|
|
14
|
+
import { harnessControlStyles, harnessSidebarStyles } from '../harness.styles.js';
|
|
15
15
|
import type {
|
|
16
16
|
IHarnessIntelligenceExchange,
|
|
17
17
|
IHarnessMessage,
|
|
@@ -135,112 +135,8 @@ export class DeesHarnessSessionSidebar extends DeesElement {
|
|
|
135
135
|
themeDefaultStyles,
|
|
136
136
|
cssManager.defaultStyles,
|
|
137
137
|
harnessControlStyles,
|
|
138
|
+
harnessSidebarStyles,
|
|
138
139
|
css`
|
|
139
|
-
:host {
|
|
140
|
-
display: flex;
|
|
141
|
-
flex-direction: column;
|
|
142
|
-
min-width: 0;
|
|
143
|
-
min-height: 0;
|
|
144
|
-
height: 100%;
|
|
145
|
-
font-family: var(--dees-font-family);
|
|
146
|
-
color: var(--dees-color-text-primary);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
.sidebar {
|
|
150
|
-
display: flex;
|
|
151
|
-
flex-direction: column;
|
|
152
|
-
min-height: 0;
|
|
153
|
-
height: 100%;
|
|
154
|
-
overflow: hidden;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
.section {
|
|
158
|
-
display: flex;
|
|
159
|
-
flex: 0 0 auto;
|
|
160
|
-
flex-direction: column;
|
|
161
|
-
min-height: 0;
|
|
162
|
-
overflow: hidden;
|
|
163
|
-
border-bottom: 1px solid var(--dees-color-border-subtle);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
.section.expanded {
|
|
167
|
-
flex: 1 1 0;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
.section:last-child {
|
|
171
|
-
border-bottom: 0;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
.sectionHeading {
|
|
175
|
-
display: flex;
|
|
176
|
-
flex: 0 0 auto;
|
|
177
|
-
align-items: center;
|
|
178
|
-
gap: var(--dees-spacing-xs);
|
|
179
|
-
width: 100%;
|
|
180
|
-
min-height: 36px;
|
|
181
|
-
margin: 0;
|
|
182
|
-
padding: 6px 12px 6px 8px;
|
|
183
|
-
border: 0;
|
|
184
|
-
background: var(--dees-color-bg-canvas);
|
|
185
|
-
color: var(--dees-color-text-primary);
|
|
186
|
-
font-family: inherit;
|
|
187
|
-
font-size: var(--dees-font-control-size-sm, 12px);
|
|
188
|
-
font-weight: 600;
|
|
189
|
-
text-align: left;
|
|
190
|
-
cursor: pointer;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
.sectionHeading:hover {
|
|
194
|
-
background: var(--dees-color-badge-default-bg);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
.sectionHeading:focus-visible {
|
|
198
|
-
position: relative;
|
|
199
|
-
z-index: 1;
|
|
200
|
-
outline: 2px solid var(--dees-color-accent-primary);
|
|
201
|
-
outline-offset: -2px;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
.sectionHeading dees-icon {
|
|
205
|
-
flex: 0 0 auto;
|
|
206
|
-
color: var(--dees-color-text-secondary);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
.sectionHeadingText {
|
|
210
|
-
min-width: 0;
|
|
211
|
-
flex-shrink: 0;
|
|
212
|
-
overflow: hidden;
|
|
213
|
-
text-overflow: ellipsis;
|
|
214
|
-
white-space: nowrap;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
.sectionSummary {
|
|
218
|
-
flex: 0 1 auto;
|
|
219
|
-
min-width: 0;
|
|
220
|
-
overflow: hidden;
|
|
221
|
-
text-overflow: ellipsis;
|
|
222
|
-
white-space: nowrap;
|
|
223
|
-
margin-left: auto;
|
|
224
|
-
color: var(--dees-color-text-muted);
|
|
225
|
-
font-size: 11px;
|
|
226
|
-
font-variant-numeric: tabular-nums;
|
|
227
|
-
font-weight: 500;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
.sectionContent {
|
|
231
|
-
flex: 1;
|
|
232
|
-
min-height: 0;
|
|
233
|
-
overflow-x: hidden;
|
|
234
|
-
overflow-y: auto;
|
|
235
|
-
padding: var(--dees-spacing-sm) var(--dees-spacing-md);
|
|
236
|
-
scrollbar-width: thin;
|
|
237
|
-
scrollbar-color: var(--dees-color-scrollbar-thumb) transparent;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
.sectionContent[hidden] {
|
|
241
|
-
display: none;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
140
|
.sectionContent dees-harness-todos {
|
|
245
141
|
min-width: 0;
|
|
246
142
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { html } from '@design.estate/dees-element';
|
|
2
|
+
import { demoPage, demoOutput, showDemoResult } from '../../../demos/templates.js';
|
|
3
|
+
import type {
|
|
4
|
+
IHarnessSidebarSection,
|
|
5
|
+
IHarnessSidebarSectionChangeDetail,
|
|
6
|
+
} from './dees-harness-sidebar.js';
|
|
7
|
+
|
|
8
|
+
// `notes` is declared without slotted content, so the panel does not offer it.
|
|
9
|
+
const sections: IHarnessSidebarSection[] = [
|
|
10
|
+
{ key: 'screencast', label: 'Screencast', icon: 'lucide:MonitorPlay', badge: '4.4 ms' },
|
|
11
|
+
{ key: 'transport', label: 'Transport', icon: 'lucide:Activity', badge: 'webrtc' },
|
|
12
|
+
{ key: 'notes', label: 'Notes', icon: 'lucide:NotebookPen' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
let sampleCount = 0;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A live panel updates its rows and badges in place: the host owns the values,
|
|
19
|
+
* so nothing collapses and the open section keeps its scroll position.
|
|
20
|
+
*/
|
|
21
|
+
const sampleCounters = (eventArg: Event): void => {
|
|
22
|
+
const scope = (eventArg.currentTarget as HTMLElement).closest('.harnessSidebarDemo')!;
|
|
23
|
+
sampleCount += 1;
|
|
24
|
+
const decodeMs = (4.4 + (sampleCount % 6) * 0.3).toFixed(1);
|
|
25
|
+
scope.querySelector('[data-metric="frames"]')!.textContent = `${1_440 + sampleCount * 24}`;
|
|
26
|
+
scope.querySelector('[data-metric="decode"]')!.textContent = `${decodeMs} ms`;
|
|
27
|
+
scope.querySelector('dees-harness-sidebar')!.sections = sections.map((section) =>
|
|
28
|
+
section.key === 'screencast' ? { ...section, badge: `${decodeMs} ms` } : section,
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const demoFunc = () => demoPage('Sidebar', 'A host-composed panel of collapsible sections in the harness sidebar style: the shell owns the accordion, every body is slotted content. One section is declared without content and is therefore not offered.', html`
|
|
33
|
+
<style>
|
|
34
|
+
.harnessSidebarPanel {
|
|
35
|
+
width: min(100%, 300px);
|
|
36
|
+
height: 420px;
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
border: 1px solid var(--dees-color-border-subtle);
|
|
39
|
+
border-radius: var(--dees-radius-xl);
|
|
40
|
+
}
|
|
41
|
+
.sidebarRow {
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: baseline;
|
|
44
|
+
justify-content: space-between;
|
|
45
|
+
gap: var(--dees-spacing-md);
|
|
46
|
+
color: var(--dees-color-text-secondary);
|
|
47
|
+
}
|
|
48
|
+
.sidebarRow + .sidebarRow {
|
|
49
|
+
margin-top: 6px;
|
|
50
|
+
}
|
|
51
|
+
.sidebarValue {
|
|
52
|
+
color: var(--dees-color-text-primary);
|
|
53
|
+
font-family: var(--dees-font-family-mono);
|
|
54
|
+
font-variant-numeric: tabular-nums;
|
|
55
|
+
}
|
|
56
|
+
</style>
|
|
57
|
+
<div class="harnessSidebarDemo dees-demo-stack">
|
|
58
|
+
<div class="dees-demo-row">
|
|
59
|
+
<dees-button @clicked=${sampleCounters}>Sample the counters</dees-button>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="harnessSidebarPanel">
|
|
62
|
+
<dees-harness-sidebar
|
|
63
|
+
.sections=${sections}
|
|
64
|
+
@harness-sidebar-section-change=${(event: CustomEvent<IHarnessSidebarSectionChangeDetail>) => {
|
|
65
|
+
showDemoResult((event.currentTarget as Element).closest('.harnessSidebarDemo')!, event.detail);
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
<div slot="section-screencast">
|
|
69
|
+
<div class="sidebarRow"><span>Codec</span><span class="sidebarValue">h264</span></div>
|
|
70
|
+
<div class="sidebarRow"><span>Encoder</span><span class="sidebarValue">hardware</span></div>
|
|
71
|
+
<div class="sidebarRow">
|
|
72
|
+
<span>Decode</span><span class="sidebarValue" data-metric="decode">4.4 ms</span>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="sidebarRow">
|
|
75
|
+
<span>Frames</span><span class="sidebarValue" data-metric="frames">1440</span>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="sidebarRow"><span>GPU compositing</span><span class="sidebarValue">on</span></div>
|
|
78
|
+
</div>
|
|
79
|
+
<div slot="section-transport">
|
|
80
|
+
<div class="sidebarRow"><span>Channel</span><span class="sidebarValue">datachannel</span></div>
|
|
81
|
+
<div class="sidebarRow"><span>Round trip</span><span class="sidebarValue">18 ms</span></div>
|
|
82
|
+
</div>
|
|
83
|
+
</dees-harness-sidebar>
|
|
84
|
+
</div>
|
|
85
|
+
${demoOutput()}
|
|
86
|
+
</div>
|
|
87
|
+
`);
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { demoFunc } from './dees-harness-sidebar.demo.js';
|
|
2
|
+
import {
|
|
3
|
+
DeesElement,
|
|
4
|
+
html,
|
|
5
|
+
css,
|
|
6
|
+
customElement,
|
|
7
|
+
type TemplateResult,
|
|
8
|
+
property,
|
|
9
|
+
state,
|
|
10
|
+
cssManager,
|
|
11
|
+
directives,
|
|
12
|
+
} from '@design.estate/dees-element';
|
|
13
|
+
import { themeDefaultStyles } from '../../00theme.js';
|
|
14
|
+
import { harnessSidebarStyles } from '../harness.styles.js';
|
|
15
|
+
import '../../00group-utility/dees-icon/dees-icon.js';
|
|
16
|
+
|
|
17
|
+
const { repeat } = directives;
|
|
18
|
+
|
|
19
|
+
/** Every section body arrives through `slot="section-<key>"`. */
|
|
20
|
+
const sectionSlotPrefix = 'section-';
|
|
21
|
+
const sectionSlotName = (keyArg: string): string => `${sectionSlotPrefix}${keyArg}`;
|
|
22
|
+
|
|
23
|
+
const sameKeys = (leftArg: ReadonlySet<string>, rightArg: ReadonlySet<string>): boolean =>
|
|
24
|
+
leftArg.size === rightArg.size && [...leftArg].every((keyArg) => rightArg.has(keyArg));
|
|
25
|
+
|
|
26
|
+
declare global {
|
|
27
|
+
interface HTMLElementTagNameMap {
|
|
28
|
+
'dees-harness-sidebar': DeesHarnessSidebar;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* One collapsible section of a sidebar panel. The element renders the heading
|
|
34
|
+
* row; the body is the host's own content, slotted as `slot="section-<key>"`.
|
|
35
|
+
*/
|
|
36
|
+
export interface IHarnessSidebarSection {
|
|
37
|
+
/** Stable identity, unique within `sections`; also names the content slot. */
|
|
38
|
+
key: string;
|
|
39
|
+
label: string;
|
|
40
|
+
/** Icon shown before the label, e.g. `lucide:Gauge`. */
|
|
41
|
+
icon?: string;
|
|
42
|
+
/** Right-aligned summary such as a count, a rate, or a live value. */
|
|
43
|
+
badge?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Detail of `harness-sidebar-section-change`: the user opened another section
|
|
48
|
+
* or collapsed the open one.
|
|
49
|
+
*/
|
|
50
|
+
export interface IHarnessSidebarSectionChangeDetail {
|
|
51
|
+
/** Key of the now-open section, or `undefined` when nothing is open. */
|
|
52
|
+
expandedSection: string | undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Sidebar shell: the panel chrome of `dees-harness-session-sidebar` with
|
|
57
|
+
* host-declared sections instead of session-specific ones. It owns the
|
|
58
|
+
* single-open accordion and nothing else — every section body is slotted
|
|
59
|
+
* content, so the host keeps its own state, updates, and presentation. A
|
|
60
|
+
* section whose slot holds no content is not offered at all, and the host
|
|
61
|
+
* mounts and unmounts the element to show or hide the panel.
|
|
62
|
+
*/
|
|
63
|
+
@customElement('dees-harness-sidebar')
|
|
64
|
+
export class DeesHarnessSidebar extends DeesElement {
|
|
65
|
+
public static demo = demoFunc;
|
|
66
|
+
public static demoGroups = ['Agent Chat'];
|
|
67
|
+
|
|
68
|
+
/** Sections in render order; each one's body is slotted as `section-<key>`. */
|
|
69
|
+
@property({ attribute: false })
|
|
70
|
+
accessor sections: IHarnessSidebarSection[] = [];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Key of the open section. Host-settable and kept in sync with what the user
|
|
74
|
+
* opens; it defaults to the first section that has content, and `undefined`
|
|
75
|
+
* leaves the panel collapsed once a section has been opened. A declared
|
|
76
|
+
* section without content renders collapsed and opens as soon as its content
|
|
77
|
+
* arrives, so the key survives an asynchronous body.
|
|
78
|
+
*/
|
|
79
|
+
@property()
|
|
80
|
+
accessor expandedSection: string | undefined = undefined;
|
|
81
|
+
|
|
82
|
+
/** Freezes the accordion: headings stay visible but cannot be toggled. */
|
|
83
|
+
@property({ type: Boolean })
|
|
84
|
+
accessor disabled: boolean = false;
|
|
85
|
+
|
|
86
|
+
@state()
|
|
87
|
+
private accessor populatedSections: ReadonlySet<string> = new Set<string>();
|
|
88
|
+
|
|
89
|
+
private sectionStateInitialized = false;
|
|
90
|
+
|
|
91
|
+
public static styles = [
|
|
92
|
+
themeDefaultStyles,
|
|
93
|
+
cssManager.defaultStyles,
|
|
94
|
+
harnessSidebarStyles,
|
|
95
|
+
css`
|
|
96
|
+
:host {
|
|
97
|
+
background: var(--dees-color-bg-canvas);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* A section without content is not offered at all: the hidden attribute
|
|
101
|
+
alone would still be laid out, because the shared chrome's display
|
|
102
|
+
declaration wins over the user-agent rule. The divider then follows the
|
|
103
|
+
last visible section instead of the last declared one. */
|
|
104
|
+
.section[hidden] {
|
|
105
|
+
display: none;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.section:not(:has(~ .section:not([hidden]))) {
|
|
109
|
+
border-bottom: 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Section bodies stay as compact as the session panel's; slotted content
|
|
113
|
+
inherits this and can override it per element. */
|
|
114
|
+
.sectionContent {
|
|
115
|
+
font-size: var(--dees-font-caption1-size, 11px);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.sectionHeading:disabled {
|
|
119
|
+
opacity: 0.62;
|
|
120
|
+
cursor: default;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.sectionHeading:disabled:hover {
|
|
124
|
+
background: var(--dees-color-bg-canvas);
|
|
125
|
+
}
|
|
126
|
+
`,
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
protected willUpdate(changedPropertiesArg: Map<PropertyKey, unknown>): void {
|
|
130
|
+
if (changedPropertiesArg.has('expandedSection')) this.sectionStateInitialized = true;
|
|
131
|
+
const populated = this.populatedSectionsInOrder;
|
|
132
|
+
// Slot assignment is only known after the first render, so an empty panel
|
|
133
|
+
// never overrides a host-set section.
|
|
134
|
+
if (populated.length === 0) return;
|
|
135
|
+
if (!this.sectionStateInitialized) {
|
|
136
|
+
this.expandedSection = populated[0].key;
|
|
137
|
+
this.sectionStateInitialized = true;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const open = this.expandedSection;
|
|
141
|
+
// A section the host withdrew from `sections` can never open again, so the
|
|
142
|
+
// panel moves on. A declared section whose content is momentarily missing
|
|
143
|
+
// keeps the key and renders collapsed until the content is back, so an
|
|
144
|
+
// asynchronous body never costs the host or the user their choice.
|
|
145
|
+
if (open !== undefined && !this.sections.some((section) => section.key === open)) {
|
|
146
|
+
this.expandedSection = populated[0].key;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public render(): TemplateResult {
|
|
151
|
+
return html`
|
|
152
|
+
<div class="sidebar">
|
|
153
|
+
${repeat(
|
|
154
|
+
this.sections,
|
|
155
|
+
(section) => section.key,
|
|
156
|
+
(section) => this.renderSection(section),
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Declared sections that currently have slotted content, in render order. */
|
|
163
|
+
private get populatedSectionsInOrder(): IHarnessSidebarSection[] {
|
|
164
|
+
return this.sections.filter((section) => this.populatedSections.has(section.key));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The heading row is the element's own; the body is the host's, and it stays
|
|
169
|
+
* mounted while collapsed so it keeps its state and scroll position.
|
|
170
|
+
*/
|
|
171
|
+
private renderSection(sectionArg: IHarnessSidebarSection): TemplateResult {
|
|
172
|
+
const populated = this.populatedSections.has(sectionArg.key);
|
|
173
|
+
const expanded = populated && this.expandedSection === sectionArg.key;
|
|
174
|
+
const contentId = `harness-sidebar-section-${sectionArg.key}`;
|
|
175
|
+
return html`
|
|
176
|
+
<section
|
|
177
|
+
class=${`section ${expanded ? 'expanded' : ''}`}
|
|
178
|
+
data-section=${sectionArg.key}
|
|
179
|
+
?hidden=${!populated}
|
|
180
|
+
>
|
|
181
|
+
${populated ? html`
|
|
182
|
+
<button
|
|
183
|
+
class="sectionHeading"
|
|
184
|
+
type="button"
|
|
185
|
+
aria-expanded=${String(expanded)}
|
|
186
|
+
aria-controls=${contentId}
|
|
187
|
+
?disabled=${this.disabled}
|
|
188
|
+
@click=${() => this.toggleSection(sectionArg.key)}
|
|
189
|
+
>
|
|
190
|
+
<dees-icon
|
|
191
|
+
icon=${expanded ? 'lucide:ChevronDown' : 'lucide:ChevronRight'}
|
|
192
|
+
iconSize="13"
|
|
193
|
+
></dees-icon>
|
|
194
|
+
${sectionArg.icon ? html`<dees-icon icon=${sectionArg.icon} iconSize="13"></dees-icon>` : ''}
|
|
195
|
+
<span class="sectionHeadingText">${sectionArg.label}</span>
|
|
196
|
+
${sectionArg.badge ? html`<span class="sectionSummary">${sectionArg.badge}</span>` : ''}
|
|
197
|
+
</button>
|
|
198
|
+
` : ''}
|
|
199
|
+
<div
|
|
200
|
+
id=${contentId}
|
|
201
|
+
class="sectionContent"
|
|
202
|
+
role="region"
|
|
203
|
+
aria-label=${sectionArg.label}
|
|
204
|
+
aria-hidden=${String(!expanded)}
|
|
205
|
+
?hidden=${!expanded}
|
|
206
|
+
>
|
|
207
|
+
<slot
|
|
208
|
+
name=${sectionSlotName(sectionArg.key)}
|
|
209
|
+
@slotchange=${this.handleSectionSlotChange}
|
|
210
|
+
></slot>
|
|
211
|
+
</div>
|
|
212
|
+
</section>
|
|
213
|
+
`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* A section is offered only while its slot holds content, so the panel
|
|
218
|
+
* recomputes from its live slots whenever the host fills or empties one.
|
|
219
|
+
*/
|
|
220
|
+
private readonly handleSectionSlotChange = (): void => {
|
|
221
|
+
const populated = new Set<string>();
|
|
222
|
+
const sectionSlots = this.renderRoot.querySelectorAll<HTMLSlotElement>(
|
|
223
|
+
`slot[name^="${sectionSlotPrefix}"]`,
|
|
224
|
+
);
|
|
225
|
+
for (const slot of sectionSlots) {
|
|
226
|
+
if (slot.assignedElements({ flatten: true }).length > 0) {
|
|
227
|
+
populated.add(slot.name.slice(sectionSlotPrefix.length));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (sameKeys(this.populatedSections, populated)) return;
|
|
231
|
+
this.populatedSections = populated;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
private toggleSection(keyArg: string): void {
|
|
235
|
+
if (this.disabled) return;
|
|
236
|
+
const expandedSection = this.expandedSection === keyArg ? undefined : keyArg;
|
|
237
|
+
this.sectionStateInitialized = true;
|
|
238
|
+
this.expandedSection = expandedSection;
|
|
239
|
+
const detail: IHarnessSidebarSectionChangeDetail = { expandedSection };
|
|
240
|
+
this.dispatchEvent(new CustomEvent<IHarnessSidebarSectionChangeDetail>(
|
|
241
|
+
'harness-sidebar-section-change',
|
|
242
|
+
{ detail, bubbles: true, composed: true },
|
|
243
|
+
));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './dees-harness-sidebar.js';
|
|
@@ -1,9 +1,40 @@
|
|
|
1
1
|
import { html } from '@design.estate/dees-element';
|
|
2
2
|
import { demoPage } from '../../../demos/templates.js';
|
|
3
|
+
import type { ITerminalResizeDetail } from './dees-terminal-view.js';
|
|
4
|
+
|
|
5
|
+
// A serialized terminal state as a host would receive it on reattach, e.g. from xterm's
|
|
6
|
+
// serialize addon: styled rows at the grid the PTY had when it was taken.
|
|
7
|
+
const sampleSnapshotSize: ITerminalResizeDetail = { cols: 64, rows: 12 };
|
|
8
|
+
const samplePrompt = '\x1b[1;32m➜\x1b[0m \x1b[36mdees-catalog\x1b[0m ';
|
|
9
|
+
const sampleSnapshot = [
|
|
10
|
+
`${samplePrompt}pnpm build`,
|
|
11
|
+
'\x1b[2mtsbuild \x1b[0m compiled the web folder',
|
|
12
|
+
'\x1b[2mtsbundle\x1b[0m wrote dist_bundle/bundle.js',
|
|
13
|
+
'',
|
|
14
|
+
`${samplePrompt}pnpm test`,
|
|
15
|
+
' \x1b[32m✔\x1b[0m restore() sizes, parses, fits and reveals',
|
|
16
|
+
' \x1b[32m✔\x1b[0m writes during a restore follow the snapshot',
|
|
17
|
+
' \x1b[32m✔\x1b[0m a newer restore supersedes an older one',
|
|
18
|
+
'',
|
|
19
|
+
samplePrompt,
|
|
20
|
+
].join('\r\n');
|
|
21
|
+
|
|
22
|
+
const restoreSample = (event: Event): void => {
|
|
23
|
+
const view = (event.currentTarget as HTMLElement)
|
|
24
|
+
.closest('.terminalDemoStack')
|
|
25
|
+
?.querySelector('dees-terminal-view');
|
|
26
|
+
if (!view) return;
|
|
27
|
+
// A click handler's promise is dropped by the DOM, so both outcomes are settled here: leaving
|
|
28
|
+
// the demo page mid-restore rejects, and nobody would be left to observe it.
|
|
29
|
+
view.restore(sampleSnapshot, sampleSnapshotSize).then(
|
|
30
|
+
() => view.focus(),
|
|
31
|
+
() => undefined,
|
|
32
|
+
);
|
|
33
|
+
};
|
|
3
34
|
|
|
4
35
|
// A loopback demo: keystrokes echo straight back into the terminal so the
|
|
5
36
|
// component is explorable without a live PTY behind it.
|
|
6
|
-
export const demoFunc = () => demoPage('Terminal', 'A terminal surface with local keyboard echo. Type to try the input and resize behavior.', html`
|
|
37
|
+
export const demoFunc = () => demoPage('Terminal', 'A terminal surface with local keyboard echo. Type to try the input and resize behavior, or restore a serialized snapshot: it is painted invisibly at its own grid and appears fitted, in one step.', html`
|
|
7
38
|
<style>
|
|
8
39
|
.terminalDemo {
|
|
9
40
|
height: 380px;
|
|
@@ -12,16 +43,21 @@ export const demoFunc = () => demoPage('Terminal', 'A terminal surface with loca
|
|
|
12
43
|
overflow: hidden;
|
|
13
44
|
}
|
|
14
45
|
</style>
|
|
15
|
-
<div class="
|
|
16
|
-
<dees-
|
|
17
|
-
@
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
46
|
+
<div class="terminalDemoStack dees-demo-stack">
|
|
47
|
+
<div class="dees-demo-row">
|
|
48
|
+
<dees-button @clicked=${restoreSample}>Restore a snapshot</dees-button>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="terminalDemo">
|
|
51
|
+
<dees-terminal-view
|
|
52
|
+
@terminal-input=${(event: CustomEvent<{ data: string }>) => {
|
|
53
|
+
const view = event.currentTarget as HTMLElementTagNameMap['dees-terminal-view'];
|
|
54
|
+
const data = event.detail.data;
|
|
55
|
+
view.write(data === '\r' ? '\r\n' : data);
|
|
56
|
+
}}
|
|
57
|
+
@terminal-resize=${(event: CustomEvent<ITerminalResizeDetail>) => {
|
|
58
|
+
console.log('terminal size', event.detail);
|
|
59
|
+
}}
|
|
60
|
+
></dees-terminal-view>
|
|
61
|
+
</div>
|
|
26
62
|
</div>
|
|
27
63
|
`);
|