@fusion.dev/helix-headless 0.23.0 → 0.24.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 (102) hide show
  1. package/custom-elements.json +48376 -26638
  2. package/dist/announcement/root/announcement-root.d.ts +19 -5
  3. package/dist/area-chart/index.d.ts +1 -0
  4. package/dist/area-chart/index.js +1 -0
  5. package/dist/area-chart/root/area-chart-root.d.ts +182 -0
  6. package/dist/area-chart/root/area-chart-root.js +205 -0
  7. package/dist/carousel/root/carousel-root.d.ts +1 -0
  8. package/dist/carousel/root/carousel-root.js +18 -2
  9. package/dist/chat/index.d.ts +1 -1
  10. package/dist/chat/message/chat-message.d.ts +7 -0
  11. package/dist/chat/message/chat-message.js +2 -1
  12. package/dist/chat/root/chat-root.d.ts +195 -4
  13. package/dist/chat/root/chat-root.js +316 -23
  14. package/dist/checkbox-group/root/checkbox-group-root.d.ts +2 -0
  15. package/dist/checkbox-group/root/checkbox-group-root.js +12 -2
  16. package/dist/code-block/index.d.ts +1 -0
  17. package/dist/code-block/index.js +1 -0
  18. package/dist/code-block/patch.d.ts +62 -0
  19. package/dist/code-block/patch.js +93 -0
  20. package/dist/code-block/root/code-block-root.d.ts +82 -17
  21. package/dist/code-block/root/code-block-root.js +83 -6
  22. package/dist/color-picker/root/color-picker-root.d.ts +4 -3
  23. package/dist/color-picker/root/color-picker-root.js +4 -1
  24. package/dist/data-table/root/data-table-root.d.ts +195 -23
  25. package/dist/data-table/root/data-table-root.js +308 -27
  26. package/dist/description-list/index.d.ts +2 -0
  27. package/dist/description-list/index.js +2 -0
  28. package/dist/description-list/item/description-list-item.d.ts +39 -0
  29. package/dist/description-list/item/description-list-item.js +26 -0
  30. package/dist/description-list/root/description-list-root.d.ts +56 -0
  31. package/dist/description-list/root/description-list-root.js +21 -0
  32. package/dist/element-events.generated.js +27 -6
  33. package/dist/filter-input/chip/filter-chip.d.ts +114 -0
  34. package/dist/filter-input/chip/filter-chip.js +61 -0
  35. package/dist/filter-input/field/filter-field.d.ts +128 -0
  36. package/dist/filter-input/field/filter-field.js +70 -0
  37. package/dist/filter-input/grammar.d.ts +96 -0
  38. package/dist/filter-input/grammar.js +133 -0
  39. package/dist/filter-input/index.d.ts +5 -0
  40. package/dist/filter-input/index.js +5 -0
  41. package/dist/filter-input/menu/filter-menu.d.ts +153 -0
  42. package/dist/filter-input/menu/filter-menu.js +164 -0
  43. package/dist/filter-input/operator/filter-operator.d.ts +105 -0
  44. package/dist/filter-input/operator/filter-operator.js +39 -0
  45. package/dist/filter-input/root/filter-input-root.d.ts +118 -9
  46. package/dist/filter-input/root/filter-input-root.js +96 -2
  47. package/dist/format-date/root/format-date-root.js +1 -1
  48. package/dist/index.d.ts +5 -0
  49. package/dist/index.js +5 -0
  50. package/dist/line-chart/root/line-chart-root.d.ts +9 -2
  51. package/dist/listbox/root/listbox-root.d.ts +8 -7
  52. package/dist/listbox/root/listbox-root.js +7 -2
  53. package/dist/menu/item/menu-item.d.ts +31 -6
  54. package/dist/menu/item/menu-item.js +18 -5
  55. package/dist/menu/root/menu-root.js +7 -2
  56. package/dist/notifications/group/notification-group.d.ts +129 -0
  57. package/dist/notifications/group/notification-group.js +106 -0
  58. package/dist/notifications/index.d.ts +1 -0
  59. package/dist/notifications/index.js +1 -0
  60. package/dist/notifications/notification/notification.d.ts +39 -13
  61. package/dist/notifications/notification/notification.js +11 -8
  62. package/dist/notifications/root/notifications-root.d.ts +57 -1
  63. package/dist/notifications/root/notifications-root.js +35 -4
  64. package/dist/number-input/root/number-input-root.d.ts +21 -1
  65. package/dist/number-input/root/number-input-root.js +18 -5
  66. package/dist/object-inspector/index.d.ts +1 -0
  67. package/dist/object-inspector/index.js +1 -0
  68. package/dist/object-inspector/root/object-inspector-root.d.ts +183 -0
  69. package/dist/object-inspector/root/object-inspector-root.js +348 -0
  70. package/dist/outline/root/outline-root.d.ts +16 -1
  71. package/dist/outline/root/outline-root.js +27 -9
  72. package/dist/radio/group/radio-group.d.ts +1 -0
  73. package/dist/scatter-chart/index.d.ts +1 -0
  74. package/dist/scatter-chart/index.js +1 -0
  75. package/dist/scatter-chart/root/scatter-chart-root.d.ts +144 -0
  76. package/dist/scatter-chart/root/scatter-chart-root.js +205 -0
  77. package/dist/skip-link/index.d.ts +1 -0
  78. package/dist/skip-link/index.js +1 -0
  79. package/dist/skip-link/root/skip-link-root.d.ts +81 -0
  80. package/dist/skip-link/root/skip-link-root.js +58 -0
  81. package/dist/speed-dial/root/speed-dial-root.d.ts +1 -0
  82. package/dist/speed-dial/root/speed-dial-root.js +6 -2
  83. package/dist/steps/root/steps-root.d.ts +1 -0
  84. package/dist/table/column/table-column.d.ts +79 -0
  85. package/dist/table/column/table-column.js +125 -5
  86. package/dist/table/root/table-root.d.ts +2 -0
  87. package/dist/table/root/table-root.js +12 -3
  88. package/dist/time-picker/root/time-picker-root.d.ts +9 -0
  89. package/dist/time-picker/root/time-picker-root.js +5 -0
  90. package/dist/toast/root/toast-root.d.ts +2 -0
  91. package/dist/tooltip/root/tooltip-root.js +1 -1
  92. package/dist/tour/root/tour-root.d.ts +1 -0
  93. package/dist/tree/item/tree-item.d.ts +7 -4
  94. package/dist/tree/item/tree-item.js +4 -1
  95. package/dist/video/root/video-root.d.ts +23 -1
  96. package/dist/video/root/video-root.js +42 -22
  97. package/dist/virtual-list/root/virtual-list-root.d.ts +10 -6
  98. package/dist/virtual-list/root/virtual-list-root.js +10 -4
  99. package/package.json +5 -5
  100. package/types/jsx.d.ts +4535 -346
  101. package/types/svelte.d.ts +11 -0
  102. package/types/vue.d.ts +737 -39
@@ -27,6 +27,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
27
27
  this.step = 1;
28
28
  this.scrubFrom = 0;
29
29
  this.scrubValue = 0;
30
+ this.scrubStart = '';
30
31
  this.handleScrubDown = (event) => {
31
32
  if (this.isDisabled || this.readOnly) {
32
33
  return;
@@ -35,6 +36,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
35
36
  event.currentTarget.setPointerCapture(event.pointerId);
36
37
  this.scrubFrom = event.clientX;
37
38
  this.scrubValue = this.numeric ?? this.min ?? 0;
39
+ this.scrubStart = this.value;
38
40
  event.preventDefault();
39
41
  };
40
42
  this.handleScrubMove = (event) => {
@@ -58,8 +60,12 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
58
60
  };
59
61
  this.handleScrubEnd = (event) => {
60
62
  const grip = event.currentTarget;
61
- if (grip.hasPointerCapture(event.pointerId)) {
62
- grip.releasePointerCapture(event.pointerId);
63
+ if (!grip.hasPointerCapture(event.pointerId)) {
64
+ return;
65
+ }
66
+ grip.releasePointerCapture(event.pointerId);
67
+ if (this.value !== this.scrubStart) {
68
+ this.dispatchEvent(new Event('change', { bubbles: true, composed: false }));
63
69
  }
64
70
  };
65
71
  this.handleInput = (event) => {
@@ -98,13 +104,13 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
98
104
  this.min !== undefined &&
99
105
  !this.isDisabled &&
100
106
  !this.readOnly) {
101
- this.requestValue(String(this.min));
107
+ this.land(String(this.min));
102
108
  }
103
109
  else if (event.key === 'End' &&
104
110
  this.max !== undefined &&
105
111
  !this.isDisabled &&
106
112
  !this.readOnly) {
107
- this.requestValue(String(this.max));
113
+ this.land(String(this.max));
108
114
  }
109
115
  else {
110
116
  return;
@@ -142,11 +148,18 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
142
148
  const step = Math.abs(this.step) || 1;
143
149
  const from = this.numeric ?? this.min ?? 0;
144
150
  const next = this.clamp(this.numeric === null ? from : from + steps * step);
145
- this.requestValue(String(next));
151
+ this.land(String(next));
152
+ }
153
+ land(next) {
154
+ const before = this.value;
155
+ const applied = this.requestValue(next);
146
156
  const control = this.control;
147
157
  if (control) {
148
158
  restoreValue(control, this.value);
149
159
  }
160
+ if (applied && this.value !== before) {
161
+ this.dispatchEvent(new Event('change', { bubbles: true, composed: false }));
162
+ }
150
163
  }
151
164
  stepUp(large = false) {
152
165
  this.nudge(large ? (this.largeStep ?? 10) : 1);
@@ -0,0 +1 @@
1
+ export * from './root/object-inspector-root.js';
@@ -0,0 +1 @@
1
+ export * from './root/object-inspector-root.js';
@@ -0,0 +1,183 @@
1
+ import { HelixElement } from '@fusion.dev/helix-core';
2
+ import { type PropertyValues, type TemplateResult } from 'lit';
3
+ import '../../copy-button/index.js';
4
+ /** One visible line of the tree: a value, where it sits, and whether it is open. */
5
+ interface Row {
6
+ branch: boolean;
7
+ depth: number;
8
+ /** The property name or array index this value is under; absent for the root. */
9
+ key?: string;
10
+ open: boolean;
11
+ path: string;
12
+ position: number;
13
+ siblings: number;
14
+ value: unknown;
15
+ }
16
+ /**
17
+ * @summary A value read as a tree: collapse what you do not need, count what
18
+ * you collapsed, copy the part you do.
19
+ *
20
+ * @description
21
+ * What a developer-facing product shows constantly is a structured value: an
22
+ * API response, a config, a row's raw record. As text in a code block, a reader
23
+ * scrolls a wall of JSON and cannot put away the branch they do not care about;
24
+ * as a Tree, every node is a label and a string reads the same as a number. This
25
+ * reads the value and draws each entry with its type visible.
26
+ *
27
+ * **It reads and never edits.** An inspector that edits is a different
28
+ * component, with validation, undo and a model of what changed, and the reading
29
+ * one is what is wanted.
30
+ *
31
+ * **The data is text in the page or a property.** `for` names a
32
+ * `<script type="application/json">` by id, the channel `VirtualList` and the
33
+ * charts already read: it survives a server, which has attributes and no
34
+ * children, and React's hydration, which skips object properties. `value` is
35
+ * there for a page that already holds the object, and wins when both are set.
36
+ *
37
+ * **One tab stop, and the rows are reached with the arrow keys**, the tree
38
+ * pattern: Up and Down move, Right opens a branch or enters it, Left closes it or
39
+ * goes to its parent, Home and End go to the ends. The tree keeps focus and
40
+ * points at the active row with `aria-activedescendant`, which is what lets a
41
+ * windowed tree drop rows from the page without dropping focus with them. Each
42
+ * row carries its real level, position and set size, so a reader on a windowed
43
+ * row hears where it is in the value rather than in the slice drawn.
44
+ *
45
+ * **Copying takes the active row**, as JSON for a branch and as itself for a
46
+ * string, from the copy button or from Command or Control with C while the tree
47
+ * has focus and nothing is selected. The button sits beside the tree rather than in each row, where a
48
+ * button would be an interactive control inside a tree item, which assistive
49
+ * technology cannot reach reliably.
50
+ *
51
+ * **`windowed` draws only the rows in view**, for a value of thousands of
52
+ * entries, through the same controller `VirtualList` and `DataTable` use. The
53
+ * box has to have a height for that to mean anything, which the styled tier
54
+ * gives it and a headless consumer has to.
55
+ *
56
+ * @since 0.1.0
57
+ * @status experimental
58
+ * @category data
59
+ * @aliases JSON viewer, Object viewer, Inspector, Data explorer
60
+ * @documentation https://helix.fusion.dev/components/object-inspector
61
+ *
62
+ * @event {HelixCopyEvent} helix-copy - A value was put on the clipboard, from the copy button or the keyboard.
63
+ *
64
+ * @csspart after - The room the rows after the window would take, when windowed.
65
+ * @csspart before - The room the rows before the window would take, when windowed.
66
+ * @csspart boolean - A `true` or `false`.
67
+ * @csspart copy - The copy button, which copies the active row.
68
+ * @csspart copy-button - The inner button.
69
+ * @csspart copy-mark - The clipboard glyph, and the confirmation that replaces it.
70
+ * @csspart copy-tooltip - The bubble, forwarded from the tooltip inside.
71
+ * @csspart copy-tooltip-arrow - The point, resting against the trigger.
72
+ * @csspart count - How many entries a branch holds.
73
+ * @csspart frame - The tree and the copy button beside it.
74
+ * @csspart header - The bar carrying the label, where there is one.
75
+ * @csspart key - A property name or an array index.
76
+ * @csspart label - The label.
77
+ * @csspart null - A `null`.
78
+ * @csspart number - A number.
79
+ * @csspart row - One entry, a tree item.
80
+ * @csspart string - A string, in quotes.
81
+ * @csspart summary - The brackets standing for a branch's contents.
82
+ * @csspart toggle - The caret on a branch: down when open, across when closed.
83
+ * @csspart tree - The tree, the one thing focus lands on.
84
+ *
85
+ * @cssproperty --object-inspector-level - How deep a row is, from 0 at the root, for indenting it.
86
+ */
87
+ export declare class HelixHeadlessObjectInspector extends HelixElement {
88
+ /**
89
+ * How many levels start open.
90
+ *
91
+ * One by default: the root is open and everything under it closed, so a
92
+ * reader sees the shape of the value before its contents. `0` starts with the
93
+ * root closed; a large number opens everything, which on a big value is the
94
+ * wall of text this exists to avoid.
95
+ */
96
+ defaultDepth: number;
97
+ /**
98
+ * The id of a `<script type="application/json">` holding the value.
99
+ *
100
+ * Read once the document exists, and again whenever this changes. Text that
101
+ * does not parse leaves the inspector empty rather than throwing, since a
102
+ * page's markup should not take the page down over a comma.
103
+ */
104
+ for: string;
105
+ /**
106
+ * Accessible name for the tree, and the text of the header where there is one.
107
+ */
108
+ label: string;
109
+ /**
110
+ * Whether to leave the copy button off.
111
+ *
112
+ * Negative for the reason Code Block's is: a boolean attribute defaulting to
113
+ * true could never be turned off from markup.
114
+ */
115
+ noCopy: boolean;
116
+ /**
117
+ * The value itself, for a page that already holds it.
118
+ *
119
+ * Takes precedence over `for`. Anything JSON can say is drawn; anything else
120
+ * is shown as its string.
121
+ */
122
+ value: unknown;
123
+ /**
124
+ * Whether to draw only the rows in view.
125
+ *
126
+ * For a value of thousands of entries. Reflected, because the box it scrolls
127
+ * needs a height, which is a stylesheet rule reading this attribute.
128
+ */
129
+ windowed: boolean;
130
+ /** The value read from `for`, when there is no `value`. */
131
+ private read;
132
+ /** The `for` that `read` came from, so it is read again only when that changes. */
133
+ private loadedFor;
134
+ /** Which branches are open, by path. */
135
+ private opened;
136
+ /** The row the arrow keys are on, by its place among the visible rows. */
137
+ private active;
138
+ /** Set by a keyboard move, so the next render brings the active row into view. */
139
+ private revealing;
140
+ private readonly virtualize;
141
+ private get root();
142
+ /** Every visible row, in order, worked out once per change rather than per read. */
143
+ private rows;
144
+ private readonly copier;
145
+ /** Every visible row, in order, for the tree as it stands. */
146
+ private flatten;
147
+ /** Opens every branch shallower than `defaultDepth`. */
148
+ private seed;
149
+ protected willUpdate(changed: PropertyValues): void;
150
+ protected firstUpdated(changed: PropertyValues): void;
151
+ protected updated(changed: PropertyValues): void;
152
+ private load;
153
+ /**
154
+ * Brings the active row into the box.
155
+ *
156
+ * **A windowed tree may not have drawn it, and a jump straight to where it
157
+ * should be does not land.** The controller places rows it has not measured at
158
+ * its estimate, so an offset computed from a real row's height points at a
159
+ * different row, and asking again from there asks the same wrong question: the
160
+ * first version of this looped until the page stopped. So each attempt steps
161
+ * from the rows that *are* drawn toward the one wanted, by how many rows lie
162
+ * between, which closes the gap every time whatever the estimate is, and gives
163
+ * up after a few rather than trusting that it always will.
164
+ */
165
+ private reveal;
166
+ /** What copying takes: a branch as indented JSON, a string as itself. */
167
+ private copyable;
168
+ private toggle;
169
+ private move;
170
+ private readonly handleKeydown;
171
+ private handleRowClick;
172
+ private renderValue;
173
+ private renderRow;
174
+ /** The copy button, behind the seam Code Block's uses, so each tier renders its own. */
175
+ protected renderCopyButton(row: Row | undefined): TemplateResult;
176
+ protected render(): TemplateResult;
177
+ }
178
+ declare global {
179
+ interface HTMLElementTagNameMap {
180
+ 'helix-headless-object-inspector': HelixHeadlessObjectInspector;
181
+ }
182
+ }
183
+ export {};
@@ -0,0 +1,348 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
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
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { CopyController, customElement, HelixElement, VirtualizeController, } from '@fusion.dev/helix-core';
8
+ import { caretDown, caretRight } from '@fusion.dev/helix-icons';
9
+ import { html, nothing } from 'lit';
10
+ import { property, state } from 'lit/decorators.js';
11
+ import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
+ import { html as staticHtml } from 'lit/static-html.js';
13
+ import '../../copy-button/index.js';
14
+ const branchy = (value) => value !== null && typeof value === 'object';
15
+ const entries = (value) => Array.isArray(value) ? value.map((item, at) => [String(at), item]) : Object.entries(value);
16
+ const child = (path, key) => `${path}/${key.replaceAll('~', '~0').replaceAll('/', '~1')}`;
17
+ let HelixHeadlessObjectInspector = class HelixHeadlessObjectInspector extends HelixElement {
18
+ constructor() {
19
+ super(...arguments);
20
+ this.defaultDepth = 1;
21
+ this.for = '';
22
+ this.label = '';
23
+ this.noCopy = false;
24
+ this.value = undefined;
25
+ this.windowed = false;
26
+ this.read = undefined;
27
+ this.opened = new Set();
28
+ this.active = 0;
29
+ this.revealing = false;
30
+ this.virtualize = new VirtualizeController(this, {
31
+ container: () => this.windowed ? this.renderRoot?.querySelector('[part="tree"]') : null,
32
+ count: () => this.rows.length,
33
+ estimate: () => 24,
34
+ measured: () => this.windowed,
35
+ rows: () => [...(this.renderRoot?.querySelectorAll('[part="row"]') ?? [])].map((element) => ({ element, index: Number(element.dataset.index) })),
36
+ });
37
+ this.rows = [];
38
+ this.copier = new CopyController(this);
39
+ this.handleKeydown = (event) => {
40
+ const rows = this.rows;
41
+ const row = rows[this.active];
42
+ if (!row) {
43
+ return;
44
+ }
45
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'c') {
46
+ if (globalThis.getSelection?.()?.toString()) {
47
+ return;
48
+ }
49
+ void this.copier.copy(this.copyable(row));
50
+ event.preventDefault();
51
+ return;
52
+ }
53
+ switch (event.key) {
54
+ case 'ArrowDown':
55
+ this.move(this.active + 1);
56
+ break;
57
+ case 'ArrowUp':
58
+ this.move(this.active - 1);
59
+ break;
60
+ case 'Home':
61
+ this.move(0);
62
+ break;
63
+ case 'End':
64
+ this.move(rows.length - 1);
65
+ break;
66
+ case 'ArrowRight':
67
+ if (row.branch && !row.open) {
68
+ this.toggle(row, true);
69
+ }
70
+ else if (row.open) {
71
+ this.move(this.active + 1);
72
+ }
73
+ break;
74
+ case 'ArrowLeft':
75
+ if (row.open) {
76
+ this.toggle(row, false);
77
+ }
78
+ else {
79
+ const parent = rows.findLastIndex((candidate, at) => at < this.active && candidate.depth === row.depth - 1);
80
+ if (parent >= 0) {
81
+ this.move(parent);
82
+ }
83
+ }
84
+ break;
85
+ case 'Enter':
86
+ case ' ':
87
+ if (row.branch) {
88
+ this.toggle(row);
89
+ }
90
+ break;
91
+ default:
92
+ return;
93
+ }
94
+ event.preventDefault();
95
+ };
96
+ }
97
+ get root() {
98
+ return this.value === undefined ? this.read : this.value;
99
+ }
100
+ flatten() {
101
+ const rows = [];
102
+ if (this.root === undefined) {
103
+ return rows;
104
+ }
105
+ const visit = (value, path, depth, position, siblings, key) => {
106
+ const branch = branchy(value);
107
+ const open = branch && this.opened.has(path);
108
+ rows.push({
109
+ branch,
110
+ depth,
111
+ open,
112
+ path,
113
+ position,
114
+ siblings,
115
+ value,
116
+ ...(key === undefined ? {} : { key }),
117
+ });
118
+ if (open) {
119
+ const children = entries(value);
120
+ children.forEach(([name, item], at) => {
121
+ visit(item, child(path, name), depth + 1, at + 1, children.length, name);
122
+ });
123
+ }
124
+ };
125
+ visit(this.root, '', 0, 1, 1);
126
+ return rows;
127
+ }
128
+ seed() {
129
+ const opened = new Set();
130
+ const visit = (value, path, depth) => {
131
+ if (!branchy(value) || depth >= this.defaultDepth) {
132
+ return;
133
+ }
134
+ opened.add(path);
135
+ for (const [name, item] of entries(value)) {
136
+ visit(item, child(path, name), depth + 1);
137
+ }
138
+ };
139
+ visit(this.root, '', 0);
140
+ this.opened = opened;
141
+ this.active = 0;
142
+ }
143
+ willUpdate(changed) {
144
+ super.willUpdate(changed);
145
+ if (this.loadedFor !== this.for &&
146
+ typeof this.ownerDocument?.getElementById === 'function' &&
147
+ !this.hydrating) {
148
+ this.loadedFor = this.for;
149
+ this.load();
150
+ }
151
+ if (changed.has('value') || changed.has('read') || changed.has('defaultDepth')) {
152
+ this.seed();
153
+ }
154
+ if (changed.has('value') ||
155
+ changed.has('read') ||
156
+ changed.has('opened') ||
157
+ changed.has('defaultDepth')) {
158
+ this.rows = this.flatten();
159
+ this.active = Math.min(this.active, Math.max(0, this.rows.length - 1));
160
+ }
161
+ }
162
+ firstUpdated(changed) {
163
+ super.firstUpdated(changed);
164
+ this.finishHydration();
165
+ }
166
+ updated(changed) {
167
+ super.updated?.(changed);
168
+ if (this.windowed) {
169
+ this.virtualize.measure();
170
+ }
171
+ if (this.revealing) {
172
+ this.revealing = false;
173
+ this.reveal();
174
+ }
175
+ }
176
+ load() {
177
+ const text = this.for === '' ? '' : (this.ownerDocument.getElementById(this.for)?.textContent ?? '');
178
+ try {
179
+ this.read = text.trim() === '' ? undefined : JSON.parse(text);
180
+ }
181
+ catch {
182
+ this.read = undefined;
183
+ }
184
+ }
185
+ reveal(attempts = 8) {
186
+ const tree = this.renderRoot?.querySelector('[part="tree"]');
187
+ const row = tree?.querySelector(`[data-index="${this.active}"]`);
188
+ if (row) {
189
+ row.scrollIntoView({ block: 'nearest' });
190
+ return;
191
+ }
192
+ const drawn = [...(tree?.querySelectorAll('[part="row"]') ?? [])];
193
+ const first = drawn[0];
194
+ const last = drawn.at(-1);
195
+ if (!tree || !this.windowed || !first || !last || attempts === 0) {
196
+ return;
197
+ }
198
+ const height = first.offsetHeight || 24;
199
+ const from = Number(this.active > Number(last.dataset.index) ? last.dataset.index : first.dataset.index);
200
+ tree.scrollTop += (this.active - from) * height;
201
+ this.virtualize.measure();
202
+ void this.updateComplete.then(() => this.reveal(attempts - 1));
203
+ }
204
+ copyable(row) {
205
+ if (!row) {
206
+ return '';
207
+ }
208
+ if (typeof row.value === 'string') {
209
+ return row.value;
210
+ }
211
+ return JSON.stringify(row.value, null, 2) ?? String(row.value);
212
+ }
213
+ toggle(row, open = !row.open) {
214
+ const opened = new Set(this.opened);
215
+ if (open) {
216
+ opened.add(row.path);
217
+ }
218
+ else {
219
+ opened.delete(row.path);
220
+ }
221
+ this.opened = opened;
222
+ }
223
+ move(to) {
224
+ const last = this.rows.length - 1;
225
+ this.active = Math.max(0, Math.min(to, last));
226
+ this.revealing = true;
227
+ }
228
+ handleRowClick(index, row) {
229
+ this.active = index;
230
+ if (row.branch) {
231
+ this.toggle(row);
232
+ }
233
+ }
234
+ renderValue(row) {
235
+ const { value } = row;
236
+ if (row.branch) {
237
+ const count = entries(value).length;
238
+ const array = Array.isArray(value);
239
+ const noun = array ? (count === 1 ? 'item' : 'items') : count === 1 ? 'key' : 'keys';
240
+ return html `<span part="summary">${array ? '[…]' : '{…}'}</span>
241
+ <span part="count">${`${count} ${noun}`}</span>`;
242
+ }
243
+ if (typeof value === 'string') {
244
+ return html `<span part="string">${JSON.stringify(value)}</span>`;
245
+ }
246
+ if (typeof value === 'number') {
247
+ return html `<span part="number">${String(value)}</span>`;
248
+ }
249
+ if (typeof value === 'boolean') {
250
+ return html `<span part="boolean">${String(value)}</span>`;
251
+ }
252
+ if (value === null) {
253
+ return html `<span part="null">null</span>`;
254
+ }
255
+ return html `<span part="string">${String(value)}</span>`;
256
+ }
257
+ renderRow(row, index) {
258
+ return html `<div
259
+ part="row"
260
+ role="treeitem"
261
+ id=${`row-${index}`}
262
+ data-index=${index}
263
+ ?data-active=${index === this.active}
264
+ aria-level=${row.depth + 1}
265
+ aria-posinset=${row.position}
266
+ aria-setsize=${row.siblings}
267
+ aria-expanded=${row.branch ? String(row.open) : nothing}
268
+ style=${`--object-inspector-level: ${row.depth}`}
269
+ @click=${() => this.handleRowClick(index, row)}
270
+ >
271
+ ${row.branch
272
+ ? html `<svg
273
+ part="toggle"
274
+ fill="currentColor"
275
+ viewBox=${(row.open ? caretDown : caretRight).viewBox}
276
+ aria-hidden="true"
277
+ >
278
+ ${unsafeSVG((row.open ? caretDown : caretRight).content)}
279
+ </svg>`
280
+ : nothing}${row.key === undefined ? nothing : html `<span part="key">${row.key}</span>: `}${this.renderValue(row)}
281
+ </div>`;
282
+ }
283
+ renderCopyButton(row) {
284
+ return staticHtml `<${this.tag('copy-button')}
285
+ part="copy"
286
+ exportparts="button: copy-button, mark: copy-mark, tooltip: copy-tooltip,
287
+ tooltip-arrow: copy-tooltip-arrow"
288
+ label=${row?.key === undefined ? 'Copy value' : `Copy ${row.key}`}
289
+ .value=${this.copyable(row)}
290
+ ></${this.tag('copy-button')}>`;
291
+ }
292
+ render() {
293
+ const rows = this.rows;
294
+ const { after, before, first, last } = this.windowed
295
+ ? this.virtualize.window
296
+ : { after: 0, before: 0, first: 0, last: rows.length };
297
+ const shown = rows.slice(first, last);
298
+ return html `${this.label ? html `<div part="header"><span part="label">${this.label}</span></div>` : nothing}
299
+ <div part="frame">
300
+ <div
301
+ part="tree"
302
+ role="tree"
303
+ tabindex="0"
304
+ ?data-scroller=${this.windowed}
305
+ aria-label=${this.label || 'Value'}
306
+ aria-activedescendant=${rows.length > 0 ? `row-${this.active}` : nothing}
307
+ @keydown=${this.handleKeydown}
308
+ @scroll=${() => this.virtualize.measure()}
309
+ >
310
+ ${this.windowed ? html `<div part="before" role="presentation" style="block-size: ${before}px"></div>` : nothing}
311
+ ${shown.map((row, at) => this.renderRow(row, first + at))}
312
+ ${this.windowed ? html `<div part="after" role="presentation" style="block-size: ${after}px"></div>` : nothing}
313
+ </div>
314
+ ${this.noCopy ? nothing : this.renderCopyButton(rows[this.active])}
315
+ </div>`;
316
+ }
317
+ };
318
+ __decorate([
319
+ property({ attribute: 'default-depth', type: Number })
320
+ ], HelixHeadlessObjectInspector.prototype, "defaultDepth", void 0);
321
+ __decorate([
322
+ property()
323
+ ], HelixHeadlessObjectInspector.prototype, "for", void 0);
324
+ __decorate([
325
+ property()
326
+ ], HelixHeadlessObjectInspector.prototype, "label", void 0);
327
+ __decorate([
328
+ property({ attribute: 'no-copy', reflect: true, type: Boolean })
329
+ ], HelixHeadlessObjectInspector.prototype, "noCopy", void 0);
330
+ __decorate([
331
+ property({ attribute: false })
332
+ ], HelixHeadlessObjectInspector.prototype, "value", void 0);
333
+ __decorate([
334
+ property({ reflect: true, type: Boolean })
335
+ ], HelixHeadlessObjectInspector.prototype, "windowed", void 0);
336
+ __decorate([
337
+ state()
338
+ ], HelixHeadlessObjectInspector.prototype, "read", void 0);
339
+ __decorate([
340
+ state()
341
+ ], HelixHeadlessObjectInspector.prototype, "opened", void 0);
342
+ __decorate([
343
+ state()
344
+ ], HelixHeadlessObjectInspector.prototype, "active", void 0);
345
+ HelixHeadlessObjectInspector = __decorate([
346
+ customElement('helix-headless-object-inspector')
347
+ ], HelixHeadlessObjectInspector);
348
+ export { HelixHeadlessObjectInspector };
@@ -58,7 +58,8 @@ export declare class HelixHeadlessOutline extends HelixElement {
58
58
  */
59
59
  label: string;
60
60
  /**
61
- * How far below the top of the window a heading counts as reached, in pixels.
61
+ * How far below the top of what scrolls a heading counts as reached, in
62
+ * pixels: the window, or the panel the sections scroll in.
62
63
  *
63
64
  * The reading line. A page with a sticky masthead sets this to its height, or
64
65
  * a little more: without it the current section changes as a heading slides
@@ -119,6 +120,20 @@ export declare class HelixHeadlessOutline extends HelixElement {
119
120
  * measured against.
120
121
  */
121
122
  private get atEnd();
123
+ /** The element the page itself scrolls with, which `atEnd` explains. */
124
+ private get page();
125
+ /**
126
+ * What the sections scroll in: the nearest ancestor of the first one that
127
+ * scrolls, or the page when none does.
128
+ *
129
+ * **The window is not always what moves.** A contents rail beside a panel
130
+ * with its own scrollbar, which is a docs layout and every sandbox preview,
131
+ * measured the window. The window never scrolled, so it was always at its end
132
+ * and the last section was always current: the events example named `api`
133
+ * at rest with `install` on screen, a wheel over the panel changed nothing,
134
+ * and a press on `install` marked it for a moment and snapped back.
135
+ */
136
+ private get scroller();
122
137
  /** The frame a mark is already scheduled for, so a scroll asks for one. */
123
138
  private frame;
124
139
  /** Where a click said to go, while the page is still on its way there. */