@fusion.dev/helix-headless 0.165.0 → 0.167.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 (57) hide show
  1. package/custom-elements.json +3235 -534
  2. package/dist/callout/root/callout-root.d.ts +8 -2
  3. package/dist/callout/root/callout-root.js +14 -1
  4. package/dist/canvas/arrange.d.ts +32 -0
  5. package/dist/canvas/arrange.js +144 -0
  6. package/dist/canvas/paste.d.ts +25 -0
  7. package/dist/canvas/paste.js +58 -0
  8. package/dist/canvas/root/canvas-root.d.ts +60 -3
  9. package/dist/canvas/root/canvas-root.js +166 -2
  10. package/dist/chat/composer/chat-composer.js +5 -1
  11. package/dist/code-block/root/code-block-root.d.ts +63 -0
  12. package/dist/code-block/root/code-block-root.js +117 -6
  13. package/dist/combobox/root/combobox-root.d.ts +6 -1
  14. package/dist/combobox/root/combobox-root.js +2 -2
  15. package/dist/date-input/root/date-input-root.d.ts +1 -1
  16. package/dist/element-events.generated.js +17 -2
  17. package/dist/file-input/root/file-input-root.d.ts +1 -1
  18. package/dist/file-input/root/file-input-root.js +1 -1
  19. package/dist/filter-input/menu/filter-menu.d.ts +1 -0
  20. package/dist/filter-input/menu/filter-menu.js +1 -1
  21. package/dist/format-date/root/format-date-root.d.ts +10 -3
  22. package/dist/format-date/root/format-date-root.js +6 -1
  23. package/dist/listbox/root/listbox-root.d.ts +12 -1
  24. package/dist/listbox/root/listbox-root.js +15 -6
  25. package/dist/menu/item/menu-item.d.ts +20 -0
  26. package/dist/menu/item/menu-item.js +53 -20
  27. package/dist/menu/root/menu-root.d.ts +84 -1
  28. package/dist/menu/root/menu-root.js +187 -44
  29. package/dist/object-inspector/root/object-inspector-root.d.ts +5 -0
  30. package/dist/object-inspector/root/object-inspector-root.js +5 -0
  31. package/dist/select/root/select-root.js +1 -1
  32. package/dist/steps/root/steps-root.d.ts +7 -0
  33. package/dist/steps/root/steps-root.js +7 -0
  34. package/dist/table/grid-controller.d.ts +10 -0
  35. package/dist/table/grid-controller.js +7 -0
  36. package/dist/table/root/table-root.d.ts +71 -1
  37. package/dist/table/root/table-root.js +122 -7
  38. package/dist/table/row/table-row.d.ts +17 -0
  39. package/dist/table/row/table-row.js +23 -0
  40. package/dist/time-input/root/time-input-root.d.ts +2 -1
  41. package/dist/time-input/root/time-input-root.js +1 -1
  42. package/dist/time-picker/root/time-picker-root.d.ts +48 -1
  43. package/dist/time-picker/root/time-picker-root.js +79 -21
  44. package/dist/video/root/video-root.d.ts +2 -0
  45. package/dist/video/root/video-root.js +4 -2
  46. package/dist/video-playlist/menu/video-playlist-menu.d.ts +1 -0
  47. package/dist/video-playlist/menu/video-playlist-menu.js +1 -1
  48. package/dist/video-playlist/root/video-playlist-root.js +15 -1
  49. package/dist/virtual-list/root/virtual-list-root.d.ts +70 -2
  50. package/dist/virtual-list/root/virtual-list-root.js +216 -32
  51. package/dist/virtual-list/sections.d.ts +27 -0
  52. package/dist/virtual-list/sections.js +43 -0
  53. package/lint.json +1 -1
  54. package/package.json +5 -5
  55. package/types/jsx.d.ts +716 -96
  56. package/types/vue.d.ts +148 -12
  57. package/types/words.d.ts +45 -0
@@ -4,16 +4,18 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { assignSlots, auditName, capturePointer, customElement, formatPercent, formatWords, HelixBeforeMoveEvent, HelixBeforeReconnectEvent, HelixBeforeRelabelEvent, HelixCanvasEditEvent, HelixCanvasPanEvent, HelixCanvasViewEvent, HelixCanvasZoomEvent, HelixConnectEvent, HelixElement, HelixMoveEvent, HelixReconnectEvent, HelixRelabelEvent, HelixRemoveEvent, HelixSelectEvent, hostAria, inlineStep, isRtl, MixinViewport, words, wordsConverter, wordsLocale, } from '@fusion.dev/helix-core';
7
+ import { assignSlots, auditName, capturePointer, customElement, formatPercent, formatWords, HelixBeforeMoveEvent, HelixBeforePasteEvent, HelixBeforeReconnectEvent, HelixBeforeRelabelEvent, HelixCanvasEditEvent, HelixCanvasPanEvent, HelixCanvasViewEvent, HelixCanvasZoomEvent, HelixConnectEvent, HelixElement, HelixMoveEvent, HelixPastedEvent, HelixReconnectEvent, HelixRelabelEvent, HelixRemoveEvent, HelixSelectEvent, hostAria, inlineStep, isRtl, MixinViewport, words, wordsConverter, wordsLocale, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing, svg } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
+ import { arrangement } from '../arrange.js';
10
11
  import { descriptionOf } from '../describe.js';
11
12
  import { HelixHeadlessCanvasEdge } from '../edge/canvas-edge.js';
12
13
  import { EdgePan } from '../edge-pan.js';
13
14
  import { neighborOf } from '../neighbor.js';
14
15
  import { HelixHeadlessCanvasNode } from '../node/canvas-node.js';
16
+ import { parseCanvas, pasteInto } from '../paste.js';
15
17
  import { snapBox, snapTo, stepTo } from '../snap.js';
16
- import { colorOf, connector, distanceTo, ends, extent, inside, port, reading, sideNear, } from './canvas-document.js';
18
+ import { colorOf, connector, distanceTo, ends, extent, fromJsonCanvas, inside, port, reading, sideNear, } from './canvas-document.js';
17
19
  export { fromJsonCanvas } from './canvas-document.js';
18
20
  const MARGIN = 32;
19
21
  const ARROW = 'canvas-arrow';
@@ -43,6 +45,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
43
45
  constructor() {
44
46
  super(...arguments);
45
47
  this.activateHintLabel = 'Press the board to use it';
48
+ this.arrangedLabel = 'Cards arranged';
46
49
  this.comesFromLabel = 'from {cards}';
47
50
  this.connectedLabel = 'Asked to connect {from} to {to}.';
48
51
  this.connectingLabel = 'Connecting from {from}. Move to another card and press Enter, or Escape to stop.';
@@ -55,9 +58,11 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
55
58
  this.leadsToLabel = 'leads to {cards}';
56
59
  this.linkedLabel = '{leadsTo}; {comesFrom}';
57
60
  this.movedLabel = '{name} moved to {x}, {y}';
61
+ this.pastedLabel = '{count, plural, one {# card pasted} other {# cards pasted}}';
58
62
  this.readOnly = false;
59
63
  this.reconnectingLabel = 'Moving the end of the connection from {from} to {to}. Move to a card and press Enter, or Escape to stop.';
60
64
  this.redoneLabel = 'Move made again';
65
+ this.removedLabel = '{count, plural, one {Asked to remove {name}.} other {Asked to remove # items.}}';
61
66
  this.resizedLabel = '{name} resized to {width} by {height}';
62
67
  this.roleLabel = 'canvas';
63
68
  this.selected = '';
@@ -106,6 +111,15 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
106
111
  event.preventDefault();
107
112
  this.say(formatWords(this, 'copiedLabel', { count: copy.nodes?.length ?? 0 }));
108
113
  };
114
+ this.handlePaste = (event) => {
115
+ const origin = event.composedPath()[0];
116
+ if (event.defaultPrevented || origin?.closest?.(INTERACTIVE)) {
117
+ return;
118
+ }
119
+ if (this.paste(event.clipboardData?.getData('text/plain') ?? '')) {
120
+ event.preventDefault();
121
+ }
122
+ };
109
123
  this.handleHistoryKey = (event) => {
110
124
  if (event.defaultPrevented || event.altKey || !(event.ctrlKey || event.metaKey)) {
111
125
  return;
@@ -129,6 +143,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
129
143
  }
130
144
  });
131
145
  this.handlePointerDown = (event) => {
146
+ this.settle();
132
147
  if (!this.gestureAllowed(event)) {
133
148
  return;
134
149
  }
@@ -494,6 +509,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
494
509
  }
495
510
  };
496
511
  this.handleStopKeydown = (event) => {
512
+ this.settle();
497
513
  const origin = event.composedPath()[0];
498
514
  const edge = this.edgeOf(event);
499
515
  const card = edge === undefined ? this.cardOf(event) : undefined;
@@ -1122,6 +1138,19 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1122
1138
  const at = Math.min(...asked.map((one) => walk.findIndex((stop) => stop.key === one)));
1123
1139
  const next = neighborOf(walk.map((one) => one.key), at, (key) => !gone.has(key));
1124
1140
  this.refocus = { gone: [...gone], next };
1141
+ const only = asked[0];
1142
+ const edge = only?.startsWith('edge:')
1143
+ ? edges.find((one) => this.keyOf(one) === only)
1144
+ : undefined;
1145
+ this.say(formatWords(this, 'removedLabel', {
1146
+ count: asked.length,
1147
+ name: edge
1148
+ ? formatWords(this, 'connectionLabel', {
1149
+ from: this.nameOf(edge.fromNode),
1150
+ to: this.nameOf(edge.toNode),
1151
+ })
1152
+ : this.nameOf(only ?? ''),
1153
+ }));
1125
1154
  this.dispatchEvent(new HelixRemoveEvent(detail));
1126
1155
  }
1127
1156
  clicked(key, event) {
@@ -1290,6 +1319,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1290
1319
  return name === undefined ? undefined : this.byName.get(name);
1291
1320
  }
1292
1321
  requestMove(detail) {
1322
+ this.settle();
1293
1323
  if (!this.dispatchEvent(new HelixBeforeMoveEvent(detail))) {
1294
1324
  return false;
1295
1325
  }
@@ -1359,6 +1389,125 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1359
1389
  this.say(words);
1360
1390
  return true;
1361
1391
  }
1392
+ paste(document) {
1393
+ const read = typeof document === 'string' ? parseCanvas(document) : document;
1394
+ if (this.readOnly || !read?.nodes?.length) {
1395
+ return false;
1396
+ }
1397
+ const taken = new Set(this.edgeElements.map((one) => one.name).filter(Boolean));
1398
+ const placed = pasteInto(read, this.nodes, taken, (this.snap ?? 0) > 0 ? this.snap : NUDGE * 2);
1399
+ if (!this.dispatchEvent(new HelixBeforePasteEvent({ document: placed }))) {
1400
+ return false;
1401
+ }
1402
+ const tier = this.constructor.tier;
1403
+ const add = (name, attributes, color, text = '') => {
1404
+ const element = this.ownerDocument.createElement(`${tier}${name}`);
1405
+ for (const [key, value] of Object.entries(attributes)) {
1406
+ if (value !== undefined && value !== false && value !== '') {
1407
+ element.setAttribute(key, value === true ? '' : String(value));
1408
+ }
1409
+ }
1410
+ if (color) {
1411
+ element.style.setProperty(`--${name}-color`, color);
1412
+ }
1413
+ element.textContent = text;
1414
+ this.append(element);
1415
+ };
1416
+ const { edges, nodes } = fromJsonCanvas(placed);
1417
+ for (const one of nodes) {
1418
+ const { color, group, height, label, name, text, tone, width, x, y } = one;
1419
+ add('canvas-node', { group, height, label, name, tone, width, x, y }, color, text ?? one.url ?? one.file);
1420
+ }
1421
+ for (const one of edges) {
1422
+ add('canvas-edge', {
1423
+ from: one.from,
1424
+ 'from-end': one.fromEnd,
1425
+ 'from-side': one.fromSide,
1426
+ label: one.label,
1427
+ name: one.name,
1428
+ to: one.to,
1429
+ 'to-end': one.toEnd,
1430
+ 'to-side': one.toSide,
1431
+ tone: one.tone,
1432
+ }, one.color);
1433
+ }
1434
+ this.requestUpdate();
1435
+ this.choose(nodes.map((one) => one.name));
1436
+ this.say(formatWords(this, 'pastedLabel', { count: nodes.length }));
1437
+ this.dispatchEvent(new HelixPastedEvent({ document: placed }));
1438
+ return true;
1439
+ }
1440
+ settle() {
1441
+ const glide = this.gliding;
1442
+ if (!glide) {
1443
+ return;
1444
+ }
1445
+ cancelAnimationFrame(glide.frame);
1446
+ this.gliding = undefined;
1447
+ for (const { name, x, y } of glide.to) {
1448
+ const card = this.byName.get(name);
1449
+ if (card) {
1450
+ card.x = x;
1451
+ card.y = y;
1452
+ }
1453
+ }
1454
+ this.requestUpdate();
1455
+ }
1456
+ arrange(options = {}) {
1457
+ this.settle();
1458
+ const places = arrangement(this.nodes, this.edges, options, this.snap);
1459
+ const from = new Map(this.cards.map((card) => [card.name, { x: card.x, y: card.y }]));
1460
+ const detail = {
1461
+ nodes: [...places]
1462
+ .filter(([name, at]) => from.get(name)?.x !== at.x || from.get(name)?.y !== at.y)
1463
+ .map(([name, at]) => ({ name, ...at })),
1464
+ };
1465
+ if (this.readOnly || detail.nodes.length === 0 || !this.requestMove(detail)) {
1466
+ return false;
1467
+ }
1468
+ this.finishMove(detail);
1469
+ this.say(this.arrangedLabel);
1470
+ const board = this.partNamed('board');
1471
+ let time = 0;
1472
+ if (board && !globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {
1473
+ this.toggleState('direct', false);
1474
+ time = Number.parseFloat(getComputedStyle(board).transitionDuration) * 1000;
1475
+ this.toggleState('direct', this.direct);
1476
+ }
1477
+ if (!(time > 0) || typeof requestAnimationFrame === 'undefined') {
1478
+ return true;
1479
+ }
1480
+ for (const { name } of detail.nodes) {
1481
+ const card = this.byName.get(name);
1482
+ const start = from.get(name);
1483
+ if (card && start) {
1484
+ card.x = start.x;
1485
+ card.y = start.y;
1486
+ }
1487
+ }
1488
+ const begun = performance.now();
1489
+ const step = (now) => {
1490
+ const t = Math.min(1, (now - begun) / time);
1491
+ const eased = t < 0.5 ? 4 * t ** 3 : 1 - (-2 * t + 2) ** 3 / 2;
1492
+ for (const { name, x, y } of detail.nodes) {
1493
+ const card = this.byName.get(name);
1494
+ const start = from.get(name);
1495
+ if (card && start) {
1496
+ card.x = start.x + (x - start.x) * eased;
1497
+ card.y = start.y + (y - start.y) * eased;
1498
+ }
1499
+ }
1500
+ this.requestUpdate();
1501
+ if (t < 1 && this.gliding) {
1502
+ this.gliding.frame = requestAnimationFrame(step);
1503
+ }
1504
+ else {
1505
+ this.gliding = undefined;
1506
+ }
1507
+ };
1508
+ this.gliding = { frame: requestAnimationFrame(step), to: detail.nodes };
1509
+ return true;
1510
+ }
1362
1511
  panCarrying(by) {
1363
1512
  const last = this.carryPointer;
1364
1513
  if (!last) {
@@ -1925,6 +2074,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1925
2074
  this.addEventListener('keydown', this.handleStopKeydown);
1926
2075
  this.addEventListener('keydown', this.handleHistoryKey);
1927
2076
  this.addEventListener('copy', this.handleCopy);
2077
+ this.addEventListener('paste', this.handlePaste);
1928
2078
  this.addEventListener('pointercancel', this.handlePointerUp);
1929
2079
  this.addEventListener('pointerdown', this.handlePointerDown);
1930
2080
  this.addEventListener('pointermove', this.handlePointerMove);
@@ -1947,6 +2097,8 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1947
2097
  this.removeEventListener('keydown', this.handleStopKeydown);
1948
2098
  this.removeEventListener('keydown', this.handleHistoryKey);
1949
2099
  this.removeEventListener('copy', this.handleCopy);
2100
+ this.removeEventListener('paste', this.handlePaste);
2101
+ this.settle();
1950
2102
  this.removeEventListener('pointercancel', this.handlePointerUp);
1951
2103
  this.removeEventListener('pointerdown', this.handlePointerDown);
1952
2104
  this.removeEventListener('pointermove', this.handlePointerMove);
@@ -2323,6 +2475,10 @@ __decorate([
2323
2475
  words,
2324
2476
  property({ attribute: 'activate-hint-label' })
2325
2477
  ], HelixHeadlessCanvas.prototype, "activateHintLabel", void 0);
2478
+ __decorate([
2479
+ words,
2480
+ property({ attribute: 'arranged-label' })
2481
+ ], HelixHeadlessCanvas.prototype, "arrangedLabel", void 0);
2326
2482
  __decorate([
2327
2483
  words,
2328
2484
  property({ attribute: 'comes-from-label' })
@@ -2376,6 +2532,10 @@ __decorate([
2376
2532
  __decorate([
2377
2533
  property({ reflect: true })
2378
2534
  ], HelixHeadlessCanvas.prototype, "order", void 0);
2535
+ __decorate([
2536
+ words,
2537
+ property({ attribute: 'pasted-label' })
2538
+ ], HelixHeadlessCanvas.prototype, "pastedLabel", void 0);
2379
2539
  __decorate([
2380
2540
  property({ reflect: true })
2381
2541
  ], HelixHeadlessCanvas.prototype, "path", void 0);
@@ -2390,6 +2550,10 @@ __decorate([
2390
2550
  words,
2391
2551
  property({ attribute: 'redone-label' })
2392
2552
  ], HelixHeadlessCanvas.prototype, "redoneLabel", void 0);
2553
+ __decorate([
2554
+ words,
2555
+ property({ attribute: 'removed-label' })
2556
+ ], HelixHeadlessCanvas.prototype, "removedLabel", void 0);
2393
2557
  __decorate([
2394
2558
  words,
2395
2559
  property({ attribute: 'resized-label' })
@@ -120,7 +120,11 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
120
120
  }
121
121
  firstUpdated(changed) {
122
122
  super.firstUpdated(changed);
123
- this.addEventListener('invalid', this.touch);
123
+ this.addEventListener('invalid', () => {
124
+ if (!this.checkingValidity) {
125
+ this.touch();
126
+ }
127
+ });
124
128
  }
125
129
  willUpdate(changed) {
126
130
  super.willUpdate(changed);
@@ -71,6 +71,18 @@ import '../../copy-button/index.js';
71
71
  * keeps hydration clean, and draws the diff once it can read the content. Diff
72
72
  * mode is experimental. The rest of the family is stable.
73
73
  *
74
+ * **Several blocks are files**, one shown at a time under a row of tabs, as
75
+ * Sandbox shows its source panes. Put Code Blocks in the default slot of a
76
+ * Code Block, and each is a file named on its tab by its `label` or, without
77
+ * one, its `language`. `file` holds the name of the file shown. The tabs are a
78
+ * tab list: the arrow keys move to a tab and show its file, the two swapping
79
+ * on a right-to-left page, and Home and End go to the first and the last.
80
+ * Choosing one sends `helix-before-change` with the file's name, which you can
81
+ * cancel, and `change` once `file` holds it. The files not shown carry
82
+ * `hidden`, and the one shown draws no name of its own, its tab naming it. One
83
+ * block draws no tabs. A server cannot see the blocks inside, so the client
84
+ * draws the tabs once it upgrades.
85
+ *
74
86
  * @since 0.1.0
75
87
  * @status stable
76
88
  * @category typography
@@ -82,7 +94,11 @@ import '../../copy-button/index.js';
82
94
  * @slot copied-icon - The copy button's icon after a copy. Defaults to a check mark.
83
95
  * @slot copy-icon - The copy button's icon before a copy. Defaults to a clipboard.
84
96
  *
97
+ * @event {Event} change - A file tab showed its file, whose name `file` now holds.
98
+ * @event {HelixBeforeChangeEvent} helix-before-change - A file tab is about to show its file, with the file's name in `detail.value`. Cancelable: cancel it to keep the file shown.
85
99
  * @event {HelixBeforeToggleEvent} helix-before-toggle - The wrap button was pressed, with the `wrap` it asks for in `detail.checked`. Cancelable: cancel it to keep the lines as they are.
100
+ * @event {HelixCopyEvent} helix-copy - The copy button put the code on the clipboard, with the text in `detail.value`.
101
+ * @event {HelixCopyErrorEvent} helix-copy-error - A press on the copy button put nothing on the clipboard: the browser did not allow the write, or there was no code.
86
102
  * @event {HelixToggleEvent} helix-toggle - Fired after the wrap button changed `wrap`, with the new value in `detail.checked`.
87
103
  *
88
104
  * @csspart actions - The box in the header that holds the `actions` slot, drawn while something is slotted there.
@@ -109,6 +125,8 @@ import '../../copy-button/index.js';
109
125
  * @csspart copy-tooltip-top - The tooltip while it is above the copy button. Applied with `copy-tooltip`.
110
126
  * @csspart expand - The button under a capped block that shows every line, and fewer again. Drawn when `expandable` is set and the cap hides something.
111
127
  * @csspart file - In a diff, one file's header and block.
128
+ * @csspart file-tab - One file tab, `aria-selected` while its file is shown.
129
+ * @csspart files - The file tabs over the blocks a block holds, where there are several.
112
130
  * @csspart filler - In a split diff, the empty row opposite a line the other side does not have.
113
131
  * @csspart fold - In a diff with `context-lines`, the button that shows a folded run of unchanged lines, and hides it again, saying which with `aria-expanded`.
114
132
  * @csspart frame - The code, and the copy button beside it where there is no header.
@@ -132,6 +150,7 @@ import '../../copy-button/index.js';
132
150
  * @cssproperty --code-block-mark-start - Written by the block on each `mark`: how far down the code its run of lines starts. Do not set it.
133
151
  *
134
152
  * @cssstate expanded - The expand button has shown every line of a capped block.
153
+ * @cssstate tabbed - The block is one of several files under another block's tabs, which name it, so it draws no name of its own.
135
154
  */
136
155
  export declare class HelixHeadlessCodeBlock extends HelixElement {
137
156
  /**
@@ -198,6 +217,17 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
198
217
  * The expand button's words while the cap hides lines.
199
218
  */
200
219
  expandLabel: string;
220
+ /**
221
+ * The name of the file shown, where the block holds several Code Blocks:
222
+ * the `label` of one, or its `language` without one.
223
+ *
224
+ * Unset, or a name no file has, shows the first.
225
+ */
226
+ file?: string;
227
+ /**
228
+ * The name of the file tabs, read by screen readers.
229
+ */
230
+ filesLabel: string;
201
231
  /**
202
232
  * The words of a button that hides a run of unchanged lines it showed, in a
203
233
  * diff with `context-lines`. `{count}` is how many, and each language
@@ -395,6 +425,39 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
395
425
  * block computed and CSS cannot, and no render follows from them.
396
426
  */
397
427
  private placeLines;
428
+ /** The Code Blocks this block holds as its code, once its children can be read. */
429
+ private get blocks();
430
+ /**
431
+ * Whether the block is one of several files under another block's tabs,
432
+ * which name it. Read from the parent, so a block needs no telling.
433
+ */
434
+ private get tabbed();
435
+ /** The file shown among several: the one `file` names, or the first. */
436
+ private shownFile;
437
+ /** Shows the file a tab names, once nobody refused. */
438
+ private chooseFile;
439
+ /**
440
+ * Arrow keys, `Home` and `End` on the file tabs, which move to a tab and
441
+ * show its file, as a tab list that follows focus does and as Sandbox's file
442
+ * tabs do. Left and Right swap on a right-to-left page.
443
+ */
444
+ private readonly handleFileKey;
445
+ /** The blocks this element hid, to show them again. */
446
+ private readonly hid;
447
+ /** How many files the block last held, so each is told when that changes. */
448
+ private fileCount;
449
+ /**
450
+ * Hides the files not shown, shows any this element hid that is now, and
451
+ * tells each file when how many there are changes, since a file draws its
452
+ * own name only while it is alone.
453
+ */
454
+ private showFile;
455
+ /**
456
+ * The tabs over several files, and the file shown under them. One file
457
+ * draws no tabs, only itself.
458
+ */
459
+ private renderFiles;
460
+ protected willUpdate(changed: PropertyValues): void;
398
461
  protected updated(changed: PropertyValues): void;
399
462
  connectedCallback(): void;
400
463
  disconnectedCallback(): void;
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { customElement, formatWords, HelixBeforeToggleEvent, HelixElement, HelixToggleEvent, requestChange, words, } from '@fusion.dev/helix-core';
7
+ import { customElement, formatWords, HelixBeforeToggleEvent, HelixElement, HelixToggleEvent, inlineStep, requestChange, requestValue, words, } from '@fusion.dev/helix-core';
8
8
  import { nothing } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  import { unsafeHTML } from 'lit/directives/unsafe-html.js';
@@ -13,6 +13,17 @@ import '../../copy-button/index.js';
13
13
  import { highlightedLines } from '../highlight.js';
14
14
  import { gutterNumbers, lineCount, lineRuns, lineSpans } from '../lines.js';
15
15
  import { foldContext, pairLines, parsePatch, } from '../patch.js';
16
+ const stopPress = (event) => event.stopPropagation();
17
+ const isBlock = (element) => element?.localName.endsWith('code-block') === true;
18
+ const blocksIn = (host) => [...host.children].filter((child) => child.slot === '' && isBlock(child));
19
+ const nameOf = (block) => {
20
+ const { label, language } = block;
21
+ return ((typeof label === 'string' && label) ||
22
+ block.getAttribute('label') ||
23
+ (typeof language === 'string' && language) ||
24
+ block.getAttribute('language') ||
25
+ '');
26
+ };
16
27
  let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
17
28
  constructor() {
18
29
  super(...arguments);
@@ -25,6 +36,7 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
25
36
  this.diff = false;
26
37
  this.expandable = false;
27
38
  this.expandLabel = 'Show all lines';
39
+ this.filesLabel = 'Files';
28
40
  this.hideUnchangedLabel = '{count, plural, one {Hide # unchanged line} other {Hide # unchanged lines}}';
29
41
  this.label = '';
30
42
  this.language = '';
@@ -49,6 +61,30 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
49
61
  [...this.renderRoot.querySelectorAll('[part="block"], [part="code"]')].some((box) => box.scrollHeight > box.clientHeight + 1);
50
62
  }
51
63
  };
64
+ this.handleFileKey = (event) => {
65
+ const blocks = this.blocks;
66
+ const count = blocks.length;
67
+ const at = blocks.indexOf(this.shownFile(blocks));
68
+ const step = inlineStep(this, event.key);
69
+ const to = event.key === 'Home'
70
+ ? 0
71
+ : event.key === 'End'
72
+ ? count - 1
73
+ : step === 0
74
+ ? -1
75
+ : (at + step + count) % count;
76
+ const block = blocks[to];
77
+ if (!block) {
78
+ return;
79
+ }
80
+ event.preventDefault();
81
+ this.chooseFile(block);
82
+ void this.updateComplete.then(() => {
83
+ this.renderRoot.querySelector('[part="file-tab"][aria-selected="true"]')?.focus();
84
+ });
85
+ };
86
+ this.hid = new Set();
87
+ this.fileCount = 0;
52
88
  }
53
89
  static { this.words = 'helix-code-block'; }
54
90
  toggleWrap() {
@@ -213,6 +249,67 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
213
249
  }
214
250
  });
215
251
  }
252
+ get blocks() {
253
+ return this.childrenKnown ? blocksIn(this) : [];
254
+ }
255
+ get tabbed() {
256
+ const parent = this.childrenKnown ? this.parentElement : null;
257
+ return this.slot === '' && isBlock(parent) && blocksIn(parent).length > 1;
258
+ }
259
+ shownFile(blocks) {
260
+ return blocks.find((block) => nameOf(block) === this.file) ?? blocks[0];
261
+ }
262
+ chooseFile(block) {
263
+ if (block !== this.shownFile(this.blocks)) {
264
+ requestValue(this, nameOf(block), (name) => {
265
+ this.file = name;
266
+ });
267
+ }
268
+ }
269
+ showFile() {
270
+ const blocks = this.blocks;
271
+ const files = blocks.length > 1 ? blocks : [];
272
+ const shown = this.shownFile(files);
273
+ for (const block of files) {
274
+ if (block !== shown && !block.hidden) {
275
+ block.hidden = true;
276
+ this.hid.add(block);
277
+ }
278
+ }
279
+ for (const block of this.hid) {
280
+ if (block === shown || !files.includes(block)) {
281
+ ;
282
+ block.hidden = false;
283
+ this.hid.delete(block);
284
+ }
285
+ }
286
+ if (blocks.length !== this.fileCount) {
287
+ this.fileCount = blocks.length;
288
+ for (const block of blocks) {
289
+ block.requestUpdate?.();
290
+ }
291
+ }
292
+ }
293
+ renderFiles(blocks) {
294
+ const slot = html `<slot @slotchange=${() => this.requestUpdate()}></slot>`;
295
+ if (blocks.length < 2) {
296
+ return slot;
297
+ }
298
+ const shown = this.shownFile(blocks);
299
+ return html `<div part="files" role="tablist" aria-label=${this.filesLabel} @keydown=${this.handleFileKey}>
300
+ ${blocks.map((block, at) => {
301
+ const selected = block === shown;
302
+ return html `<button part="file-tab" id=${`file-${at}`} role="tab" type="button"
303
+ aria-controls="files-panel" aria-selected=${selected ? 'true' : 'false'}
304
+ tabindex=${selected ? '0' : '-1'} @click=${() => this.chooseFile(block)}>${nameOf(block)}</button>`;
305
+ })}
306
+ </div>
307
+ <div id="files-panel" role="tabpanel" aria-labelledby=${`file-${blocks.indexOf(shown)}`}>${slot}</div>`;
308
+ }
309
+ willUpdate(changed) {
310
+ super.willUpdate(changed);
311
+ this.toggleState('tabbed', this.tabbed);
312
+ }
216
313
  updated(changed) {
217
314
  super.updated(changed);
218
315
  if (typeof ResizeObserver !== 'undefined') {
@@ -220,6 +317,7 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
220
317
  this.sizes.observe(this);
221
318
  }
222
319
  this.measure();
320
+ this.showFile();
223
321
  }
224
322
  connectedCallback() {
225
323
  super.connectedCallback();
@@ -239,6 +337,10 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
239
337
  this.finishHydration();
240
338
  }
241
339
  render() {
340
+ const blocks = this.blocks;
341
+ if (blocks.length > 0) {
342
+ return this.renderFiles(blocks);
343
+ }
242
344
  const files = this.files;
243
345
  if (files.length > 0) {
244
346
  return this.renderDiff(files);
@@ -273,10 +375,11 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
273
375
  ? html `<span part="gutter" aria-hidden="true">${numbers}</span>`
274
376
  : nothing;
275
377
  const name = this.label || this.language;
276
- return html `${this.renderHeader(name)}
378
+ const drawn = this.tabbed ? '' : name;
379
+ return html `${this.renderHeader(name, undefined, drawn)}
277
380
  <div part="frame">
278
381
  <pre id="block" part="block" translate="no">${gutter}${code}</pre>
279
- ${this.headed(name) ? nothing : this.renderCopy()}
382
+ ${this.headed(drawn) ? nothing : this.renderCopy()}
280
383
  </div>${this.renderExpand()}`;
281
384
  }
282
385
  renderExpand() {
@@ -299,10 +402,10 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
299
402
  headed(name) {
300
403
  return name !== '' || (this.wrappable && !this.diff) || this.slotted('actions');
301
404
  }
302
- renderHeader(name, value) {
405
+ renderHeader(name, value, drawn = name) {
303
406
  const acting = this.slotted('actions');
304
- return this.headed(name)
305
- ? html `<div part="header">${name ? html `<span part="label">${name}</span>` : nothing}${acting ? html `<span part="actions"><slot name="actions"></slot></span>` : nothing}${this.renderWrapToggle()}${this.renderCopy(value, name)}</div>`
407
+ return this.headed(drawn)
408
+ ? html `<div part="header">${drawn ? html `<span part="label">${drawn}</span>` : nothing}${acting ? html `<span part="actions"><slot name="actions"></slot></span>` : nothing}${this.renderWrapToggle()}${this.renderCopy(value, name)}</div>`
306
409
  : nothing;
307
410
  }
308
411
  renderWrapToggle() {
@@ -331,6 +434,7 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
331
434
  tooltip-arrow-top: copy-tooltip-arrow-top, tooltip-bottom: copy-tooltip-bottom,
332
435
  tooltip-left: copy-tooltip-left, tooltip-right: copy-tooltip-right, tooltip-top: copy-tooltip-top"
333
436
  copied-label=${this.copiedLabel}
437
+ @helix-press=${stopPress}
334
438
  label=${name ? formatWords(this, 'copyNameLabel', { copy: this.copyLabel, name }) : this.copyLabel}
335
439
  .value=${value}
336
440
  >${this.renderCopyIcons()}</${this.tag('copy-button')}>`;
@@ -434,6 +538,13 @@ __decorate([
434
538
  words,
435
539
  property({ attribute: 'expand-label' })
436
540
  ], HelixHeadlessCodeBlock.prototype, "expandLabel", void 0);
541
+ __decorate([
542
+ property()
543
+ ], HelixHeadlessCodeBlock.prototype, "file", void 0);
544
+ __decorate([
545
+ words,
546
+ property({ attribute: 'files-label' })
547
+ ], HelixHeadlessCodeBlock.prototype, "filesLabel", void 0);
437
548
  __decorate([
438
549
  words,
439
550
  property({ attribute: 'hide-unchanged-label' })
@@ -1,7 +1,7 @@
1
1
  import { AriaForwardController, HelixFormElement, PopoverController, UpgradeController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  import '../../listbox/index.js';
4
- import '../../tag/index.js';
4
+ import '../../tag/root/index.js';
5
5
  declare const HelixHeadlessCombobox_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerOpenInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinTokensInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerValueInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinFieldInterface) & typeof HelixFormElement;
6
6
  /**
7
7
  * @summary A text field that filters a list of options as you type.
@@ -668,6 +668,11 @@ export declare class HelixHeadlessCombobox extends HelixHeadlessCombobox_base {
668
668
  * slot is for the page and the method is for whoever is building the styled
669
669
  * layer, and a fallback with only the first is one a tier can restyle and
670
670
  * cannot change.
671
+ *
672
+ * **One em square by its own attributes**, which any stylesheet overrides.
673
+ * An inline `<svg>` with a `viewBox` and no size fills the width it is given:
674
+ * measured with no stylesheet, the caret was 1,264px tall, the field 1,289px,
675
+ * and the panel opened above the top of the page.
671
676
  */
672
677
  protected renderIndicator(): TemplateResult;
673
678
  /**
@@ -13,7 +13,7 @@ import { html } from 'lit/static-html.js';
13
13
  import { optionsAmong } from '../../option/root/option-root.js';
14
14
  import { HelixHeadlessTag } from '../../tag/root/tag-root.js';
15
15
  import '../../listbox/index.js';
16
- import '../../tag/index.js';
16
+ import '../../tag/root/index.js';
17
17
  const isInsertion = (event) => !event.inputType?.startsWith('delete');
18
18
  let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerOpen(MixinTokens(MixinControllerValue(MixinField(HelixFormElement)))) {
19
19
  constructor() {
@@ -612,7 +612,7 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
612
612
  }
613
613
  renderIndicator() {
614
614
  return html `<slot name="indicator">
615
- <svg part="indicator" fill="currentColor" viewBox=${caretDown.viewBox} aria-hidden="true">
615
+ <svg part="indicator" fill="currentColor" width="1em" height="1em" viewBox=${caretDown.viewBox} aria-hidden="true">
616
616
  ${unsafeSVG(caretDown.content)}
617
617
  </svg>
618
618
  </slot>`;
@@ -51,7 +51,7 @@ declare const HelixHeadlessDateInput_base: (new (...args: any[]) => import("@fus
51
51
  * | Page Up, Page Down on a segment | Steps by a page: a week of days, two months, or five years. |
52
52
  * | Home, End on a segment | Goes to the segment's first or last value. |
53
53
  * | Digits on a segment | Fill it, and move to the next once it can take no more. |
54
- * | Backspace, Delete on a segment | Empty the segment. |
54
+ * | Backspace, Delete on a segment | Take off its last digit, or move to the segment before where it is empty. |
55
55
  * | Left, Right on a segment | Move to the previous or the next segment, along the line of text. |
56
56
  *
57
57
  * In the calendar, the keys are `DatePicker`'s: the arrows move by a day or a