@abreen/tada 1.15.5 → 1.15.7

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.
@@ -380,5 +380,5 @@ export function renderTraceWidgetHtml({
380
380
  return `<div class="trace-source" data-trace-source-file="${escapeAttr(source.file)}"${hidden}>${source.highlightedSource}</div>`;
381
381
  })
382
382
  .join('');
383
- return `<div class="${wrapperClass}"${wrapperAttrs}><noscript><p>This interactive trace requires JavaScript.</p></noscript><div class="trace-body"><div class="trace-toolbar"><div class="trace-controls" role="toolbar" aria-label="Trace navigation">${controls}</div></div><div class="trace-content"><div class="trace-diagram"></div><div class="trace-source-wrapper">${sourcePanels}</div></div></div></div>`;
383
+ return `<div class="${wrapperClass}"${wrapperAttrs}><noscript><p>This interactive trace requires JavaScript.</p></noscript><div class="trace-body"><div class="trace-toolbar"><div class="trace-controls" role="toolbar" aria-label="Trace navigation">${controls}</div></div><div class="trace-content"><div class="trace-diagram"></div><div class="trace-resizer" role="separator" aria-label="Resize trace panes" aria-orientation="horizontal" tabindex="0"></div><div class="trace-source-wrapper">${sourcePanels}</div></div></div></div>`;
384
384
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.15.5",
3
+ "version": "1.15.7",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
@@ -33,7 +33,7 @@ function isInteractive(
33
33
  target instanceof view.Element ? target : target.parentElement;
34
34
  return Boolean(
35
35
  element?.closest(
36
- 'a, button, input, select, textarea, summary, label, [role="button"], [contenteditable=""], [contenteditable="true"]',
36
+ 'a, button, input, select, textarea, summary, label, [role="button"], [role="separator"], [contenteditable=""], [contenteditable="true"]',
37
37
  ),
38
38
  );
39
39
  }
@@ -7,12 +7,20 @@ interface WidgetState {
7
7
  currentStep: number;
8
8
  }
9
9
 
10
+ interface TraceResizeState {
11
+ sourceHeight: number | null;
12
+ dragStartY: number | null;
13
+ dragStartSourceHeight: number;
14
+ }
15
+
10
16
  interface WidgetElements {
11
17
  root: HTMLElement;
12
18
  sourceWrapper: HTMLElement;
13
19
  controls: HTMLElement;
14
20
  content: HTMLElement;
15
21
  diagram: HTMLElement;
22
+ resizer: HTMLElement | null;
23
+ resizeState: TraceResizeState;
16
24
  }
17
25
 
18
26
  function getStep(state: WidgetState): TraceChunkEntry {
@@ -216,6 +224,187 @@ function scaleDiagramSvg(diagram: HTMLElement, doc: Document): void {
216
224
  svg.setAttribute('height', String(baseHeight * scale));
217
225
  }
218
226
 
227
+ function parseCssPixelValue(rawValue: string | null | undefined): number {
228
+ if (!rawValue || rawValue === 'none') {
229
+ return 0;
230
+ }
231
+
232
+ const parsed = Number.parseFloat(rawValue);
233
+ return Number.isFinite(parsed) ? parsed : 0;
234
+ }
235
+
236
+ function elementHeight(element: HTMLElement | null): number {
237
+ if (!element) {
238
+ return 0;
239
+ }
240
+
241
+ return element.getBoundingClientRect().height || element.clientHeight;
242
+ }
243
+
244
+ function getTraceContentGap(content: HTMLElement, doc: Document): number {
245
+ const view = doc.defaultView;
246
+ if (!view) {
247
+ return 0;
248
+ }
249
+
250
+ const styles = view.getComputedStyle(content);
251
+ return parseCssPixelValue(styles.rowGap || styles.gap);
252
+ }
253
+
254
+ function getVisibleSourceContentHeight(
255
+ sourceWrapper: HTMLElement,
256
+ ): number | null {
257
+ const visibleSource = Array.from(
258
+ sourceWrapper.querySelectorAll('.trace-source'),
259
+ ).find(source => !(source as HTMLElement).hidden) as HTMLElement | undefined;
260
+
261
+ if (!visibleSource) {
262
+ return null;
263
+ }
264
+
265
+ return visibleSource.scrollHeight > 0 ? visibleSource.scrollHeight : null;
266
+ }
267
+
268
+ function getResizableSourceBounds(
269
+ elements: WidgetElements,
270
+ doc: Document,
271
+ ): { min: number; max: number } {
272
+ const view = doc.defaultView;
273
+ const sourceStyles = view?.getComputedStyle(elements.sourceWrapper);
274
+ const diagramStyles = view?.getComputedStyle(elements.diagram);
275
+ const minSourceHeight = Math.max(
276
+ 0,
277
+ parseCssPixelValue(sourceStyles?.minHeight),
278
+ );
279
+ const minDiagramHeight = Math.max(
280
+ 0,
281
+ parseCssPixelValue(diagramStyles?.minHeight),
282
+ );
283
+ const contentHeight = elementHeight(elements.content);
284
+ if (contentHeight === 0) {
285
+ return { min: minSourceHeight, max: Number.POSITIVE_INFINITY };
286
+ }
287
+
288
+ const directChildren = Array.from(elements.content.children).filter(
289
+ child => !child.hasAttribute('hidden'),
290
+ ).length;
291
+ const gapTotal =
292
+ Math.max(0, directChildren - 1) * getTraceContentGap(elements.content, doc);
293
+ const output = elements.content.querySelector(
294
+ ':scope > .trace-output',
295
+ ) as HTMLElement | null;
296
+ const fixedHeight = elementHeight(elements.resizer) + elementHeight(output);
297
+ const layoutMaxSourceHeight =
298
+ contentHeight - minDiagramHeight - fixedHeight - gapTotal;
299
+ const contentMaxSourceHeight = getVisibleSourceContentHeight(
300
+ elements.sourceWrapper,
301
+ );
302
+ const maxSourceHeight =
303
+ contentMaxSourceHeight === null
304
+ ? layoutMaxSourceHeight
305
+ : Math.min(layoutMaxSourceHeight, contentMaxSourceHeight);
306
+
307
+ return {
308
+ min: minSourceHeight,
309
+ max: Math.max(minSourceHeight, maxSourceHeight),
310
+ };
311
+ }
312
+
313
+ function applySourceHeight(
314
+ elements: WidgetElements,
315
+ doc: Document,
316
+ nextHeight: number,
317
+ ): void {
318
+ const bounds = getResizableSourceBounds(elements, doc);
319
+ const sourceHeight = Math.min(Math.max(nextHeight, bounds.min), bounds.max);
320
+ elements.resizeState.sourceHeight = sourceHeight;
321
+ elements.sourceWrapper.style.flex = `0 0 ${sourceHeight}px`;
322
+ elements.sourceWrapper.style.maxHeight = 'none';
323
+ }
324
+
325
+ function reclampSourceHeight(elements: WidgetElements, doc: Document): void {
326
+ if (elements.resizeState.sourceHeight === null) {
327
+ return;
328
+ }
329
+
330
+ applySourceHeight(elements, doc, elements.resizeState.sourceHeight);
331
+ }
332
+
333
+ function setupTraceResizer(elements: WidgetElements, doc: Document): void {
334
+ const resizer = elements.resizer;
335
+ if (!resizer) {
336
+ return;
337
+ }
338
+
339
+ resizer.addEventListener('pointerdown', (event: PointerEvent) => {
340
+ if (event.button !== 0) {
341
+ return;
342
+ }
343
+
344
+ event.preventDefault();
345
+ event.stopPropagation();
346
+ elements.resizeState.dragStartY = event.clientY;
347
+ elements.resizeState.dragStartSourceHeight = elementHeight(
348
+ elements.sourceWrapper,
349
+ );
350
+ resizer.setPointerCapture?.(event.pointerId);
351
+ });
352
+
353
+ resizer.addEventListener('pointermove', (event: PointerEvent) => {
354
+ const dragStartY = elements.resizeState.dragStartY;
355
+ if (dragStartY === null) {
356
+ return;
357
+ }
358
+
359
+ if ((event.buttons & 1) !== 1) {
360
+ elements.resizeState.dragStartY = null;
361
+ resizer.releasePointerCapture?.(event.pointerId);
362
+ return;
363
+ }
364
+
365
+ event.preventDefault();
366
+ event.stopPropagation();
367
+ const deltaY = event.clientY - dragStartY;
368
+ applySourceHeight(
369
+ elements,
370
+ doc,
371
+ elements.resizeState.dragStartSourceHeight - deltaY,
372
+ );
373
+ });
374
+
375
+ const endDrag = (event: PointerEvent): void => {
376
+ if (elements.resizeState.dragStartY === null) {
377
+ return;
378
+ }
379
+
380
+ event.preventDefault();
381
+ event.stopPropagation();
382
+ elements.resizeState.dragStartY = null;
383
+ resizer.releasePointerCapture?.(event.pointerId);
384
+ };
385
+
386
+ resizer.addEventListener('pointerup', endDrag);
387
+ resizer.addEventListener('pointercancel', endDrag);
388
+ resizer.addEventListener('click', (event: MouseEvent) => {
389
+ event.preventDefault();
390
+ event.stopPropagation();
391
+ });
392
+
393
+ resizer.addEventListener('keydown', (event: KeyboardEvent) => {
394
+ if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') {
395
+ return;
396
+ }
397
+
398
+ event.preventDefault();
399
+ const currentHeight =
400
+ elements.resizeState.sourceHeight ??
401
+ elementHeight(elements.sourceWrapper);
402
+ const step = event.shiftKey ? 40 : 10;
403
+ const direction = event.key === 'ArrowUp' ? 1 : -1;
404
+ applySourceHeight(elements, doc, currentHeight + direction * step);
405
+ });
406
+ }
407
+
219
408
  function updateOutput(
220
409
  content: HTMLElement,
221
410
  output: TraceOutputEvent[],
@@ -273,6 +462,7 @@ function renderWidgetState(
273
462
  output.push(...(s.output ?? []));
274
463
  }
275
464
  updateOutput(elements.content, output, doc);
465
+ reclampSourceHeight(elements, doc);
276
466
 
277
467
  const prevHeight = elements.diagram.scrollHeight;
278
468
  elements.diagram.innerHTML = entry.svg;
@@ -311,6 +501,7 @@ async function initWidget(root: HTMLElement, doc: Document): Promise<void> {
311
501
  ) as HTMLElement;
312
502
  const content = root.querySelector('.trace-content') as HTMLElement;
313
503
  const diagram = root.querySelector('.trace-diagram') as HTMLElement;
504
+ const resizer = root.querySelector('.trace-resizer') as HTMLElement | null;
314
505
 
315
506
  const controls = root.querySelector('.trace-controls') as HTMLElement;
316
507
  const firstBtn = controls.querySelector('.trace-first') as HTMLButtonElement;
@@ -358,8 +549,15 @@ async function initWidget(root: HTMLElement, doc: Document): Promise<void> {
358
549
  controls,
359
550
  content,
360
551
  diagram,
552
+ resizer,
553
+ resizeState: {
554
+ sourceHeight: null,
555
+ dragStartY: null,
556
+ dragStartSourceHeight: 0,
557
+ },
361
558
  };
362
559
 
560
+ setupTraceResizer(elements, doc);
363
561
  renderWidgetState(state, elements, doc);
364
562
  }
365
563
 
@@ -2,7 +2,7 @@
2
2
  display: flex;
3
3
  flex-direction: column;
4
4
  gap: var(--gap);
5
- height: calc(100dvh - var(--header-collapsed-height) - 3 * var(--gap));
5
+ height: calc(100svh - var(--header-collapsed-height) - 3 * var(--gap));
6
6
  overflow: hidden;
7
7
  }
8
8
 
@@ -14,6 +14,36 @@
14
14
  min-height: 0;
15
15
  }
16
16
 
17
+ .trace-content > .trace-resizer {
18
+ position: relative;
19
+ flex: 0 0 16px;
20
+ width: 100%;
21
+ margin-block: calc(-0.5 * var(--gap));
22
+ touch-action: none;
23
+ cursor: ns-resize;
24
+
25
+ &::before {
26
+ position: absolute;
27
+ top: 50%;
28
+ left: 50%;
29
+ width: 25%;
30
+ height: 6px;
31
+ content: '';
32
+ background: var(--bg2-color);
33
+ border-radius: 999px;
34
+ transform: translate(-50%, -50%);
35
+ }
36
+
37
+ &:focus-visible {
38
+ outline: none;
39
+ }
40
+
41
+ &:focus-visible::before {
42
+ outline: 2px solid var(--theme-color);
43
+ outline-offset: 2px;
44
+ }
45
+ }
46
+
17
47
  .trace-content > .trace-diagram {
18
48
  flex: 1 1 100%;
19
49
  min-height: 8rem;